# deca采集翻页 优化记录 ## 1. 优化背景 排查 `spiders/shop_test.py`(临时测试:抓单店铺历史成交存 `deca_product_record_copy1`)时发现:某店铺历史成交只抓到 60 条就停,而 App 显示 1943 条。 实测确认根因是**服务端接口硬限**,不是采集逻辑漏采: - 已售接口 `/api/v1/app/groupbuy/merchant/sold-list`:`pageSize` 锁死 20(传其他值报 `code=10001 "PageSize不能小于或大于20"`),且 `page` 翻页**只开放前 3 页(60 条)**,page=4(offset≥60)返回 `total=0`、空 list。响应里的 `total`(如 1943)只是统计展示值,不代表可拉取条数。 - 盲试 13 个常见游标参数名(`lastCompletedAt`/`beforeCompletedAt`/`cursor`/`lastCode`/`lastId` 等)全被服务端忽略;App 端手动下滑同样是 3 页到底——确认无隐藏翻页方式。 - 在售接口 `/api/v1/app/groupbuy/merchant/on-sale-list` 同源、同样 `pageSize≤20`;实测库内在售最多的商家仅 29 个(远 <60),触不到上限。 结论:这类列表接口天花板就是最近 3 页,遂把在售/已售所有列表翻页上限统一收敛到 3 页,并清理因此失效的「连续无新页早停」逻辑。 ## 2. 改动内容 统一目标:所有在售/已售**列表**翻页上限 = 3 页;去重一律靠唯一键 `INSERT IGNORE` / upsert,不依赖翻页早停。 | 文件 | 函数 | 改动 | |---|---|---| | `common/deca_sold_core.py` | `get_sold_list` | `MAX_SOLD_PAGES` 500→3;删除 `STOP_AFTER_DUPE_PAGES` 常量与 `_filter_new_codes` 函数;去掉增量早停分支,统一翻 3 页 + `INSERT IGNORE`;`incremental` 参数保留仅兼容 `run_pipeline`,不再影响翻页 | | `spiders/buy_record_spider.py` | `get_monitored_onsale` | `ON_SALE_MAX_PAGES` 20→3(去重 = `save_products` upsert,原有 `total`/末页早停保留) | | `reports/onsale_report/deca_on_sale_report.py` | `full_onsale_sweep` | `ONSALE_MAX_PAGES` 30→3(去重 = `save_products` upsert,原有早停保留) | | `spiders/onsale_alert_spider.py` | `fetch_onsale` | 死常量 `MAX_PROD_PAGES` 100→3,并用它替换硬编码 `while page <= 20`(去重 = `product_code` 字典去重 + `INSERT IGNORE`) | `run_pipeline` 及 `sold_daily_spider.py` / `sold_history_spider.py` 的 `incremental=` 调用**未改**(保持最小影响面)。 ## 3. 改动前后对比 `common/deca_sold_core.py` `get_sold_list` 核心逻辑: ```python # 改动前:分全量/增量两条路径,增量靠 _filter_new_codes + STOP_AFTER_DUPE_PAGES 连续无新页早停 while page <= MAX_SOLD_PAGES: # 500 ... if incremental: new_codes = _filter_new_codes(pool, [...]) # 查库筛新 code rows = [r for r in rows if r["product_code"] in new_codes] pool.insert_many(table=T_PROD, data_list=rows, ignore=True) if incremental: if new_on_page == 0: dupe_pages += 1 if dupe_pages >= STOP_AFTER_DUPE_PAGES: break # 连续无新页早停 # 改动后:单一路径,翻满 3 页,去重全交给唯一键 INSERT IGNORE while page <= MAX_SOLD_PAGES: # 3 ... if rows: pool.insert_many(table=T_PROD, data_list=rows, ignore=True) # 靠唯一键 product_code 去重 if len(items) < PAGE_SIZE: break # 不足一页=末页 ``` ## 4. 注意事项 - **接口天花板不可逾越**:`sold_history`/`sold_daily` 对每个店铺同样只能采到最近 60 条历史成交,这是接口上限不是漏采。要积累更完整历史,只能靠每日增量长期跑(每天把新产生的成交追进库),不能指望一次性深翻。 - **在售的 3 页是纯保护值**:在售商家在售量普遍远 <60(实测 top 29 个),正常在第 1~2 页就靠 `total`/末页早停停下,3 页上限不会触发也不会漏采。 - **`incremental` 参数已成兼容摆设**:`get_sold_list` 保留它只为不改 `run_pipeline` 调用链;全量/增量翻页行为现已完全一致。若后续要彻底清理,需连带改 `run_pipeline` 与两个 spider 的调用。 - **校验**:4 个文件均通过 `py_compile`;`STOP_AFTER_DUPE_PAGES` / `_filter_new_codes` 全库无残留引用。 - `MAX_CARD_PAGES`(卡密清单)、`MAX_SHOP_PAGES`(商家列表)为不同接口,本次未涉及,保持原值。 ## 5. 后续增强:密集时段每小时占坑补采(同日) ### 5.1 背景 60 条上限本质是**按 `completedAt` 倒序的滑动窗口**:接口只吐最近 60 笔成交。若某商家在**相邻两次采集间隔内**成交 >60 笔,最早那批会被挤出前 3 页,永久漏采。而该店铺 13:00~06:00(实测真正密集在 22:00~04:00)售卖集中,每天一次 08:00 采集无法覆盖这种密集段。 ### 5.2 方案 在 `spiders/sold_daily_spider.py` 内新增高频轻量任务(不新建脚本): - **`hourly_task(log)`**:13:00~06:00 每整点跑一次,只对 `HOURLY_MERCHANT_IDS` 里的目标商家调 `core.get_sold_list`(翻 3 页 `INSERT IGNORE` 占坑入库),**不跑**详情/随机团/卡密/报告等精加工。 - **每天 08:00 `main_task`(完整 `run_pipeline`)不变**:负责详情补抓 / 随机团回补 / 拆卡报告等精加工。 **解耦是方案成立的关键**:`run_pipeline` 里"步骤 2 占坑入库"与"步骤 3~5 精加工"完全解耦——精加工均由**扫库存量**驱动(`fill_details` 扫未补详情的行、`backfill_team_amount` 扫 team 为 NULL 的行、`fill_reports` 扫无报告的行)。所以高频只需保证 `product_code` 尽早进库不丢,08:00 完整流程会自动扫到新行补齐;`INSERT IGNORE` 幂等,高频重复抓不会重复入库。 ### 5.3 数据依据(频率是否够) 统计库内 881226408 的成交(1912 条,跨 8/3~9/11)按「日期+小时」分桶: - **单小时成交峰值 = 41 笔**(9/9 凌晨 3 点),top15 全在 20~41 之间。 - 每小时采一次可覆盖最近 60 条,留 60−41=19 条余量 → **每小时够,不漏**。 - (库数据本身受"每天只采 60 条"限制,极端爆量日可能被低估。) ### 5.4 改动点 | 文件 | 改动 | |---|---| | `spiders/sold_daily_spider.py` | 新增常量 `HOURLY_MERCHANT_IDS`、`HOURLY_HOURS`(13~23、0~6);新增 `hourly_task`;`schedule_task` 注册 18 个整点跑 `hourly_task`;修正模块/`main_task` 已失准的"连续无新页早停"注释 | ### 5.5 注意事项 - **爆量风险**:若某商家新品首发单小时冲破 60,每小时仍会漏最早一段——届时对该商家提频到半小时/15 分钟(或按商家分级调度)。 - **既有隐患(本次未改)**:`main_task` 的 `@retry(wait_fixed(3600))` 是**同步阻塞**——若 08:00 完整流程失败进入重试,会 `sleep` 一小时并阻塞整个 `schedule` 单线程,期间 `hourly_task` 也无法触发。要让高频不受完整流程故障拖累,需将 `main_task` 失败重试改为非阻塞(独立线程/进程)。 - 扩商家:往 `HOURLY_MERCHANT_IDS` 追加 ID 即可。