소스 어댑터 패턴으로 크롤링을 시작한다. 쿠팡은 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>
28 lines
1.2 KiB
Python
28 lines
1.2 KiB
Python
# 쿠팡 파서 결정론적 단위 테스트(네트워크/브라우저 불필요).
|
|
# fixture 는 실제 렌더된 검색결과에서 카드 4개를 추출한 것(단위가격 '1개당' 케이스 포함).
|
|
from pathlib import Path
|
|
|
|
from services.search.coupang.parser import parse_search_html
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "coupang_search.html"
|
|
|
|
|
|
def test_parse_extracts_valid_products():
|
|
items = parse_search_html(FIXTURE.read_text())
|
|
|
|
assert len(items) >= 3
|
|
for p in items:
|
|
assert p.source == "coupang"
|
|
assert p.name and len(p.name) > 2
|
|
# 단위가격('1개당 44,400원')의 '1' 을 가격으로 오인하던 회귀 방지
|
|
assert p.price >= 100, f"이상 저가: {p.price} ({p.name})"
|
|
assert p.detail_url and p.detail_url.startswith("https://www.coupang.com/vp/products/")
|
|
assert p.external_id # vendorItemId(data-id)
|
|
|
|
|
|
def test_parse_price_is_sale_not_unit_price():
|
|
# fixture 첫 카드의 판매가는 44,400원(단위가격도 '1개당 44,400원'이라 값은 같지만
|
|
# 파서가 정가(del)나 '%'가 아닌 판매가를 정확히 집는지 확인)
|
|
items = parse_search_html(FIXTURE.read_text())
|
|
assert items[0].price == 44400
|