네이버 폴백용 오픈마켓 크롤러 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>
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""오픈마켓 검색결과 공용 카드 파서(순수 함수, selectolax).
|
|
|
|
G마켓·옥션·11번가처럼 '카드 목록 → 카드별 이름/가격/링크/배송' 구조가 같은 소스를 하나로 파싱한다.
|
|
사이트별 셀렉터·소스명·베이스URL 만 주입한다. 브라우저/네트워크 무관 — 저장 HTML 로 단위 테스트 가능.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from urllib.parse import urljoin
|
|
|
|
from selectolax.parser import HTMLParser
|
|
|
|
from services.search.contract import NormalizedProduct
|
|
from services.search.util import extract_price, clean_name, shipping_from_text
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CardConfig:
|
|
source: str # naver 외 크롤 소스 코드 (gmarket|auction|st11)
|
|
mall_name: str # 표시 몰명 (G마켓|옥션|11번가)
|
|
base_url: str # 상대경로 링크 절대화용
|
|
card: str
|
|
name: str
|
|
price: str
|
|
link: str = "a[href]"
|
|
|
|
|
|
def parse_cards(html: str, cfg: CardConfig) -> list[NormalizedProduct]:
|
|
tree = HTMLParser(html)
|
|
products: list[NormalizedProduct] = []
|
|
seen: set[str] = set()
|
|
|
|
for card in tree.css(cfg.card):
|
|
name_el = card.css_first(cfg.name)
|
|
name = clean_name(name_el.text(strip=True)) if name_el else None
|
|
if not name:
|
|
continue
|
|
|
|
price_el = card.css_first(cfg.price)
|
|
price = extract_price(price_el.text(strip=True)) if price_el else None
|
|
if price is None or price <= 0:
|
|
continue # 광고/헤더/추천 슬롯 등 유효 상품 아님
|
|
|
|
a = card.css_first(cfg.link)
|
|
href = a.attributes.get("href") if a else None
|
|
detail_url = urljoin(cfg.base_url, href) if href else None
|
|
|
|
key = detail_url or name # 동일 링크/광고 반복 제거
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
|
|
shipping_fee, shipping_type = shipping_from_text(card.text(separator=" "), name)
|
|
|
|
products.append(
|
|
NormalizedProduct(
|
|
source=cfg.source,
|
|
name=name,
|
|
price=price,
|
|
detail_url=detail_url,
|
|
mall_name=cfg.mall_name,
|
|
shipping_fee=shipping_fee,
|
|
shipping_type=shipping_type,
|
|
)
|
|
)
|
|
|
|
return products
|