403 차단 우회와 웹 방화벽 봇 탐지 회피 전략
업데이트: 2026-10-02
파이썬의 requests나 기본 Playwright로 데이터 수집을 시도했을 때 응답 코드 403 Forbidden이나 Cloudflare의 보안 확인(Turnstile/대기 화면)이 나타나는 이유는 단순히 User-Agent 문자열 때문이 아닙니다.
최신 웹 보안 엔진은 브라우저의 내부 자바스크립트 변수(navigator.webdriver), 그래픽 렌더러 지문, 그리고 요청 간격의 기계적 규칙성까지 다층적으로 검증합니다. 실전 수집 환경에서 차단율을 90% 이상 낮추는 핵심 방어 기법들을 상세히 다룹니다.
최신 웹 방화벽이 크롤러를 식별하는 4가지 기준
- 자동화 플래그 노출 (
navigator.webdriver): Playwright나 Selenium이 실행한 브라우저는 자바스크립트 엔진 내부에navigator.webdriver = true라는 플래그를 기본적으로 들고 있습니다. 보안 스크립트는 이 변수 하나만으로 즉시 봇 판정을 내리고 차단합니다. - 그래픽 렌더러 지문 (WebGL / Canvas):
가상 서버 환경에서는 실제 외장 그래픽 카드가 없기 때문에 WebGL 렌더러 이름으로
SwiftShader나llvmpipe같은 소프트웨어 가상 드라이버 이름이 노출되어 자동화 샌드박스임이 들통납니다. - 기계적인 고정 주기 요청: 정확히 3.0초나 5.0초마다 오차 없이 요청을 반복하는 행위는 행동 분석 엔진이 크롤러를 식별하는 가장 손쉬운 지표입니다.
- 취약한 절대 경로 XPath 파싱:
사이트가 약간만 레이아웃을 개편해도
/html/body/div[2]/div[1]/span같은 절대 경로는 즉시 파싱 에러를 발생시켜 크롤러를 중단시킵니다.
회피 기법 1: playwright-stealth 자동화 플래그 은폐
playwright-stealth 모듈은 브라우저 컨텍스트가 생성되는 순간, 자동화 탐지에 사용되는 내부 자바스크립트 프로퍼티를 실제 일반 사용자의 크롬 브라우저와 동일하게 위장합니다.
playwright>=1.43.0
playwright-stealth>=1.0.6from 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) 대신, 정규분포(가우시안 분포) 기반의 불규칙한 지연 시간을 적용하여 기계적 패턴을 제거합니다.
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 규격의 상품 가격과 재고 정보를 내장하고 있습니다.
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완성형 프로덕션 안티봇 크롤러 템플릿
스텔스 플래그 위장, 랜덤 지터, 구조화 데이터 추출이 결합된 견고한 수집 코드입니다.
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단계 (기본 사이트): 올바른
User-Agent와 자연스러운 헤더(Accept-Language,Referer)만으로 충분합니다. - 2단계 (중간 보안 WAF):
playwright-stealth를 적용하여 자동화 플래그를 숨기고 가변 지연 시간(랜덤 지터)을 적용합니다. - 3단계 (고난도 보안 사이트): 데이터센터 IP 자체가 블랙리스트 처리된 경우, 주거용 회전 프록시 연동 가이드를 참고하여 주거용 IP 풀을 투입하세요.