fix(services): playwright-fetcher CF JS 챌린지 통과 대기 — aiche.org 인터스티셜 스냅샷 함정 (검증 게이트 발견)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user