charley 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
..
README.md 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
YamlLoader.py 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
application.yml 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
ddl.sql 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
mysql_pool.py 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
requirements.txt 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu
snk_jp_spider.py 85013e71e5 feat(mysql): 新增SNKRDUNK日本站爬虫数据库支持 1 Minggu lalu

README.md

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 接口
  • 反爬 / 加密措施:无签名、无加密参数;核心业务接口无需登录、无需 cookiecredentials: 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 保留字(conditioncondition_gradestatussale_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 即已售)、titlelink(含 apparel_id+used_item_id)、imageUrlsalePriceconditionfavoriteCount
  • 交易记录 trades[]price→成交价、soldAt→成交时间(UTC)、title→品相(A/B/C..)、label→枚数(1枚..)

4. 逆向思路

  1. 列表页找数据源/search 是 Next.js SSR,商品卡片在客户端由 flight 数据 hydration,页面 raw HTML 里没有 <a href> 卡片 DOM,_rsc 接口又依赖动态 token。最终定位:数据以 self.__next_f.push([1,"..."]) 分片写入 HTML。
  2. 还原 flight 数据:正则抽出所有分片字符串 → 逐段 json.loads('"'+p+'"') 反转义 → 拼接成完整文本 → 「花括号配平」抠出每个 {"displayCardPattern"...} 卡片对象 → json.loads。(见 _extract_flight_cards
  3. SOLD 判定:卡片 displayCardPatternSoldOut = 已售(对应网页左上角 SOLD 角标)。
  4. 详情/交易记录:直接命中 v1/v3 JSON 接口,无需解析 HTML。
  5. 无 cookie 验证credentials:'omit' 实测三类接口均 200,确认可纯 requests 抓取。

5. 算法要点

无加密 / 无签名。唯一「非直观」点是列表页的 Next.js flight data 解析

# 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. 使用方式

# 依赖: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_TEMPLATEkeywords / brandIds / searchCategoryIds / itemConditions 即可,解析逻辑通用。
  • 该 flight 解析法适用于所有 Next.js App Router SSR 站点:self.__next_f 分片 + 花括号配平抠对象。
  • EN 站(/en/v1/...)已有独立 JSON 接口,若需英文数据可直接调用,无需解析 flight。