네이버 폴백용 오픈마켓 크롤러 3종. 4중 복제를 피해 BrowserSearchAdapter 베이스로 브라우저 수명·프록시 회전·차단감지+IP회전 재시도·감지기록을 공유하고, 사이트 차이는 훅(_search_url/_parse/ready_selector/block_markers)으로 분리. - browser_base: patchright 공통 machinery(쿠팡 machinery 일반화) - card_parser: 오픈마켓 공용 카드 파서(CardConfig 주입) + util(extract_price/clean_name/shipping) - esm: G마켓·옥션(eBay 백엔드 공유, site 파라미터 1개로 커버, '잠시만' 챌린지 대기) - st11: 11번가 PC(공식API 판매자계정 필요라 불가). 지연로딩 → scroll_steps, '.c-card-item' 토큰 - 학습 반영: 오픈마켓은 리소스차단 OFF(렌더/챌린지 깨짐), 스턱 챌린지='잠시만' 잔존 시 차단 간주→IP회전 - 파서 결정론 테스트 7종(실렌더 카드 3개씩 경량 fixture). 라이브 검증: 신선 IP로 3사 모두 파싱 성공 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
50 lines
2.0 KiB
Python
50 lines
2.0 KiB
Python
# 오픈마켓(G마켓·옥션·11번가) 크롤 파서 결정론적 단위 테스트(네트워크/브라우저 불필요).
|
|
# fixture 는 실제 렌더된 검색결과에서 카드 3개씩 추출한 것.
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from services.search.card_parser import parse_cards
|
|
from services.search.esm.selectors import GMARKET, AUCTION
|
|
from services.search.st11.selectors import CARDS as ST11
|
|
|
|
FIX = Path(__file__).parent / "fixtures"
|
|
|
|
CASES = [
|
|
("gmarket_search.html", GMARKET.cards, "gmarket", "G마켓"),
|
|
("auction_search.html", AUCTION.cards, "auction", "옥션"),
|
|
("st11_search.html", ST11, "st11", "11번가"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("fixture,cfg,source,mall", CASES)
|
|
def test_parse_extracts_valid_products(fixture, cfg, source, mall):
|
|
items = parse_cards((FIX / fixture).read_text(), cfg)
|
|
assert len(items) >= 2, f"{source}: 카드 파싱 실패"
|
|
for p in items:
|
|
assert p.source == source
|
|
assert p.mall_name == mall
|
|
assert p.name and len(p.name) > 2
|
|
assert "상품명" not in p.name and "브랜드명" not in p.name # a11y 라벨 제거 확인
|
|
assert p.price >= 100, f"이상 저가: {p.price} ({p.name})"
|
|
|
|
|
|
@pytest.mark.parametrize("fixture,cfg,source,mall", CASES)
|
|
def test_shipping_type_valid(fixture, cfg, source, mall):
|
|
items = parse_cards((FIX / fixture).read_text(), cfg)
|
|
for p in items:
|
|
assert p.shipping_type in (None, "free", "paid")
|
|
if p.shipping_type == "paid":
|
|
assert p.shipping_fee and p.shipping_fee > 0
|
|
if p.shipping_type == "free":
|
|
assert p.shipping_fee == 0
|
|
|
|
|
|
def test_dedup_by_link():
|
|
# 동일 링크 카드가 반복돼도 1건으로 축약
|
|
card = ('<div class="box__item-container"><a href="/x"><span class="text__item-title">상품명 물병</span></a>'
|
|
'<div class="box__price-seller">판매가5,000원</div></div>')
|
|
html = f"<ul>{card}{card}{card}</ul>"
|
|
items = parse_cards(html, GMARKET.cards)
|
|
assert len(items) == 1 and items[0].price == 5000
|