断点续爬需持久化存储URL状态至数据库,字段含url、status、updated_at等;requests必须配Session、超时与Retry策略,区分可重试与应跳过错误。

断点续爬必须持久化记录已抓取的 URL
不存已抓取 URL,重启后根本无法判断哪些请求做过、哪些失败过。靠内存变量或临时文件完全不可靠,进程一崩就清零。
推荐直接写入数据库(如 SQLite 或 PostgreSQL),字段至少包含:url、status(success/failed/pending)、updated_at、response_size(可选)。避免用纯文件追加日志——查重、去重、分页都难做。
-
status = 'pending'用于标记已入队但未开始抓取的任务,防止重复入队 - 插入前先
SELECT COUNT(*) FROM tasks WHERE url = ? AND status IN ('success', 'failed'),跳过已完结任务 - 不要用
INSERT OR IGNORE依赖唯一索引代替逻辑判断——状态可能需要更新(比如失败后重试)
requests + requests.Session() 必须配超时与重试策略
网络抖动、DNS 失败、连接拒绝这些不是异常,是常态。裸调 requests.get() 没设 timeout,卡死半小时都等不到报错。
用 requests.Session() 管理连接池,并绑定 urllib3.Retry:
立即学习“Python免费学习笔记(深入)”;
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
<p>session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
-
backoff_factor=1表示第 n 次重试前等待1 * 2^(n-1)秒,比固定延时更抗突发限流 - 务必显式指定
allowed_methods,新版 urllib3 默认禁用重试 POST/PUT - 所有请求必须带
timeout=(3.05, 27)这类元组(connect, read),不能只写一个数字
故障恢复要区分“可重试”和“应跳过”的错误类型
不是所有异常都能重试。盲目重试 404、403、422 或返回空 HTML 的页面,只会浪费资源并触发风控。
建议在解析前做响应质量校验:
- 检查
r.status_code:4xx 错误基本不重试(除 429),5xx 可进重试队列 - 检查
r.headers.get('content-length')是否为 0 或极小(如 - 用
lxml.etree.fromstring(r.content)尝试解析,捕获LxmlError—— 很多反爬会返回 JS 跳转或乱码 HTML - 对失败任务,更新数据库中
status = 'failed'并记录error_type(如 'empty_body'、'parse_failed'),便于后续批量分析
并发控制下如何安全更新任务状态?
多线程/多进程并发时,直接 UPDATE ... WHERE url = ? 更新状态,可能因竞态导致同个 URL 被多次处理。尤其当任务刚被查出 pending,还没来得及改状态,另一线程又查到它。
解决方案只有两个靠谱路径:
- 用数据库行级锁:
SELECT ... FOR UPDATE(PostgreSQL/MySQL InnoDB 支持),查出 pending 任务后立刻加锁再更新状态为 running - 更轻量的做法:用原子性
UPDATE ... SET status = 'running' WHERE url = ? AND status = 'pending',检查rowcount == 1再继续;否则说明已被其他 worker 占用,直接跳过 - 绝对不要用“先 SELECT 再 UPDATE”的两步操作,中间任何延迟都会引发冲突
真正麻烦的从来不是怎么爬,而是怎么让几百个并发 worker 在不互相踩脚的前提下,把状态流转对齐到数据库里。这点容易被忽略,但线上一跑就暴露。


















