返回博客

批量采集怎么不半路夭折:错误分类、退避重试与断点续跑

Rnote API 团队 · · 4 次阅读 · English
小红书数据 批量采集 错误处理 重试

单条调通和批量跑稳,是两件事。一万条笔记的采集任务,跑到第三千条挂掉,重跑一遍不但费钱,还可能因为"已经拿到的也重扣一次"而白花。

这篇讲批量场景下的三件事:怎么分类错误、怎么重试、怎么让任务可以从中断处继续。

先把错误分成三类

不是所有失败都该重试。分错类,轻则浪费额度,重则死循环。

类型 典型状态码 该怎么做
可重试 4295xx、连接超时 指数退避后重试
不可重试 400404 记下来跳过,重试一万次也一样
必须停机 401402 立刻停整个任务并告警

第三类最容易被写错。见过不少任务把 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 并配低一些的并发。接口清单见文档,用量可在计费页按日期和接口核对。