charley 7471ff9226 fix(wx): 更换企业微信群机器人Webhook地址 3 周之前
..
HANDOFF.md dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
README.md dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
YamlLoader.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
application.yml dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
auto_send_wx_msg.py 7471ff9226 fix(wx): 更换企业微信群机器人Webhook地址 3 周之前
init_db.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jiwu_core.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_detail.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_onsale_alert.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_onsale_report.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_onsale_spider.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_report_excel.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_sold_report.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
jw_sold_spider.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
mysql_pool.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
requirements.txt dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
run_all.py dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前
schema.sql dacb127039 feat(wechat): 字符重试机制优化企微群机器人发送模块 3 周之前

README.md

集物星球(jiwuplanet)爬虫

集物星球 App 数据采集项目:在售 / 已售 / 购买记录三路抓取,每日在售·已售 Excel 报告自动推企业微信,以及针对重点商家(Jake / 九叔)的在售上架实时监控。

  • 域名:https://api.jiwuplanet.com,全部 POST + JSON
  • 业务成功码 code == 1code == 200 且 data 为空 是被风控/会话无效的空壳。
  • 整体流程、产出与逻辑对齐参考项目 deca_spider,仅加解密(签名算法)不同。

逆向 / 接入的完整细节(签名逐字节验证、TLS 指纹、自登录抓包基准)见 HANDOFF.md


1. 反爬与接入要点

关键点 说明
请求签名 每请求带 6 个头 X_VERSION / X_TIME / X_SID / X_RAND / X_PLATFORM / X_SIG_1X_SIG_1 = base64(HMAC_SHA256(key, msg)),拼接与密钥见 jiwu_core._sign(密钥常量 API_SIGN_SECRET_KEY)。
TLS 指纹 必须用 curl_cffiimpersonate="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、签名串)必须同值。

flowchart TD
    A["do_request(path, body, need_auth)"] --> B{"need_auth?"}
    B -- "False 免登录(默认)" --> C["get_anon_sid()<br/>随机自造 sid,不调 regLogin"]
    B -- "True 需登录" --> D["ensure_session()"]
    D --> E{"缓存 sid 未过期?<br/>SESSION_TTL=1800s"}
    E -- "是" --> F["复用缓存 sid"]
    E -- "否" --> G["login(): 自造 sid<br/>→ regLogin 绑定 → 写缓存"]
    C --> H["sessionId 写入 body.sessionId + 头 X_SID + 签名串<br/>(三处同值)"]
    F --> H
    G --> H
    H --> I["_sign(): X_SIG_1 = base64(HMAC_SHA256(key, msg))<br/>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_recordnew_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 首图 resAddrFILE_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_cffilogurutenacityschedulerequestsopenpyxl,以及全局公共库 charley-utils(提供 mysql_pool / YamlLoader,依赖 pymysqlDBUtilsPyYAML)。

    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. 运行

# 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。

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 天。