bigywater.tech
爬虫工程 · 38

去重:set 与 Bloom 过滤器的内存和误判率实测

爬虫要判断一个 URL 抓过没有,最直接的是放进 set,最省内存的是 Bloom 过滤器。两者差多少、代价是什么?这篇用 100 万个 URL 实测内存、误判率,以及容量估计错了会怎样。

128 对 1.1 MiB
100 万个 URL 的 set 与 1% 误判率 Bloom 过滤器的内存
1.036%
Bloom(目标 1%)实测误判率——约 1 万个没见过的页面会被误认为已抓过
15.7%
装入 2 倍设计容量后的实测误判率

内存

实测 · 本机执行输出
  结构                                        内存      平均每个 URL
  set 里存完整 URL 字符串                 128.0 MiB      134.2 字节   (构建 1.7 秒,集合里 1,000,000 个元素)
  set 里存 16 字节 MD5 摘要               78.7 MiB       82.6 字节   (构建 3.0 秒,集合里 1,000,000 个元素)
  set 里存 64 位整数哈希                   66.3 MiB       69.5 字节   (构建 3.9 秒,集合里 1,000,000 个元素)
  set 里存 32 位整数(crc32)              62.5 MiB       65.5 字节   (构建 3.2 秒,集合里 999,873 个元素)
  Bloom 过滤器(目标误判率 1%,m=9,585,058 位,k=7)      1.1 MiB       1.20 字节   (构建 2.1 秒)
  Bloom 过滤器(目标误判率 0.1%,m=14,377,587 位,k=10)      1.7 MiB       1.80 字节   (构建 2.6 秒)

100 万个 URL 放进 set 要 128.0 MiB;把 URL 换成 MD5 摘要 78.7 MiB,换成 64 位整数还要 66.3 MiB——Python 的 set 加整数对象每个元素就有约 65 字节的开销。Bloom 过滤器 1% 误判率只要 1.1 MiB,0.1% 要 1.7 MiB。

误判的含义

实测 · 本机执行输出
  Bloom(目标 1%):实测误判 1.036%(按公式 (1-e^(-kn/m))^k 的理论值 1.004%),2073 / 200,000 个没见过的 URL 被误认为"已抓过"
  Bloom(目标 0.1%):实测误判 0.109%(按公式 (1-e^(-kn/m))^k 的理论值 0.100%),218 / 200,000 个没见过的 URL 被误认为"已抓过"
  set 里存 32 位 crc32:实测误判 0.021%(粗略预期 n / 2^32 = 0.023%)
  set 里存 64 位哈希:实测误判 0.0000%(粗略预期 n / 2^64 ≈ 0)
  set 里存完整 URL:实测误判 0.000%(精确集合没有误判)

Bloom 实测误判 1.036%,和理论公式 1.004% 吻合。对爬虫来说,误判不是“多抓一次”,而是“这个 URL 没抓过却被当作抓过,永远不会被抓”——是漏抓。用 32 位哈希放进 set 同样有这个问题:100 万个里就有 127 个撞车;64 位哈希在这个规模下没有观察到。

容量估错

实测 · 本机执行输出
         已加入      理论误判      实测误判
   1,000,000    1.004%    1.000%
   2,000,000   15.745%   15.737%
   3,000,000   43.604%   43.646%

过滤器按 100 万个、1% 设计,装到 200 万个误判率变成 15.7%,300 万个 43.6%,与公式吻合。爬虫事先不知道总量时,要么按上限预留,要么用可扩展的 Bloom(分层增长),本文没有测后者。

速度

实测 · 本机执行输出
  生成 URL 字符串本身:3738 千次/秒;set 查询(含生成):2400 千次/秒;Bloom 查询(含生成,k=7,纯 Python):480 千次/秒

我这份纯 Python 的 Bloom 查询 480 千次/秒,比 set 的 2400 千次/秒慢约 5 倍;速度对一个受网络限制的爬虫通常不是瓶颈,内存才是。

分步演示

set 与 Bloom 过滤器
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。URL 是 http://shop.test/category//item/?id=…&ref=list 形式的合成字符串;内存用 tracemalloc 的 current 值,只统计放进数据结构后留下的部分;Bloom 过滤器是我自己写的(blake2b 双哈希:index_i = a + i*b mod m;bytearray 当位数组);误判率用 20 万个从未插入的 URL 实测;每项各跑一次
    • 没有验证:C 实现的 Bloom 库(pybloom_live、pybloomfiltermmap 等)的速度与内存;可扩展 Bloom 与计数 Bloom;磁盘上的去重(SQLite、RocksDB)的内存与速度对比;更长或更短的 URL 对 set 内存的影响;Python 之外语言里 set/HashSet 的内存;分布式去重(下面有 Redis 的一篇)
    京ICP备2026067502号-1京公网安备11010502063487号