diff --git a/app/core/crawl_politeness.py b/app/core/crawl_politeness.py index abcbaf7..bba760a 100644 --- a/app/core/crawl_politeness.py +++ b/app/core/crawl_politeness.py @@ -40,6 +40,16 @@ _AUTH_DELAY_MAX = 60.0 _FETCHER_URL = "http://playwright-fetcher:3400" _FETCHER_TIMEOUT = 120.0 # 브라우저 기동 + 네비게이션 + settle 포함 +# 안티봇 챌린지 페이지 식별 마커 (DataDome/Cloudflare 등) — 좁게 유지(오탐 회피). +# 실측: 르몽드 기사 = DataDome "Client Challenge" + "Entrez les caractères" CAPTCHA. +_CHALLENGE_MARKERS = ( + "Client Challenge", + "Entrez les caractères affichés", + "Checking your browser before", + "captcha-delivery.com", + "geo.captcha-delivery", +) + _ROBOTS_CACHE_TTL = 24 * 3600 # 24h _MAX_PAGE_BYTES = 5 * 1024 * 1024 # 피드 fetch 와 동일 5MB cap _PAGE_TIMEOUT = 20.0 @@ -230,6 +240,11 @@ async def fetch_page_via_browser(url: str, profile: str) -> tuple[str, str]: html_text = data.get("html", "") if len(html_text.encode("utf-8", errors="replace")) > _MAX_PAGE_BYTES: raise CrawlSkip(f"크기 초과 (browser): {url}") + # 안티봇 챌린지 페이지(DataDome 등) 식별 — 본문 길이 게이트(200자)를 통과하는 + # 짧은 챌린지 HTML 이 기사 본문으로 승격되는 silent corruption 차단. 헤드리스 탐지라 + # 재시도 무의미 → CrawlBlocked(=degrade, RSS 요약 유지). 마커는 보수적으로 좁게. + if any(m in html_text for m in _CHALLENGE_MARKERS): + raise CrawlBlocked(f"안티봇 챌린지 페이지(headless 차단): {url}") return html_text, data.get("final_url", url)