"""ラクマ(fril.jp)站点常量 集中维护站点入口 URL、浏览器指纹参数,以及搜索页 URL 的排序码 / 筛选码映射。 排序与筛选参数并非猜测,而是从站点前端 bundle (asset.fril.jp/assets/v2/application-*.js)中 SearchPanel 组件的 `_url()` 方法里提取的——那段代码逐条拼出搜索 URL,因此参数名与取值与站点行为一一对应。 与乐天市场(app/core/site.py)的关键差异: - 页面是服务端渲染的 HTML,**没有** `window.__INITIAL_STATE__`,只能解析 DOM - 前置的不是 Akamai Bot Manager,无限速行为,冷请求即 ~0.6-1.1s,不需要 cookie 预热 - 商品详情页用 PC UA 即可,不需要像乐天那样切手机 UA """ from __future__ import annotations from typing import Final from app.shared import headers # ---- 站点入口 ---- HOME_URL: Final = "https://fril.jp/" SEARCH_BASE_URL: Final = "https://fril.jp/s" ITEM_BASE_URL: Final = "https://item.fril.jp/" SHOP_BASE_URL: Final = "https://fril.jp/shop/" CATEGORY_BASE_URL: Final = "https://fril.jp/category/" BRAND_BASE_URL: Final = "https://fril.jp/brand/" # 分类一览页。它是 Next.js App Router 页面,服务端把整棵分类树写进 RSC flight # payload(`self.__next_f.push`)。实测这份数据与 `?category_id=` 无关:传任意 # 合法分类、或完全不传,返回的 categoryList 都是同一份全量树(1686 条), # 因此取分类只需一次请求,不像乐天 genre 那样必须逐层下钻。 CATEGORY_LIST_URL: Final = "https://fril.jp/category" SEARCH_HOST: Final = "fril.jp" ITEM_HOST: Final = "item.fril.jp" # 站点每页固定返回 40 条(实测第 1 页与深翻页均为 40) PAGE_SIZE: Final = 40 # 站点侧翻页上限:page=100 正常返回,page=101 起直接 404。 MAX_PAGE: Final = 100 # ---- 浏览器指纹 ---- # 两类页面都用 PC UA:详情页 PC 版即为完整模板,无需像乐天那样切手机 UA。 USER_AGENT: Final = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" ) ACCEPT_LANGUAGE: Final = headers.ACCEPT_LANGUAGE def default_headers() -> dict[str, str]: """构造一套完整的浏览器导航请求头""" return headers.navigation_headers(USER_AGENT, mobile=False) # ---- 排序(搜索页 `sort=` + `order=` 两个参数)---- # 键为对外暴露的语义化排序名,值为 (sort, order)。 # 站点把排序字段与升降序拆成两个参数,因此这里成对给出。 SORT_CODES: Final[dict[str, tuple[str, str]]] = { "standard": ("relevance", "desc"), # 站点默认的おすすめ順 "newest": ("created_at", "desc"), "price_asc": ("sell_price", "asc"), "price_desc": ("sell_price", "desc"), "like_count": ("like_count", "desc"), # いいね数順 } # ---- 商品状态(搜索页 `statuses=`,逗号分隔可多选)---- # 取值来自 SearchPanel 组件的 statusOptions 定义。 CONDITION_CODES: Final[dict[str, str]] = { "new": "5", # 新品、未使用 "almost_new": "4", # 未使用に近い "no_damage": "6", # 目立った傷や汚れなし "slight_damage": "3", # やや傷や汚れあり "damaged": "2", # 傷や汚れあり "poor": "1", # 全体的に状態が悪い } # ---- 售卖状态(搜索页 `transaction=`)---- # 站点默认 all(不带该参数),仅在筛选时下发。 TRANSACTION_CODES: Final[dict[str, str]] = { "on_sale": "selling", # 販売中のみ "sold_out": "soldout", # 売切れのみ } # ---- 鉴定服务(搜索页 `authenticity_types=`)---- AUTHENTICITY_CODES: Final[dict[str, str]] = { "before_delivery": "pre", # お届け前鑑定 "after_delivery": "post", # 後から鑑定 } # 运费负担:站点用 carriage=1 表示「送料込みのみ」(卖家承担),0 表示不限 CARRIAGE_INCLUDED: Final = "1" # 商品详情页上表示已售出的标记文案 SOLD_OUT_MARKERS: Final = ("SOLD OUT", "SOLDOUT", "売り切れました") # 页面校验用的结构标记:这几类页面各自必须出现的 DOM 特征 SEARCH_PAGE_MARKER: Final = "page-count" ITEM_PAGE_MARKER: Final = "item-info" SHOP_PAGE_MARKER: Final = "profile-area__shop-name"