bigywater.tech
爬虫工程 · 34

断点续爬:状态存哪、崩溃后丢什么

爬虫跑了几个小时被杀掉,重启后是从头来还是接着来?丢了多少、重复了多少?这篇让真实的爬虫进程在随机时刻被 SIGKILL,对比五种保存状态的做法:放内存、两种 JSON 快照、两种 SQLite 提交方式。

1 / 20
JSON 直接覆盖写,被杀后状态文件损坏的次数(原子改名是 0 / 20)
≤ 8
SQLite 每个 URL 提交一次时,重启后最多重复抓取的个数——等于在途请求数
0.55 秒 对 1.12 秒
SQLite 每个提交 与 JSON 整体重写 的无崩溃抓取耗时

保存状态的代价

实测 · 本机执行输出
  只放内存(没有持久化):0.54 秒;状态文件 0.0 MiB
  JSON 快照,每 25 个 URL 直接覆盖写:1.12 秒;状态文件 29.3 MiB
  JSON 快照,每 25 个写临时文件 + fsync + 原子改名:1.14 秒;状态文件 29.3 MiB
  SQLite(WAL),每抓完一个 URL 提交一次:0.55 秒;状态文件 29.5 MiB
  SQLite(WAL),每 25 个 URL 批量提交一次:0.65 秒;状态文件 29.5 MiB

300 个 100 KB 的响应,状态文件 29 MiB。JSON 快照每 25 个 URL 重写一次整个文件,耗时从 0.54 秒涨到 1.12 秒;SQLite 每抓完一个 URL 提交一次只是 0.55 秒。把整体状态当一个文件反复重写,代价随数据量增长。

被杀之后

实测 · 本机执行输出
  设计                                      状态文件损坏      重复抓取(平均)       重复抓取(最少/最多)    遗漏
  只放内存(没有持久化)                          0 / 20           112           0 / 203       0
  JSON 快照,每 25 个 URL 直接覆盖写             1 / 20            36           7 / 206       0
  JSON 快照,每 25 个写临时文件 + fsync + 原子改名       0 / 20            27          12 / 32        0
  SQLite(WAL),每抓完一个 URL 提交一次           0 / 20             7           0 / 8         0
  SQLite(WAL),每 25 个 URL 批量提交一次        0 / 20            18           7 / 32        0

只放内存,进度全丢,平均重复 112 个(最多 203,看杀在哪儿)。JSON 直接覆盖写在写到一半时被杀,文件被截断,20 次里有 1 次读不出来,只能从头开始。原子改名(写临时文件、fsync、再改名)不会损坏。SQLite 每个 URL 提交一次,重启后最多重复抓取在途的那 8 个。

至少一次的语义

实测 · 本机执行输出
  JSON 快照,每 25 个 URL 直接覆盖写:207/175, 207/175, 157/150, 207/175, 223/200, 215/200
  JSON 快照,每 25 个写临时文件 + fsync + 原子改名:206/175, 181/150, 181/150, 207/175, 182/150, 207/175
  SQLite(WAL),每抓完一个 URL 提交一次:66/59, 44/38, 70/62, 111/103, 112/104, 59/51
  SQLite(WAL),每 25 个 URL 批量提交一次:73/50, 165/150, 57/50, 165/150, 143/125, 122/100

已提交的状态不会丢;没提交的会重新抓。所有设计的“遗漏”都是 0,但“重复”从 7 到 112 不等。要做到不重复,需要把“抓取”和“保存”做成同一个事务,或让下游对重复幂等。

分步演示

断点续爬
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。爬虫是单独的 Python 子进程(requests,8 个线程,抓本机服务器上 300 个各 100 KB 的页面),状态存放在 JSON 文件或 SQLite(WAL、synchronous=NORMAL)里;父进程用 SIGKILL 在各设计无崩溃耗时的 15%–85% 的位置杀掉它(从子进程启动那一刻算起,包含 Python 启动时间),再让第二个子进程续爬到结束;“重复”=服务器收到的请求总数减去 300;“遗漏”=续爬完成后服务器一次也没收到的 URL 数
    • 没有验证:断电或系统崩溃(SIGKILL 只杀进程,操作系统缓冲区里的数据仍会写盘;这里的 SQLite synchronous=NORMAL 和 JSON 不 fsync 在断电时的表现没有测);更多杀死次数下 JSON 直接覆盖写的损坏概率(20 次里 1 次只是一个样本);分布式队列(Redis 等)的持久化;状态里同时有队列和已见集合时的一致性;网络文件系统上的行为
    京ICP备2026067502号-1京公网安备11010502063487号