# SNKRDUNK 日本站 逆向分析文档 > 宝可梦单卡「已售出品 + 售买履历」采集。列表页(表1) + 详情页(表1) + 交易记录(表2)。 ## 1. 目标信息 - **站点**:https://snkrdunk.com (日文站,スニダン) - **列表页**:`/search?keywords=Pokemon+Card+Game...&brandIds=pokemon&searchCategoryIds=6/33&sort=launch&itemConditions=...&page=1` - **详情页**:`/apparels/{apparel_id}/used/{used_item_id}`,如 `/apparels/835482/used/47574548` - **技术栈**:前端 Next.js App Router(SSR + RSC flight data);后端提供 v1/v2/v3 REST JSON 接口 - **反爬 / 加密措施**:无签名、无加密参数;核心业务接口**无需登录、无需 cookie**(`credentials: omit` 可直接访问)。仅账号相关接口(`/v1/accounts/me`、EN 站 `trading-histories`)需登录态 ## 2. 数据目标(字段) ### 表1 `snkrdunk_jp_used_item`(二手出品 / 商品信息) 列表页来源:`apparel_id / used_item_id / name_ja / condition_grade(品相) / price / is_sold / primary_image_url / detail_url` 详情页补全:`product_id / product_number(品番) / name_en / brand / category / quantity_text(枚数) / condition_desc / sale_status / image_urls(多图逗号分割) / released_at` ### 表2 `snkrdunk_jp_trading_history`(买卖历史 / 交易记录) `product_id / apparel_id / product_number / sold_price / condition_grade(品相) / quantity_text(枚数) / sold_at / trade_index_in_second` 联合唯一索引 `(product_id, sold_at, sold_price, condition_grade, quantity_text, trade_index_in_second)` 配合 `INSERT IGNORE` 累积去重(详见第6节踩坑)。 > 建表脚本见 `ddl.sql`。字段一律按原意翻译、避开 MySQL 保留字(`condition`→`condition_grade`、`status`→`sale_status`); > 时间字段统一 `gmt_create_time / gmt_modified_time`,成交/发售时间转为北京时间(UTC+8)与现有管线一致。 ## 3. 请求分析(关键接口) | 用途 | 接口 | 说明 | |---|---|---| | 列表页(表1) | `GET /search?...&page=N` | **无独立 JSON 接口**,数据内嵌在 SSR HTML 的 `self.__next_f` flight 里 | | 详情(表1) | `GET /v1/apparels/{apparel_id}/used/{used_item_id}` | 返回完整 `apparelUsedItem`,无需 cookie | | 交易记录(表2) | `GET /v3/products/{product_id}/trading-history?range=all` | 返回 `trades[]`,无需 cookie,一次性全量 | **三个 ID 的区别(易踩坑)**: - `apparel_id`(URL 中 `/apparels/{}`):款式ID - `used_item_id`(URL 中 `/used/{}`):某个具体二手出品ID - `product_id` = 详情里的 `apparel.productId`(商品目录 catalog ID):**交易记录接口用的是它,不是 apparel_id**。需先抓详情拿到 product_id,再抓交易记录。 **关键字段来源**: - 列表卡片:`displayCardPattern`(含 `SoldOut` 即已售)、`title`、`link`(含 apparel_id+used_item_id)、`imageUrl`、`salePrice`、`condition`、`favoriteCount` - 交易记录 `trades[]`:`price`→成交价、`soldAt`→成交时间(UTC)、`title`→品相(A/B/C..)、`label`→枚数(1枚..) ## 4. 逆向思路 1. **列表页找数据源**:`/search` 是 Next.js SSR,商品卡片在客户端由 flight 数据 hydration,页面 raw HTML 里没有 `` 卡片 DOM,`_rsc` 接口又依赖动态 token。最终定位:数据以 `self.__next_f.push([1,"..."])` 分片写入 HTML。 2. **还原 flight 数据**:正则抽出所有分片字符串 → 逐段 `json.loads('"'+p+'"')` 反转义 → 拼接成完整文本 → 「花括号配平」抠出每个 `{"displayCardPattern"...}` 卡片对象 → `json.loads`。(见 `_extract_flight_cards`) 3. **SOLD 判定**:卡片 `displayCardPattern` 含 `SoldOut` = 已售(对应网页左上角 SOLD 角标)。 4. **详情/交易记录**:直接命中 v1/v3 JSON 接口,无需解析 HTML。 5. **无 cookie 验证**:`credentials:'omit'` 实测三类接口均 200,确认可纯 requests 抓取。 ## 5. 算法要点 无加密 / 无签名。唯一「非直观」点是列表页的 **Next.js flight data 解析**: ```python # 1) 抽取并拼接所有 flight 分片 parts = re.findall(r'self\.__next_f\.push\(\[1,"((?:[^"\\]|\\.)*)"\]\)', html) flight = "".join(json.loads('"' + p + '"') for p in parts) # 反转义后拼接 # 2) 花括号配平(尊重字符串内的转义引号)逐个抠出 {"displayCardPattern"...} 对象 → json.loads ``` ## 6. 踩坑记录 - **交易记录无图**(需求3):`trading-history` 接口的 `trades[]` 是平台**聚合/匿名化**数据,每条只有价格/时间/品相/枚数,**故意不含图片和出品ID**——不是遗漏。若要给每条配图,复用现有管线 `match_data` 思路:以 `(product_id + 品相 + 价格)` 关联表1、取 `primary_image_url` 回填(届时给表2 加一个 `image_url` 字段)。 - **交易记录接口硬限 20 条**:不管 `range=all/oneWeek/oneMonth/...`、`page/perPage/limit/offset/size` 各种参数都固定返回最近 20 条。所以**不能用「先删后插」**(会永远丢老历史),必须**跨天累积**。 - **同秒真实重复成交**:同一 `soldAt` 秒可能出现多条完全相同的真实成交(如 6 笔 `¥1000 / B / 1枚 / 2026-07-17T03:50:03Z`)。爬虫端解析时给同签名记录编 `trade_index_in_second = 0/1/2/...` 位序,配合联合唯一索引 + `INSERT IGNORE` 累积去重,既能保留真实重复、又能跨天自动跳过已存在的。前提:接口对 `trades` 的排序在同秒内相对稳定(实测成立)。 - **同款卡多出品共享同一份买卖历史**:买卖历史按 `product_id` 聚合,而一张卡可能有几十个不同的 `used_item_id` 出品。批量模式下用 `seen_product_ids` 集合,每次 run 每个 `product_id` 只调一次交易记录接口。 - **JP 站无 `/v1/trading-cards/used`**:EN 站有、JP 站 404,故 JP 列表只能解析搜索页 flight。 - **EN 分页交易接口需 cookie**:`/en/v1/products/SW---{id}/trading-histories` 返回 401;JP 的 `/v3/.../trading-history` 无需 cookie,优先用后者。 - **图片带尺寸参数**:`...jpeg?size=m`,入库前 `split("?")[0]` 去掉,保留原图。 - **product_id ≠ apparel_id**:交易记录必须用 `apparel.productId`。 - **mysql_pool 读运行目录 `application.yml`**:务必在项目目录下运行脚本。 ## 7. 使用方式 ```bash # 依赖:requests / parsel / user_agent / schedule / loguru / tenacity / charley-utils(mysql_pool) # 1) 先执行 ddl.sql 建表(snkrdunk_jp_used_item / snkrdunk_jp_trading_history) # 2) 确保项目目录下有 application.yml(mysql 连接配置) # 3) 运行 snk_jp_spider.py,按底部开关选择任务: ``` | 场景 | 调用 | |---|---| | 任务2 单条详情+交易记录 | `detail_main(logger, apparel_id=835482, used_item_id=47574548)` | | 任务1 列表页全部已售 → 表1 | `list_main(log=logger)` | | 批量补全表1 列表记录的详情+交易记录 | `detail_main(logger)`(先跑完 `list_main`) | | 每日定时(列表+详情+交易记录) | `schedule_task()` | ## 8. 举一反三 - 换商品/品类:改 `LIST_URL_TEMPLATE` 的 `keywords / brandIds / searchCategoryIds / itemConditions` 即可,解析逻辑通用。 - 该 flight 解析法适用于所有 Next.js App Router SSR 站点:`self.__next_f` 分片 + 花括号配平抠对象。 - EN 站(`/en/v1/...`)已有独立 JSON 接口,若需英文数据可直接调用,无需解析 flight。