# 集物星球(jiwuplanet)爬虫
集物星球 App 数据采集项目:在售 / 已售 / 购买记录三路抓取,每日在售·已售 Excel 报告自动推企业微信,以及针对重点商家(Jake / 九叔)的在售上架实时监控。
- 域名:`https://api.jiwuplanet.com`,全部 `POST + JSON`。
- 业务成功码 `code == 1`;`code == 200 且 data 为空` 是被风控/会话无效的空壳。
- 整体流程、产出与逻辑对齐参考项目 `deca_spider`,仅加解密(签名算法)不同。
> 逆向 / 接入的完整细节(签名逐字节验证、TLS 指纹、自登录抓包基准)见 [`HANDOFF.md`](./HANDOFF.md)。
---
## 1. 反爬与接入要点
| 关键点 | 说明 |
|---|---|
| 请求签名 | 每请求带 6 个头 `X_VERSION / X_TIME / X_SID / X_RAND / X_PLATFORM / X_SIG_1`;`X_SIG_1 = base64(HMAC_SHA256(key, msg))`,拼接与密钥见 `jiwu_core._sign`(密钥常量 `API_SIGN_SECRET_KEY`)。 |
| TLS 指纹 | **必须用 `curl_cffi`(`impersonate="chrome"`)**发请求;`requests` 的指纹会被判为非真机、返回空壳。已封装在 `jiwu_core._post`。 |
| 直连 | 显式 `proxies={"http":None,"https":None}`,绕开本机 Clash 代理环境变量。 |
| 鉴权 | 无 token,`sessionId` 即会话;客户端自造 32 位 hex 经 `regLogin` 绑定,服务端沿用。三处(头 `X_SID`、body `sessionId`、签名串)须同值。脚本自登录见 `jiwu_core.login`(账号常量 `LOGIN_ACCOUNT`)。 |
| 金额单位 | 接口原始值 ÷ 1000000 = 元(实测 99000000 → ¥99.00)。**入库前已由 `core.to_yuan` 换算成元存 DECIMAL**,故库里/报告里的金额直接就是元。 |
---
## 1.1 登录 / 会话流程(`jiwu_core`)
核心:**无 token,客户端自造 `sessionId`;「签名是门票、sessionId 是身份」**。免登录接口用随机身份糊弄过签名即可取数,只有服务端强制认身份的接口才 `regLogin` 绑一个真身份。三处 `sessionId`(头 `X_SID`、body、签名串)必须同值。
```mermaid
flowchart TD
A["do_request(path, body, need_auth)"] --> B{"need_auth?"}
B -- "False 免登录(默认)" --> C["get_anon_sid()
随机自造 sid,不调 regLogin"]
B -- "True 需登录" --> D["ensure_session()"]
D --> E{"缓存 sid 未过期?
SESSION_TTL=1800s"}
E -- "是" --> F["复用缓存 sid"]
E -- "否" --> G["login(): 自造 sid
→ regLogin 绑定 → 写缓存"]
C --> H["sessionId 写入 body.sessionId + 头 X_SID + 签名串
(三处同值)"]
F --> H
G --> H
H --> I["_sign(): X_SIG_1 = base64(HMAC_SHA256(key, msg))
msg = path+版本+平台+sessionId+时间+随机+body"]
I --> J["curl_cffi POST(impersonate=chrome,直连)"]
J --> K{"code == 1 ?"}
K -- "是" --> L["成功,返回 JSON"]
K -- "否 · 免登录" --> M["轮换匿名 sid,下轮再来"]
K -- "否 · 需登录" --> N["清缓存 → 重登,最多再试 1 次"]
```
- **免登录**(`need_auth=False`,默认):在售 `index/top`、已售 `corp/history`、商品详情 `merchantGoodsId`——匿名随机 sid,不暴露账号。
- **需登录**(`need_auth=True`):商家列表 `hotRecommend`、购买记录 `publicity/user/group/pager`、拆卡报告 `gift/report`——`ensure_session` 复用/续期真会话(`SESSION_TTL` 内复用,超时自动重登)。
- 设计动机:登录态是账号被风控关联/封号的主要抓手,故走「最小登录面」——能免登录就不登录。
---
## 2. 目录结构
```
2026-08-17(jiwu_spider)/
├── jiwu_core.py # 核心库:签名、自登录拿 sessionId、curl_cffi 通用请求
├── auto_send_wx_msg.py # 企业微信群机器人:发文本/markdown、发文件(Excel)
├── schema.sql # 建表 DDL(6 张 jw_ 表)
├── init_db.py # 读 schema.sql 幂等建表
├── run_all.py # ★一键启动:5 个常驻任务各拉子进程 + 守护自动重启(服务器部署跑这个)
├── requirements.txt # 依赖清单(版本实测;pip install -r requirements.txt)
│
├── jw_detail.py # 商品详情补全工具(merchantGoodsId 免登录,补两表详情字段 + img)
├── jw_onsale_spider.py # 板块1 在售抓取(免登录,每日)+ 详情补全
├── jw_sold_spider.py # 板块2 已售抓取 + 详情补全 + 拆卡报告 + 购买记录(全站已售)
│
├── jw_report_excel.py # 报表 Excel 样式工具库(openpyxl,两报告共用)
├── jw_onsale_report.py # 板块4 在售日报 → 企微
├── jw_sold_report.py # 板块4 已售日报 → 企微(Jake/九叔 各独立 sheet)
├── jw_onsale_alert.py # 板块5 在售监控 / 上架提醒
│
├── application.yml # MySQL 连接配置(mysql_pool 读取)
├── HANDOFF.md # 逆向/接入交接文档
└── reports/ logs/ # Excel 报表留档 / 运行日志(自动生成)
```
数据库连接池 `mysql_pool.MySQLConnectionPool` 来自全局公共库 `charley-utils`,读运行目录下的 `application.yml`。
---
## 3. 五大板块
| # | 板块 | 脚本 | 定时 | 落表 / 产出 |
|---|---|---|---|---|
| 1 | 在售抓取 | `jw_onsale_spider.py` | 每天 09/15/20/01 四档 | 免登录全量抓 index/top → `jw_onsale_product_record`(upsert + 下架对账 `is_on_sale`);同步写每日快照 `jw_onsale_daily_record`(供在售趋势差分) |
| 2+3 | 已售抓取 + 购买记录 + 拆卡报告 | `jw_sold_spider.py` | 每天 08:00 | 商家列表(需登录)→各商家已售 corp/history(免登录) `jw_sold_product_record`;再对全站已售商品抓:详情补全(免登录) + 拆卡报告 `jw_report_record`(需登录) + 购买记录 `jw_player_record`(玩家维度, 需登录) |
| 4 | 每日报告 | `jw_onsale_report.py` / `jw_sold_report.py` | 在售 09/15/20/01 四档 / 已售 09:10 | 生成 Excel → 企微发文件;只读库不抓取(在售每档各出一份带小时的文件) |
| 5 | 在售监控 | `jw_onsale_alert.py` | 手写轮询(默认全天,每轮 60~90s 随机) | 三类告警推企微;去重状态落 `jw_onsale_alert_record` |
> 购买记录与拆卡报告都并入「已售抓取」,对**全站已售商品**抓取:
> - **购买记录** = 赠品公示·玩家维度 `publicity/user/group/pager`(需登录),翻页拿全量买家,字段 userId/userNick/picId/count。**不做 DB 去重**:靠 `jw_sold_product_record.buy_fetched` 状态位控制每商品抓一次——翻页取全→批量入库成功→再置 `buy_fetched=1`,失败不置位、下轮重试(买家列表售罄后即固定)。
> - **拆卡报告** = `gift/report`(需登录),有 `giftReportId`+`userId`+中奖状态;因**更新可能不及时**,对**结束 `REPORT_REFETCH_DAYS`(默认3) 天内的已售商品每轮重查**(`INSERT IGNORE` 按 giftReportId 去重、自然补齐),老车只补抓从未抓过的。
**板块4 报告细节**:openpyxl 生成 Excel(`jw_report_excel.py` 提供统一样式),通过 `auto_send_wx_msg.send_wechat_group_file` 以「文件消息」发到企微群。
- 在售日报(6 sheet,对齐 deca):`概览` / `今日新增商家` / `其他商家`(存量商家 `gmt_create_time!=CURDATE()`)/ `商品明细`(今日新上架行淡红高亮)/ `在售趋势`(按每日快照差分,最近 4 天在售商家/拼团数与净新增)/ `上架时段分布`(`HOUR(sold_time)` 24 桶、近 7 日、█ 条形)。每天 09/15/20/01 四档各生成一份带小时的独立文件。
- 已售日报(9 sheet,对齐 deca):`平台总览`(KPI + 当日环比vs昨日同窗口 + 商家GMV集中度Top1/3/5/10)/ `产品系列榜`(按 goods_ip_name) / `商家GMV榜` / `运营节奏`(重点商家快照 + 成交时段分布) / `Jake明细`(汇总+逐团明细+购买记录覆盖检测) / `Jake用户排行榜` / `九叔明细` / `九叔用户排行榜` / `其他商家`。GMV=已售数×单价元;参与人数取 `jw_player_record` 去重 user_id。**业务日窗口**:成交完成时间 `COALESCE(finish_time, soldout_time)` 落在 `[昨17:00, 今06:00]`(含两端)算「昨天」的已售(对齐 deca 的 completed_at 口径,凌晨仍开播故延到 06:00);采集 08:00 先落库、报告 09:10 再读。
**板块5 监控细节**:`index/top` 无按商家过滤参数,故全量翻 4 个 `productType` 后按 `corp_info_id` 客户端过滤重点商家。三类告警:
- 🆕 **新品上架**:监控内首次出现且 `soldTime ≥ 本场窗口起点(最近一个 RUN_START)`(对齐 deca 最新逻辑);更早上架的老货静默建档不刷屏。
- 🔥 **进度过半**:`(总份数-剩余)/总份数 ≥ HALF_THRESHOLD`(0.5),首次达标才发。
- 🏁 **一车结束**:商品从在售列表消失 + 打详情二次确认 `surplusStock<=0`(售罄) **或** 已过 `offShelfTime`(下架/销售结束时间),任一即结束;战报含标题/价格/售出件数/购买人数(玩家维度 totalCount)。
运行窗口:对齐 deca 走窗口制 `[RUN_START 20:30, RUN_END 06:00]`(`RUN_ALL_DAY=False`),`RUN_START` 同时是「本场新上架」判定门槛;集物直播时段不同可改 `RUN_START/RUN_END` 或设 `RUN_ALL_DAY=True` 全天。去重靠 `jw_onsale_alert_record` 的 `new_notified / half_notified / ended_notified` 三标记位 + 进程内 `_seen_onsale` 集合(只播报程序运行后才结束的车)。新品/结束为「发送成功才置位」,失败下轮补发;过半「先置位再发」。
---
## 4. 数据表(`schema.sql`)
| 表 | 用途 | 主要唯一键 |
|---|---|---|
| `jw_onsale_product_record` | 在售商品最新状态 | `goods_id` |
| `jw_sold_product_record` | 已售商品(只增) | `goods_id` |
| `jw_shop_record` | 商家最新状态 | `corp_info_id` |
| `jw_player_record` | 购买记录(玩家维度买家列表,只增;不 DB 去重,靠已售表 `buy_fetched` 控制每商品抓一次) | 无唯一键 |
| `jw_report_record` | 拆卡报告(含 userId/中奖状态,只增) | `gift_report_id` |
| `jw_onsale_alert_record` | 在售监控提醒去重状态 | `goods_id` |
| `jw_onsale_daily_record` | 在售每日快照(商品+日期,四档 upsert 保最新,供在售趋势差分) | `goods_id + snapshot_date` |
统一规范:自增 `id` 物理主键;时间字段 `gmt_create_time / gmt_modified_time`;金额字段(`amount/price/original_price/highest_price/lowest_price`)**入库前已 ÷1000000 换算成「元」存 DECIMAL(12,2)**。
**详情补全字段**:两个商品表都带一组来自商品详情 `merchantGoodsId`(免登录)的字段——`img`(商品主图完整URL) / `goods_type` / `price` / `original_price` / `plan_up_time` / `off_shelf_time` / `stock` / `surplus_stock` / `status` / `collection_card_name` / `gift_way` / `random_way`。这些列表接口没有,由 `jw_detail.enrich_detail` 逐商品补拉,用各表 `detail_fetched` 状态位控制**每商品只补一次**(在售的列表 upsert 不覆盖这些列)。
- `img` = `FILE_DOMAIN` + `resList` 首图 `resAddr`;`FILE_DOMAIN = https://files.jiwustar.com/`(逆向 `EnvironmentManager` 正式环境所得,接口只给相对路径)。
- `collection_card_name / gift_way / random_way` 来自详情的 `cardGoods` 子对象。
---
## 5. 接口清单
| 板块 | path | 登录 | 关键参数 / 说明 |
|---|---|---|---|
| 在售列表 | `/search/app/index/top` | 🟢 免登录 | currentPage, limit, productType(1福袋2变风盒3错版卡4原盒), systemBusinessType:5 |
| 已售历史 | `/search/app/corp/history` | 🟢 免登录 | corpInfoId, currentPage, limit, systemBusinessType:5 |
| 商品详情 | `/search/app/merchantGoodsId` | 🟢 免登录 | goodsId, systemBusinessType:5(监控判结束用;字段 stock/surplusStock/price/highestPrice) |
| 购买记录 | `/order/merchant/app/query/gift/publicity/user/group/pager` | 🔴 需登录 | 赠品公示·玩家维度:currentPage, giftBusinessName:"", goodsId, limit, systemBusinessType:5;每条=一个买家 **userId/userNick/picId/count**,翻页拿全量 |
| 拆卡报告 | `/goods/gift/report/query/search/pager` | 🔴 需登录 | goodsId, currentPage, limit, **systemBusinessType:6**;字段 giftReportId(唯一)/serialItemName/userId/winnerStatus/createTime |
| 商家列表 | `/search/app/index/corp/hotRecommend` | 🔴 需登录 | currentPage, limit, systemBusinessType:6 |
| 登录 | `/acct/user/regLogin` | — | 见 `jiwu_core.LOGIN_ACCOUNT` |
**登录态最小化**(已实测,2026/08/19):`jiwu_core.do_request` 默认 `need_auth=False` 走免登录(匿名随机 sid 签名、不调 regLogin,不暴露账号);只有商家列表 `hotRecommend`、购买记录 `publicity/user/group/pager`、拆卡报告 `gift/report` 实测服务端强制登录,才传 `need_auth=True`。即**在售/已售/商品详情抓取全程零登录**。
重点商家:**Jake球星卡 `corpInfoId=100716`**、**九叔的喷火龙 `corpInfoId=100715`**。
---
## 6. 环境与安装
- Python 3.12.10
- 依赖:`curl_cffi`、`loguru`、`tenacity`、`schedule`、`requests`、`openpyxl`,以及全局公共库 `charley-utils`(提供 `mysql_pool` / `YamlLoader`,依赖 `pymysql`、`DBUtils`、`PyYAML`)。
```bash
pip install -r requirements.txt
# charley-utils 为本地 editable 安装:pip install -e D:\work\common\charley-utils
```
配置 `application.yml` 里的 MySQL 连接(host/port/username/password/db)。
---
## 7. 运行
```bash
# 1) 建表(幂等 CREATE IF NOT EXISTS)。
# schema 经多轮调整,若库里已有旧结构的表,CREATE IF NOT EXISTS 不会改动它们,
# 建议先 DROP 掉全部 jw_ 表再重建:
# mysql> DROP TABLE IF EXISTS jw_onsale_product_record, jw_sold_product_record, jw_shop_record,
# jw_player_record, jw_report_record, jw_onsale_alert_record, jw_onsale_daily_record;
python init_db.py
# 2) 一键启动全部任务(推荐,服务器部署用这个)
# run_all.py 会把下面 5 个任务各拉成独立子进程并守护(崩溃自动重启)
python run_all.py
```
**单跑(仅调试用)**:想单独跑某个任务时直接跑对应脚本即可,不必用 run_all。
```bash
python jw_onsale_spider.py # 在售(每天 09/15/20/01 四档,免登录,含每日快照)
python jw_sold_spider.py # 已售 + 拆卡报告 + 购买记录(每天 08:00)
python jw_onsale_report.py # 在售日报(09/15/20/01 四档发企微)
python jw_sold_report.py # 已售日报(09:10 发企微)
python jw_onsale_alert.py # 在售监控(默认窗口起点 20:30)
# python jw_onsale_alert.py 17:00 或 --start 17:00 # 传窗口起点(该时间后新上架才提醒)
```
生产部署:直接 `python run_all.py`(自带子进程守护/自动重启);如需开机自启/进程托管,可再用 nssm / supervisor / pm2 托管 `run_all.py` 这一个进程即可。各脚本 `schedule_task()` 里可取消注释 `main_task/run_once` 立即跑一次用于调试。
---
## 8. 关键配置项
| 位置 | 配置 | 说明 |
|---|---|---|
| `auto_send_wx_msg.WEBHOOK_URL` | 企微群机器人 webhook | 当前为测试群,上线换正式群只改这一处 |
| `jw_onsale_alert.WATCH_CORPS` | `{100716, 100715}` | 监控的重点商家 |
| `jw_onsale_alert.HALF_THRESHOLD` | `0.5` | 进度过半阈值 |
| `jw_onsale_alert.RUN_ALL_DAY` / `RUN_START` / `RUN_END` | 运行窗口 | 默认 False 走 [20:30, 06:00] 跨午夜窗口(对齐 deca,RUN_START 兼作新品判定门槛);置 True 则全天 |
| `jw_sold_report.FOCUS_CORPS` | Jake / 九叔 | 已售报告独立成 sheet 的商家 |
| `jiwu_core.SESSION_TTL` | `1800` | sessionId 复用时长,超时自动重登 |
| `jiwu_core.NO_PROXY` | 直连 | 如需代理在此改 |
---
## 9. 待实测确认(不影响运行,仅影响判定精度)
1. **新品上架判定**:`jw_onsale_alert.is_new_arrival` 假定列表字段 `soldTime` 为上架时间(依 schema 注释)。若实测为售罄/成交时间,需改判定逻辑或改用其它上架时间字段。
2. **一车结束二次确认**:`jw_onsale_alert.confirm_ended` 依赖商品详情返回 `surplusStock`(剩余库存)。若语义不同,需调整结束判定。
---
## 10. 注意事项
- 登录态最小化:业务接口均由 `jiwu_core` 统一带 `sessionId` + 签名,会话失效自动重登一次重试。
- 抓取与报告解耦:报告脚本只读库、不触发抓取,抓取由各 spider 按自身定时落库。
- 报告 Excel 存 `reports/` 目录、文件名带日期、发送后不删除(留档)。
- 日志按天切分存 `logs/`,保留 7 天。