G마켓/옥션 Cloudflare Turnstile 을 실제로 통과시키고, 쌓이는 브라우저를 정리한다. Turnstile 규명(실측): patchright 가 콜드 ~12초에 자동 통과, 통과 후 cf_clearance 쿠키로 이후 요청은 ~5초(웜). 단 **리소스를 조금이라도 차단하면 Turnstile 지문검사에 걸려 실패** (이미지만 막아도 실패). 하지만 **cf_clearance 확보 후엔 차단해도 재챌린지 없음**. - 동적 차단: _block_active + _blocking_now() 훅. ESM 은 cf_clearance 있을 때만 차단 (콜드=허용→Turnstile 통과, 웜=이미지/미디어/폰트 차단→대역폭↓). ready_timeout 25s. best-effort Turnstile 체크박스 클릭(인터랙티브/나쁜IP 대비). - 웜업: 기동 시 gmarket/auction/coupang 를 1회 풀어 쿠키 선점(백그라운드) → 실 작업은 웜. - 유휴 브라우저 정리: _last_used + close_if_idle + run_browser_reaper(유휴 120s) — 메모리 회수. 쿠키는 user_data_dir 에 남아 재기동해도 (같은 IP면) 웜 유지. - 테스트: 유휴 정리 2종. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
4.5 KiB
Python
88 lines
4.5 KiB
Python
"""ESM(G마켓·옥션) 검색 어댑터 — 네이버 폴백용(네이버에 해당 몰이 없을 때만 크롤).
|
|
|
|
두 사이트는 eBay Korea 백엔드 공유 + '잠시만 기다리십시오…' JS 챌린지(수 초 후 자동 통과)가
|
|
동일해, BrowserSearchAdapter 위에 site 파라미터 하나로 커버한다(카드/셀렉터만 분기).
|
|
챌린지는 ready_selector(카드) 가 렌더될 때까지 대기하면 자연히 통과한다.
|
|
"""
|
|
|
|
import time
|
|
from urllib.parse import quote
|
|
|
|
from common.logger import LOG
|
|
from services.search.browser_base import BrowserSearchAdapter
|
|
from services.search.contract import NormalizedProduct
|
|
from services.search.card_parser import parse_cards
|
|
from services.search.esm.selectors import SITES
|
|
|
|
# ESM 차단 마커. '잠시만 기다리십시오' 챌린지는 렌더 완료 후에도(parse=0) 남아 있으면 = 이 IP 가
|
|
# 챌린지를 못 푼 것 → 차단으로 간주해 IP 회전 재시도를 유도한다(정상 시엔 카드가 떠 parse>0 이라 여기 안 옴).
|
|
_BLOCK_MARKERS = ("errors.edgesuite.net", "You don't have permission to access", "비정상적인 접근", "잠시만 기다리")
|
|
|
|
|
|
# Cloudflare Turnstile('사람인지 확인' 체크박스) 위젯 iframe.
|
|
_CF_IFRAME = 'iframe[src*="challenges.cloudflare.com"]'
|
|
|
|
|
|
class EsmAdapter(BrowserSearchAdapter):
|
|
block_markers = _BLOCK_MARKERS
|
|
min_result_html = 8000 # 챌린지 페이지(~21KB)는 크므로 short 폴백은 명백한 에러만
|
|
# G마켓/옥션은 Cloudflare Turnstile. patchright 가 콜드 ~12초에 자동 통과하고, 통과하면 cf_clearance
|
|
# 쿠키로 이후 요청은 ~5초(웜). 콜드 자동통과 여유로 25초까지 대기(잡은 폴백 데드라인 15s 가 상한).
|
|
ready_timeout_ms = 25000
|
|
# Cloudflare Turnstile 은 리소스 로딩 패턴까지 지문검사 → 챌린지 solving 중 차단하면 봇 판정(이미지만 막아도 실패).
|
|
# 하지만 **cf_clearance 확보(웜) 후엔 차단해도 재챌린지 없음**(실측) → 동적 차단: 콜드=허용, 웜=차단.
|
|
block_resources_default = True
|
|
blocked_resource_types = {"image", "media", "font"}
|
|
|
|
def __init__(self, site: str, **kwargs):
|
|
if site not in SITES:
|
|
raise ValueError(f"unknown ESM site: {site}")
|
|
self._site = SITES[site]
|
|
self.source = site # base __init__ 의 user_data_dir 기본값에 쓰임
|
|
self.ready_selector = self._site.ready_selector
|
|
super().__init__(**kwargs)
|
|
|
|
def _search_url(self, query: str, limit: int) -> str:
|
|
return self._site.search_url.format(q=quote(query))
|
|
|
|
def _parse(self, html: str) -> list[NormalizedProduct]:
|
|
return parse_cards(html, self._site.cards)
|
|
|
|
async def _blocking_now(self) -> bool:
|
|
"""Turnstile 챌린지를 이미 통과(cf_clearance 쿠키 있음)했을 때만 리소스 차단.
|
|
콜드(쿠키 없음)엔 차단하면 Turnstile 지문검사에 걸리므로 허용."""
|
|
if not self._block_resources or self._ctx is None:
|
|
return False
|
|
try:
|
|
cookies = await self._ctx.cookies(self._site.search_url)
|
|
return any(c.get("name") == "cf_clearance" for c in cookies)
|
|
except Exception:
|
|
return False
|
|
|
|
async def _wait_ready(self, page):
|
|
"""Turnstile 대응 렌더 대기: 카드가 뜰 때까지 폴링하되, 매 회 인터랙티브 체크박스면 클릭 시도.
|
|
대부분은 patchright 가 자동 통과(클릭 무해). cf_clearance 웜 상태면 카드가 곧바로 뜬다."""
|
|
deadline = time.monotonic() + self.ready_timeout_ms / 1000
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
if await page.query_selector(self.ready_selector):
|
|
return
|
|
except Exception:
|
|
pass
|
|
await self._try_pass_turnstile(page)
|
|
await page.wait_for_timeout(1000)
|
|
|
|
async def _try_pass_turnstile(self, page):
|
|
"""Turnstile 인터랙티브 체크박스(나쁜 IP 에스컬레이션)면 클릭. best-effort — 없거나 실패해도 무시.
|
|
위젯은 challenges.cloudflare.com cross-origin iframe 안에 있다."""
|
|
try:
|
|
fl = page.frame_locator(_CF_IFRAME)
|
|
for sel in ('input[type="checkbox"]', 'label'):
|
|
loc = fl.locator(sel)
|
|
if await loc.count():
|
|
await loc.first.click(timeout=1200)
|
|
LOG.d(f"[{self.source}] Turnstile 체크박스 클릭 시도")
|
|
return
|
|
except Exception:
|
|
pass
|