两个看起来差不多的 URL,Scrapy 认为是同一个请求还是两个?这篇把请求指纹的每个输入——URL 规范化、方法、请求体、请求头、meta、片段——逐个对比,再看 JOBDIR 里的 requests.seen 和指纹集合的内存开销。
差异 相同? 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。
默认: 相同 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 的请求不检查也不记录指纹。
第 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 之类的替代方案(本文没有测)。