fix(services): playwright-fetcher CF JS 챌린지 통과 대기 — aiche.org 인터스티셜 스냅샷 함정 (검증 게이트 발견)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-06-11 07:23:58 +09:00
parent 8583465c58
commit f3530e382d
2 changed files with 20 additions and 2 deletions
+3
View File
@@ -49,6 +49,9 @@ _CHALLENGE_MARKERS = (
"Checking your browser before",
"captcha-delivery.com",
"geo.captcha-delivery",
# CF JS 챌린지 인터스티셜의 스크립트 도메인 (aiche.org 실측 2026-06-11) —
# fetcher 의 챌린지 대기를 끝까지 통과 못 한 최종 HTML 만 여기 걸린다.
"challenges.cloudflare.com",
)
_ROBOTS_CACHE_TTL = 24 * 3600 # 24h
+17 -2
View File
@@ -30,6 +30,11 @@ AUTH_DIR = Path("/auth")
NAV_TIMEOUT_MS = 45_000
SETTLE_MS = 1_500 # domcontentloaded 후 lazy 본문 settle 대기
MAX_DOWNLOAD_BYTES = 60 * 1024 * 1024
# Cloudflare JS 챌린지(title='Just a moment...')는 통과에 수 초 + 자동 재네비게이션이
# 걸린다 — aiche.org 실측(2026-06-11): 1.5s settle 시점 스냅샷 = 인터스티셜.
# 통과 못 하면 호출측 _CHALLENGE_MARKERS 가 최종 HTML 에서 차단 판정.
CHALLENGE_POLL_TRIES = 8
CHALLENGE_POLL_MS = 2_500
app = FastAPI(title="playwright-fetcher")
_browser_slot = asyncio.Semaphore(1) # 동시 1 인스턴스 (B-3 ① persistent 제약과 동일 규율)
@@ -73,6 +78,16 @@ def _context_kwargs(state: Path | None) -> dict:
return kwargs
async def _settle(page) -> None:
"""기본 settle + CF JS 챌린지 통과 대기 (통과 실패 시 인터스티셜 그대로 반환)."""
await page.wait_for_timeout(SETTLE_MS)
for _ in range(CHALLENGE_POLL_TRIES):
title = (await page.title()).lower()
if "just a moment" not in title:
return
await page.wait_for_timeout(CHALLENGE_POLL_MS)
async def _browse(url: str, state: Path | None) -> tuple[str, str, str]:
"""(html, final_url, visible_text). 요청당 브라우저 — 종료를 finally 로 보장."""
async with async_playwright() as pw:
@@ -81,7 +96,7 @@ async def _browse(url: str, state: Path | None) -> tuple[str, str, str]:
context = await browser.new_context(**_context_kwargs(state))
page = await context.new_page()
await page.goto(url, wait_until="domcontentloaded", timeout=NAV_TIMEOUT_MS)
await page.wait_for_timeout(SETTLE_MS)
await _settle(page)
html = await page.content()
final_url = page.url
text = await page.evaluate("document.body ? document.body.innerText : ''")
@@ -127,7 +142,7 @@ async def download(req: DownloadReq):
page = await context.new_page()
await page.goto(req.referer, wait_until="domcontentloaded",
timeout=NAV_TIMEOUT_MS)
await page.wait_for_timeout(SETTLE_MS)
await _settle(page) # CF 챌린지 통과 쿠키를 컨텍스트에 적재
resp = await context.request.get(req.url, timeout=NAV_TIMEOUT_MS)
body = await resp.body()
finally: