限流发生时,第一目标不是继续拿全量数据,而是先把已经拿到的结果完整保存下来,再决定是降速重试、分批补抓,还是接受不完整结果。只要脚本没有把“已写入的结果”和“待抓取任务”分开,限流就可能让前面几十分钟的查询白做。
保护已有结果之前,要区分两种情形。第一种是单次请求被拒绝,但会话、参数和已返回的数据都正常,说明只是频率超过了对方允许的节奏。第二种是连续多次被拒绝,甚至已经返回空响应、错误码混杂,说明脚本可能触发了更严格的限制,继续硬跑只会扩大失败面。
可用的判断依据是看三件事:被拒绝前是否已经稳定写入过结果;拒绝是否集中在短时间窗口内;换一个更慢的间隔后,是否还能拿到正常响应。如果第三项仍失败,就不应把原因归为单纯限流,还要检查参数、权限和任务范围。请求量归零或抓取量骤降也不能单独证明脚本被正确限流,它也可能是目标端临时不可用、参数写错或本地任务提前退出。
如果脚本已经把每次返回写进本地文件或数据库,处理顺序应是:先停止新请求,确认最后一条成功记录的位置,再把待抓取清单单独保存。这样做的结果是,后续重试可以从断点继续,而不是从头再查一遍。
实际操作可以按这个顺序:
假设一个脚本需要查询 200 条记录,抓到第 80 条时开始被限流。如果结果文件已保存前 80 条,待办清单从第 81 条开始,那么恢复时只需处理剩余 120 条。若没有断点记录,就只能重新从第 1 条开始,既浪费时间,也再次增加触发限流的概率。
例外是:如果结果之间强依赖,例如后一条查询必须基于前一条返回的字段,那么不能简单从断点续跑,而要先保存依赖链,再决定是否重跑整段。此时保护已有结果的重点是保存中间状态,而不是只保存最终输出。
如果脚本一直把结果放在内存里,直到全部跑完才写文件,限流一来就可能全部丢失。这种情况下,最合理的动作不是继续重试,而是立即改变写入策略:每成功一条就追加保存,哪怕格式暂时不完整,也比全部丢失强。
可执行的最小动作是:先把当前内存中已经拿到的结果导出为临时文件,再给后续请求加上逐条写入。这样做的结果是,即使下一次限流再次出现,也只会损失最后一条未写入的数据,而不会损失整批结果。
需要接受的代价是:逐条写入会降低脚本速度,也可能让结果文件出现重复或半成品记录。因此要同时记录状态字段,例如“已完成”“待重试”“失败”。后续处理时先按状态筛选,再决定是否补抓。不能因为文件里有记录就默认它一定完整,也不能因为某次请求失败就推断整个查询不可用。
恢复策略取决于两个条件:结果是否已经足够支撑当前决策,以及剩余任务是否必须补齐。如果已有结果已经覆盖关键区间,可以先用于初步判断,同时把未完成部分标记为缺口。如果剩余任务属于必须完整的数据,就降速分批执行,而不是一次性重跑。
降速时不要只调大固定间隔。更稳妥的做法是设置阶梯间隔,例如第一次重试等待较短时间,连续失败后再逐步拉长,并设置总时限。总时限到达后停止,保留已有结果和待办清单。这样做的结果是,脚本不会因为无限重试而长时间占用资源,也便于下一次从明确位置继续。
以下情况应考虑放弃自动补抓:目标端持续拒绝且没有恢复迹象;剩余任务对当前结论影响很小;继续重试的成本已经高于重新设计查询范围。放弃不等于删除结果,而是把已有结果、缺口和失败原因一起留存,供下一次判断。
有些限流并不是按请求次数触发,而是按并发连接、会话时长或单位时间内的数据量触发。此时单纯降低每秒请求数可能不够,还需要减少并发、缩短单次会话,或把大任务拆成多个小批次。具体阈值和触发条件因工具而异,需要以实际返回的错误信息和自身测试为准,不能套用固定数字。
另一个例外是结果本身带有时效性。如果查询的是会变化的数据,冻结旧结果只能保护“当时拿到的版本”,不能保证它仍然代表当前状态。这种情况下,保存结果的同时要记录抓取时间,后续使用时明确它是历史快照,而不是实时结论。
无论采用哪种方式,保护已有结果的核心都是把“已获得的数据”和“尚未完成的任务”分开管理。只要这两部分没有混在一起,限流带来的损失就只是延迟,而不是全部作废。