bigywater.tech
Scrapy · 9

去重:RFPDupeFilter 的请求指纹怎么算

两个看起来差不多的 URL,Scrapy 认为是同一个请求还是两个?这篇把请求指纹的每个输入——URL 规范化、方法、请求体、请求头、meta、片段——逐个对比,再看 JOBDIR 里的 requests.seen 和指纹集合的内存开销。

SHA1 20 字节
请求指纹 = SHA1(方法 + 规范化 URL + 请求体)
22 字节
requests.seen 里每个指纹(2 字节长度 + 20 字节)
86.6 MB
100 万个指纹放进 Python set 的内存

URL 规范化

实测 · 本机执行输出
  差异                          相同?    URL 1                             URL 2
  查询参数顺序                    相同     http://a.com/p?a=1&b=2            http://a.com/p?b=2&a=1
  片段 #frag                  相同     http://a.com/p?a=1                http://a.com/p?a=1#top
  主机名大小写                    相同     http://A.COM/p                    http://a.com/p
  默认端口 :80                  不同     http://a.com/p                    http://a.com:80/p
  百分号编码的十六进制大小写             相同     http://a.com/p?q=%e4%b8%ad        http://a.com/p?q=%E4%B8%AD
  中文原样 与 百分号编码              相同     http://a.com/p?q=中                http://a.com/p?q=%E4%B8%AD
  空格 %20 与 +                相同     http://a.com/p?q=a%20b            http://a.com/p?q=a+b
  末尾的 / (路径)                不同     http://a.com/p                    http://a.com/p/
  http 与 https              不同     http://a.com/p                    https://a.com/p
  空参数 ?a= 与没有 a             不同     http://a.com/p?a=                 http://a.com/p
  末尾的 ?                     相同     http://a.com/p?                   http://a.com/p
  路径 /./ 和 /../             相同     http://a.com/x/../p               http://a.com/p
  重复键的顺序 a=1&a=2            相同     http://a.com/p?a=1&a=2            http://a.com/p?a=2&a=1
  www.a.com 与 a.com         不同     http://www.a.com/p                http://a.com/p

指纹先用 w3lib.url.canonicalize_url 规范化 URL:查询参数排序、去掉片段、统一百分号编码大小写等。但 :80 默认端口、末尾的 /、www. 前缀都不会被抹平。

方法、请求体、请求头

实测 · 本机执行输出
  GET 与 POST(同 URL): 不同
  POST 体 {"a":1,"b":2} 与 {"b":2,"a":1}(同样的 JSON,键顺序不同): 不同
  POST 体 "a=1&b=2" 与 "b=2&a=1": 不同
  POST 体 多一个空格: 不同
  Cookie 头不同: 相同
  Accept-Language 头不同(默认): 相同
  Accept-Language 头不同,fingerprint(include_headers=['Accept-Language']): 不同
  meta 不同(默认): 相同
  自定义 fingerprinter(把 meta['page'] 算进去),meta 不同: 不同
  fingerprint 的长度: 20 字节(SHA1);十六进制: 5174c47860791a95…

方法和请求体参与指纹(请求体按字节比较,JSON 键顺序不同就是不同请求);请求头和 meta 默认不参与,要自定义 fingerprinter。

片段与原样 URL

实测 · 本机执行输出
  默认: 相同
  keep_fragments=True: 不同
  meta verbatim_url=True(不规范化): 不同
  verbatim_url 下两个“只差参数顺序”的 URL: 不同

keep_fragments 和 verbatim_url 可以分别保留片段、取消规范化。

过滤的表现

实测 · 本机执行输出
  默认:服务器收到 ['/seed', '/x?a=1&b=2', '/y', '/x?a=1&b=2'];dupefilter/filtered=2;scheduler/enqueued=4
      [scrapy.dupefilters] DEBUG: Filtered duplicate request: <GET http://127.0.0.1:9960/x?b=2&a=1> - no more duplicates will be shown (see DUPEFILTER_DEBUG to show all duplicates)
  DUPEFILTER_DEBUG=1:服务器收到 ['/seed', '/x?a=1&b=2', '/x?a=1&b=2', '/y'];dupefilter/filtered=2;scheduler/enqueued=4
      [scrapy.dupefilters] DEBUG: Filtered duplicate request: <GET http://127.0.0.1:9960/x?b=2&a=1> (referer: http://127.0.0.1:9960/seed)
      [scrapy.dupefilters] DEBUG: Filtered duplicate request: <GET http://127.0.0.1:9960/x?a=1&b=2#f> (referer: http://127.0.0.1:9960/seed)

被过滤的请求不会发出,只在 dupefilter/filtered 里计数;dont_filter=True 的请求不检查也不记录指纹。

JOBDIR 与 requests.seen

实测 · 本机执行输出
  第 1 次运行:seed(dont_filter=True)+ p0…p999:服务器收到 1001 个请求;dupefilter/filtered=0;requests.seen = 22,000 字节
  第 2 次运行,同一个 JOBDIR:seed(dont_filter=True)+ p0…p1499:服务器收到 501 个请求;dupefilter/filtered=1000;requests.seen = 33,000 字节
  第 3 次运行,同一个 JOBDIR:seed 没写 dont_filter(默认 False):服务器收到 1 个请求;dupefilter/filtered=1500;requests.seen = 33,022 字节
  22 字节/个 × (1 个 seed + 1500 个 p) = 33,022 字节(与文件大小一致)

启用 JOBDIR 后指纹顺序追加写进 requests.seen,下次运行读回来;自己写的 start() 要自己加 dont_filter=True,否则重启时起始请求会被去重。

内存与耗时

实测 · 本机执行输出
  计算 20,000 个指纹:平均 10.3 微秒/个(含 URL 规范化 + SHA1,首次计算)
  1,000,000 个 20 字节指纹放进 set:tracemalloc 当前 86.6 MB(平均 87 字节/个,指纹本身 20 字节,bytes 对象 53 字节)

每个指纹 20 字节,但在 Python set 里平均占 87 字节;计算一个指纹约 16 微秒。大规模爬取要考虑 Bloom filter 之类的替代方案(本文没有测)。

分步演示

请求去重
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。第一到三节直接在虚拟环境里调用 scrapy.utils.request.fingerprint;第四、五节是真实抓取;第六节指纹耗时用 20,000 个不同 URL 的请求,内存用 tracemalloc 测 100 万个 SHA1 的 set
    • 没有验证:Bloom filter 或 Redis 去重的替代实现;REQUEST_FINGERPRINTER_CLASS 其它实现的互操作;缓存(HTTP 缓存)用的指纹是否与去重一致;不同 w3lib 版本的规范化差异;百万级请求的真实 RSS(只测了 set 本身)
    京ICP备2026067502号-1京公网安备11010502063487号