한 검색의 소요·DECODO 비용을 크게 낮춘다(실측: 110s→52s, proxy 8MB→2MB, 총비용 70%↓). - 폴백 병렬화: _enrich_with_fallback 의 순차 for-loop → gather 동시 크롤. 각 몰 어댑터는 별 브라우저 인스턴스라 병렬 안전. 소요=합→최댓값. AI usage 계측 동시성 불변식 주석 명시 (judge.last_usage 세팅~읽기 사이 await 없음 → asyncio 협조 스케줄링상 안전). - 대역폭: blocked_resource_types 를 어댑터별 설정화. 오픈마켓은 이미지/미디어/폰트만 차단 (CSS/JS 유지 — 챌린지/렌더 보호). 격리 실험으로 차단 유무가 ESM 챌린지 성패에 무관함 확인 (실패 원인은 IP평판/headless). st11 라이브 0.11MB 검증. 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 = 22000 # '잠시만' 챌린지 통과(~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)
|