크롤러 / 스크래퍼

403 차단 우회와 웹 방화벽 봇 탐지 회피 전략

Cloudflare 및 웹 방화벽의 봇 식별 원리, playwright-stealth 연동, 가변 지연 시간(랜덤 지터) 및 변경에 강한 셀렉터 설계.

업데이트: 2026-10-02

파이썬의 requests나 기본 Playwright로 데이터 수집을 시도했을 때 응답 코드 403 Forbidden이나 Cloudflare의 보안 확인(Turnstile/대기 화면)이 나타나는 이유는 단순히 User-Agent 문자열 때문이 아닙니다.

최신 웹 보안 엔진은 브라우저의 내부 자바스크립트 변수(navigator.webdriver), 그래픽 렌더러 지문, 그리고 요청 간격의 기계적 규칙성까지 다층적으로 검증합니다. 실전 수집 환경에서 차단율을 90% 이상 낮추는 핵심 방어 기법들을 상세히 다룹니다.


최신 웹 방화벽이 크롤러를 식별하는 4가지 기준

  1. 자동화 플래그 노출 (navigator.webdriver): Playwright나 Selenium이 실행한 브라우저는 자바스크립트 엔진 내부에 navigator.webdriver = true라는 플래그를 기본적으로 들고 있습니다. 보안 스크립트는 이 변수 하나만으로 즉시 봇 판정을 내리고 차단합니다.
  2. 그래픽 렌더러 지문 (WebGL / Canvas): 가상 서버 환경에서는 실제 외장 그래픽 카드가 없기 때문에 WebGL 렌더러 이름으로 SwiftShader나 llvmpipe 같은 소프트웨어 가상 드라이버 이름이 노출되어 자동화 샌드박스임이 들통납니다.
  3. 기계적인 고정 주기 요청: 정확히 3.0초나 5.0초마다 오차 없이 요청을 반복하는 행위는 행동 분석 엔진이 크롤러를 식별하는 가장 손쉬운 지표입니다.
  4. 취약한 절대 경로 XPath 파싱: 사이트가 약간만 레이아웃을 개편해도 /html/body/div[2]/div[1]/span 같은 절대 경로는 즉시 파싱 에러를 발생시켜 크롤러를 중단시킵니다.

회피 기법 1: playwright-stealth 자동화 플래그 은폐

playwright-stealth 모듈은 브라우저 컨텍스트가 생성되는 순간, 자동화 탐지에 사용되는 내부 자바스크립트 프로퍼티를 실제 일반 사용자의 크롬 브라우저와 동일하게 위장합니다.

requirements.txt에 추가
playwright>=1.43.0
playwright-stealth>=1.0.6
stealth_example.py
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
 
with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    
    # 일반 크롬 브라우저와 동일한 설정으로 컨텍스트 생성
    context = browser.new_context(
        viewport={"width": 1920, "height": 1080},
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        locale="ko-KR",
        timezone_id="Asia/Seoul"
    )
    
    page = context.new_page()
    
    # [핵심] stealth 모듈 적용: navigator.webdriver 숨김, WebGL/플러그인 위장
    stealth_sync(page)
    
    # 봇 탐지 테스트 사이트 접속 검증
    page.goto("https://bot.sannysoft.com", timeout=30000)
    print("스텔스 모드 적용 완료. WebDriver 플래그가 완벽히 숨겨졌습니다.")
    
    context.close()
    browser.close()

회피 기법 2: 불규칙한 가변 지연 시간 (랜덤 지터)

인간의 웹 브라우징은 글을 읽는 속도와 마우스 움직임에 따라 페이지 간 체류 시간이 불규칙합니다.

고정된 time.sleep(5) 대신, 정규분포(가우시안 분포) 기반의 불규칙한 지연 시간을 적용하여 기계적 패턴을 제거합니다.

human_delay.py
import time
import random
import logging
 
logger = logging.getLogger("Delay")
 
def human_like_delay(mean: float = 4.0, std_dev: float = 1.2, min_delay: float = 2.0):
    """
    평균 4.0초, 표준편차 1.2초의 자연스러운 가변 지연 시간 생성
    (실제 지연: 2.1초 ~ 6.5초 사이로 자연스럽게 분산됨)
    """
    delay = random.gauss(mean, std_dev)
    delay = max(min_delay, delay)  # 최소 대기 시간 보장
    
    logger.debug(f"자연스러운 인간형 딜레이 적용: {delay:.2f}초 대기")
    time.sleep(delay)

회피 기법 3: 변경에 강한 복합 셀렉터 및 구조화 데이터 활용

쇼핑몰이나 포털 사이트가 디자인을 리뉴얼하더라도 크롤러가 부러지지 않도록 데이터 위치를 추적하는 우선순위 원칙입니다.

[ 셀렉터 설계 3대 우선순위 ]
1순위 (가장 안전): <script type="application/ld+json"> 구조화 데이터 (디자인이 바뀌어도 유지)
2순위 (안전):      data-testid, data-product-id, data-price 등 의미론적 속성
3순위 (주의 필요): 시각적 CSS 클래스 (.product-title, .price-now)
비추천 (절대 금지): /html/body/div[3]/div[2]/span (배너 하나만 추가되어도 파싱 에러)

JSON-LD 구조화 데이터 추출 예시 (추천)

대부분의 현대 이커머스 사이트는 SEO 최적화를 위해 HTML 헤더에 순수 JSON 규격의 상품 가격과 재고 정보를 내장하고 있습니다.

extract_json_ld.py
import json
from bs4 import BeautifulSoup
 
def extract_product_data(html_content: str):
    soup = BeautifulSoup(html_content, "html.parser")
    # ld+json 스크립트 태그 탐색
    for script in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(script.string)
            if data.get("@type") in ["Product", "Offer"]:
                return {
                    "name": data.get("name"),
                    "price": data.get("offers", {}).get("price") or data.get("price"),
                    "in_stock": "InStock" in str(data.get("offers", {}).get("availability", ""))
                }
        except (json.JSONDecodeError, TypeError):
            continue
    return None

완성형 프로덕션 안티봇 크롤러 템플릿

스텔스 플래그 위장, 랜덤 지터, 구조화 데이터 추출이 결합된 견고한 수집 코드입니다.

stealth_crawler.py
import logging
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
from human_delay import human_like_delay
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("StealthCrawler")
 
def crawl_safe(target_url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=[
                "--no-sandbox",
                "--disable-dev-shm-usage",
                "--disable-blink-features=AutomationControlled", # 자동화 감지 무력화
            ]
        )
 
        context = browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
        )
 
        page = context.new_page()
        # 스텔스 플래그 주입
        stealth_sync(page)
 
        try:
            logger.info(f"페이지 접근 시도: {target_url}")
            page.goto(target_url, wait_until="domcontentloaded", timeout=30000)
 
            # 자연스러운 인간형 체류 딜레이
            human_like_delay(mean=3.5, std_dev=1.0)
 
            # 제목 확인
            title = page.title()
            logger.info(f"접근 성공: {title}")
 
        except Exception as e:
            logger.error(f"접근 차단 또는 오류 발생: {e}")
        finally:
            context.close()
            browser.close()
 
if __name__ == "__main__":
    crawl_safe("https://example.com")

대상 사이트 방어 수준별 점진적 대응 가이드

  1. 1단계 (기본 사이트): 올바른 User-Agent와 자연스러운 헤더(Accept-Language, Referer)만으로 충분합니다.
  2. 2단계 (중간 보안 WAF): playwright-stealth를 적용하여 자동화 플래그를 숨기고 가변 지연 시간(랜덤 지터)을 적용합니다.
  3. 3단계 (고난도 보안 사이트): 데이터센터 IP 자체가 블랙리스트 처리된 경우, 주거용 회전 프록시 연동 가이드를 참고하여 주거용 IP 풀을 투입하세요.

다음 추천 문서