소스 어댑터 패턴으로 크롤링을 시작한다. 쿠팡은 Akamai Bot Manager 의 JS 행동 챌린지라 curl_cffi 단독 불가 → Patchright(스텔스 Playwright) + 실제 Chrome(channel=chrome)으로 챌린지를 통과하고 selectolax 로 파싱한다. - contract: SearchAdapter(ABC)·NormalizedProduct·AdapterHealth·AdapterError - coupang: adapter(브라우저 재사용, 챌린지 1회)·parser(순수)·selectors(외부화, webpack 해시 prefix 매칭) - 공용: rate_limiter(2~8s 랜덤)·proxy(무프록시 off 인터페이스)·util.parse_price - 가격 파싱: 단위가격 '(1개당 44,400원)'의 앞 '1' 오인 방지 — '원' 앞 숫자 앵커링 - deps: curl_cffi·selectolax·patchright(nodriver 는 py3.14 버그로 대체) - tests: 파서 회귀(소형 fixture) — 라이브 2회 검색 + 파서 3/3 통과 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 lines
915 B
Python
26 lines
915 B
Python
"""프록시 풀 인터페이스.
|
|
|
|
현재는 무프록시로 시작(residential 프록시 미보유). 인터페이스만 두고 off 상태로 동작한다.
|
|
쿠팡 차단이 발생하면 residential/mobile 프록시 목록을 주입해 로테이션을 켠다.
|
|
(datacenter IP 는 금방 차단되므로 residential/mobile 을 전제로 설계 — new.md)
|
|
"""
|
|
|
|
import itertools
|
|
from typing import Optional
|
|
|
|
|
|
class ProxyPool:
|
|
def __init__(self, proxies: Optional[list[str]] = None):
|
|
self._proxies = list(proxies or [])
|
|
self._cycle = itertools.cycle(self._proxies) if self._proxies else None
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return bool(self._proxies)
|
|
|
|
def get(self) -> Optional[str]:
|
|
"""다음 프록시 URL 반환. 비어있으면(off) None → 어댑터는 직접 연결."""
|
|
if self._cycle is None:
|
|
return None
|
|
return next(self._cycle)
|