'가장 싼 값'과 '실제로 살 수 있는 가장 싼 값'은 다르다. 리뷰·평점이 전혀 없는 오퍼는 재고 없는 미끼가격일 수 있고, 그걸 최저가로 보고하면 사용자는 그 가격에 살 수 없다 — 조금 비싼 정답보다 나쁘다. 판단 근거를 수집해 둔다. - NormalizedProduct.rating / review_count 추가. 두 소스 모두 카드에 노출하는 값만 담아 교차 비교가 되게 했다. **없으면 None 유지** — '리뷰 0개'와 '리뷰 정보 없음'은 다른 뜻이다 - 네이버: product_grade 의 <strong>평점</strong><em>리뷰수</em>. 텍스트를 통째로 정규식 돌리면 '평점4.7473' 이 4.74/73 인지 4.7/473 인지 못 가르므로 노드로 분리해 읽는다. '1.7만' 같은 축약은 parse_ko_count 로 푼다(그대로 int() 하면 1 이 된다) - 쿠팡: 별점은 채워진 별 개수가 아니라 컨테이너 aria-label 에, 리뷰 수는 괄호 텍스트에 있다 - price_history.final_rating/final_review_count 추가(+마이그레이션) → "리뷰 0인 최저가가 몇 %인가"를 SQL 로 물을 수 있다. NULL 과 0 을 구분해야 해서 기본값을 두지 않았다 정렬 점검(사용자 제기): 두 소스 다 정렬 파라미터 없이 **랭킹/추천순**이다(픽스처 가격이 오름차순이 아님으로 확인). 가격순(sort=price_asc)은 차단 없이 동작하고 실측상 더 싼 후보를 찾지만(15,400→10,900), 리뷰·평점 없는 유령상품을 위로 끌어올려 미채택 — 추천순 유지. 신뢰 신호가 쌓이면 "리뷰 N 이상" 가드를 걸고 가격순을 켜는 선택지가 열린다. e2e: TR-1/TR-2 최저가에 평점 4.89·리뷰 7,314/102,000 이 함께 기록됨. 테스트 5건 추가, 228 passed.
30 lines
1.4 KiB
Python
30 lines
1.4 KiB
Python
"""쿠팡 검색결과 셀렉터(외부화).
|
|
|
|
쿠팡 클래스는 webpack 해시 suffix(예: ProductUnit_productUnit__Qd6sv)를 달고 있어
|
|
배포마다 suffix 가 바뀐다. 따라서 접두 부분 매칭([class*=...])으로 견고성을 확보한다.
|
|
레이아웃이 바뀌면 이 파일만 고치면 되도록 파서에서 분리한다(new.md: 셀렉터 외부 설정).
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CoupangSelectors:
|
|
card: str = "li[class*=ProductUnit_productUnit]"
|
|
name: str = "[class*=ProductUnit_productNameV2]"
|
|
price_area: str = "[class*=PriceArea_priceArea]"
|
|
orig_price: str = "del" # price_area 내부: 정가(취소선)
|
|
link: str = "a[href]"
|
|
image: str = "figure img"
|
|
data_id_attr: str = "data-id" # li 의 vendorItemId
|
|
# 배송 뱃지: 로고 이미지 src 로 판별(배송 텍스트는 해시 없는 인라인 스타일 span 이라 텍스트 매칭).
|
|
rocket_badge: str = "img[src*=rocket]"
|
|
rocket_merchant_marker: str = "rocket_merchant" # src 에 포함 시 판매자로켓, 그 외 rocket* 는 로켓배송
|
|
# 신뢰 신호 — 별점은 채워진 별 개수가 아니라 **컨테이너의 aria-label**(예: "5")에 들어 있고,
|
|
# 리뷰 수는 그 옆 괄호 텍스트("(41,448)")다.
|
|
rating_area: str = "[class*=ProductRating_productRating]"
|
|
rating_value_attr: str = "aria-label"
|
|
|
|
|
|
SELECTORS = CoupangSelectors()
|