소스 어댑터 패턴으로 크롤링을 시작한다. 쿠팡은 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>
27 lines
954 B
Python
27 lines
954 B
Python
"""정중한 요청을 위한 랜덤 딜레이 레이트리미터.
|
|
|
|
기계적 등간격 요청은 탐지 신호(new.md). 요청 사이에 2~8s 랜덤 간격을 둔다.
|
|
소스(도메인)별로 인스턴스를 두고 마지막 요청 시각을 기준으로 최소 간격을 보장한다.
|
|
"""
|
|
|
|
import asyncio
|
|
import random
|
|
import time
|
|
|
|
|
|
class RateLimiter:
|
|
def __init__(self, min_delay: float = 2.0, max_delay: float = 8.0):
|
|
self._min = min_delay
|
|
self._max = max_delay
|
|
self._last = 0.0
|
|
self._lock = asyncio.Lock()
|
|
|
|
async def wait(self) -> None:
|
|
"""직전 요청으로부터 랜덤 간격이 지나도록 대기(동시 호출 직렬화)."""
|
|
async with self._lock:
|
|
target = random.uniform(self._min, self._max)
|
|
elapsed = time.monotonic() - self._last
|
|
if elapsed < target:
|
|
await asyncio.sleep(target - elapsed)
|
|
self._last = time.monotonic()
|