"""ラクマ 店铺页 HTML → 卖家详情与卖家商品列表 C2C 集市里的「商家」就是个人卖家,页面在 fril.jp/shop/{hash}: - 店铺页本身:卖家资料 + 该卖家的商品分页列表(含已售出) - /review 子页:评价明细与好评/普通/差评分档计数 商品卡片与搜索页共用 `.item-box` 结构,直接复用 search 里的解析。 """ from __future__ import annotations import json import re from selectolax.parser import HTMLParser from app.scraping.core import rakuma_site as site from app.shared.errors import ScrapeParseError from app.scraping.models.scrape import ( RakumaRatingBreakdown, RakumaReview, RakumaShopDetailData, RakumaShopItemsData, ) from app.scraping.parsers.rakuma.base import ( attr, event_payload, image_url, node_text, parse_float, parse_int, parse_total_count, ) from app.scraping.parsers.rakuma.search import parse_item_cards # 评价条目标题左侧的图标 class → 评价档位 _RATING_ICONS = { "icon_review_sun": "good", # よい "icon_review_cloud": "normal", # ふつう "icon_review_rain": "bad", # わるい } # /review 页上三组分档计数的容器 id 前缀 _ALL_RATINGS_PREFIX = "all" _SELLER_RATINGS_PREFIX = "seller" _LD_JSON_RE = re.compile( r']*type="application/ld\+json"[^>]*>(.*?)', re.S ) def _require_shop_page(html: str) -> HTMLParser: tree = HTMLParser(html) if tree.css_first(f".{site.SHOP_PAGE_MARKER}") is None: raise ScrapeParseError("页面不是店铺页(缺少店铺资料区块)") return tree def _parse_rating_breakdown(tree: HTMLParser, prefix: str) -> RakumaRatingBreakdown: """解析一组好评/普通/差评计数 页面用 `