批量采集怎么不半路夭折:错误分类、退避重试与断点续跑
小红书数据
批量采集
错误处理
重试
单条调通和批量跑稳,是两件事。一万条笔记的采集任务,跑到第三千条挂掉,重跑一遍不但费钱,还可能因为"已经拿到的也重扣一次"而白花。
这篇讲批量场景下的三件事:怎么分类错误、怎么重试、怎么让任务可以从中断处继续。
先把错误分成三类
不是所有失败都该重试。分错类,轻则浪费额度,重则死循环。
| 类型 | 典型状态码 | 该怎么做 |
|---|---|---|
| 可重试 | 429、5xx、连接超时 |
指数退避后重试 |
| 不可重试 | 400、404 |
记下来跳过,重试一万次也一样 |
| 必须停机 | 401、402 |
立刻停整个任务并告警 |
第三类最容易被写错。见过不少任务把 402(余额不足)当成普通失败重试——余额不会因为重试变多,结果就是几万次无效请求刷满日志,真正的问题反而被埋掉了。
重试要退避,而且要有上限
import time, requests
RETRYABLE = {429, 500, 502, 503, 504}
FATAL = {401, 402}
def fetch(url, params, max_tries=5):
headers = {"X-API-Key": "YOUR_API_KEY"}
delay = 1.0
for attempt in range(max_tries):
try:
r = requests.get(url, headers=headers, params=params, timeout=30)
except requests.RequestException:
time.sleep(delay); delay *= 2; continue
if r.status_code in FATAL:
raise SystemExit(f"停机:{r.status_code} {r.text[:200]}")
if r.status_code in RETRYABLE:
time.sleep(delay); delay *= 2; continue
return r # 2xx 或不可重试的 4xx,交给调用方判断
return None # 重试用尽,记为失败项
三个细节:退避基数从 1 秒起翻倍(1→2→4→8→16);FATAL 直接抛出而不是返回;重试用尽返回 None 而不是抛异常,让主循环能把这一条记进失败清单继续跑下去。
让任务可以断点续跑
批量任务一定会中断——网络、部署、手滑 Ctrl+C。做法很简单:把"已完成"落盘,而不是留在内存里。
import json, pathlib
done = set()
state = pathlib.Path("done.txt")
if state.exists():
done = set(state.read_text().split())
with state.open("a") as f:
for note_id in all_ids:
if note_id in done: # 已完成,跳过,不再花钱
continue
r = fetch(url, {"note_id": note_id})
if r is None or r.status_code >= 400:
continue # 失败项留给第二轮,不写入 done
save(r.json())
f.write(note_id + "\n"); f.flush() # 关键:立刻 flush
flush() 那一行常被省掉,然后进程被 kill 时缓冲区里的几百条记录一起丢失——恰好是最需要它的时候失效。
两轮制:先跑通,再补漏
第一轮全量跑,失败的只记不重试;第二轮只跑失败清单。这样做的好处是第一轮不会被少数难缠的条目拖住,而第二轮往往因为限速已经过去、上游恢复,成功率反而很高。
跑完两轮还失败的,多半是真的取不到(笔记被删、账号注销),该记进"确认不可得"清单,而不是留在队列里无限重试。
开始使用
失败请求不扣费,所以合理的重试不会额外花钱——但请求本身仍占限速。建议给批量任务单独一把 API Key 并配低一些的并发。接口清单见文档,用量可在计费页按日期和接口核对。