G마켓 챌린지가 안 풀린 채 25~70초를 먹던 문제에 상한을 씌운다. 폴백은 '있으면 좋은' 보강이라 데드라인으로 잘라도 안전(그 몰만 스킵). - handler: 각 폴백 크롤에 fallback_deadline_sec(기본 15s) wait_for. 초과 시 스킵. _timed_search 예외를 BaseException 로(CancelledError 계측 포함). - 폴백 어댑터 max_block_retries=0(챌린지 대기 2배 방지), ESM ready_timeout 22→12s, st11 20→15s. - 테스트: 느린 폴백 데드라인 스킵 1종. 실측(동시성2·4상품): 상품당 p50 84.6s→34.2s(~2.5x), 벽시계 181s→65s(~2.8x), 처리량 1.3→3.7 상품/분, 1000건 $7.12→$5.06(~29%↓, 실패 챌린지 대역폭 절감). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
40 lines
2.2 KiB
Python
40 lines
2.2 KiB
Python
"""ESM(G마켓·옥션) 검색 어댑터 — 네이버 폴백용(네이버에 해당 몰이 없을 때만 크롤).
|
|
|
|
두 사이트는 eBay Korea 백엔드 공유 + '잠시만 기다리십시오…' JS 챌린지(수 초 후 자동 통과)가
|
|
동일해, BrowserSearchAdapter 위에 site 파라미터 하나로 커버한다(카드/셀렉터만 분기).
|
|
챌린지는 ready_selector(카드) 가 렌더될 때까지 대기하면 자연히 통과한다.
|
|
"""
|
|
|
|
from urllib.parse import quote
|
|
|
|
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", "비정상적인 접근", "잠시만 기다리")
|
|
|
|
|
|
class EsmAdapter(BrowserSearchAdapter):
|
|
block_markers = _BLOCK_MARKERS
|
|
min_result_html = 8000 # 챌린지 페이지(~21KB)는 크므로 short 폴백은 명백한 에러만
|
|
ready_timeout_ms = 12000 # 챌린지 통과(정상 ~9초). 폴백은 데드라인이 상한이라 과도한 대기 회피
|
|
block_resources_default = True # 라우팅 on — 단, 아래 집합만(CSS/JS 유지)
|
|
blocked_resource_types = {"image", "media", "font"} # 대역폭 대부분(이미지)만 차단, 챌린지/렌더용 CSS/JS는 유지
|
|
|
|
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)
|