scrapy 中判断重复内容的方法(RFPDupeFilter)


爬虫抓取数据时,重复肯定是存在的,scrapy 是如何来筛选这些重复的 url 的呢?

这个处理的代码是编写在 dupefilter.py 文件中的,其中定义了处理重复 url 的方法。

在 scrapy 启动时,如果配置了重复 url 写入文件(requests.seen),那么就会以追加的方式打开这个文件,并且从这个文件中载入以前的数据到内存 set() 中保存,当遇到一个新来的 url 时,通过指纹计算,在已抓取 url 集合中查找,如果不存在,就添加进去,如果需要写入文件,就写入文件;如果已经存在了,告诉上层调用 url 已经抓取过了。

具体可以参考 class RFPDupeFilter(BaseDupeFilter) 类。

那么在 scrapy 中是如何来使用这个类的方法的呢?什么时候使用,这个流程是怎样的呢?

这个可以追溯到 scrapy.core.scheduler 中定义的 Scheduler 类来决定。

现在就来看看 Scheduler 类中和过滤重复 url 有关的内容。

在 Scheduler 类中,在调度时,采用了 memory queue 和 disk queue 的存储方法,所以,有一个入队的方法,在入队前,就要对 request 进行检查,检查是否是重复,如果已经重复了,就不入队了。

1
if not request.dont_filter and self.df.request_seen(request)

这里两个条件控制,首先是配置中 dont_filter,如果它是 True,就说明是不筛选的,如果是 False,才是要筛选的。
后面的 request_seen() 在默认内置的筛选方法中,就是 RFPDupeFilter() 中的方法,检查 request 是否已经存在。

只有要筛选且没有见过这个 request,才会去筛选 url。

所以这里已经很清晰了,调度器收到了 enqueue_request() 调用时,会检查这个 url 重复的判断开关,如果要筛选,就要检查这个 request 是否已经存在了;这里的检查 if 如果成立,就直接返回了,只有不成立时,才会有后续的存储操作,也就是入队。


下面来看看 scrapy 中是如何判断两个 url 重复的。

关键的函数是 request_fingerprint,这个是判断是否重复的关键实现方法。( scrapy.utils.request.request_fingerprint())。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def request_fingerprint(request, include_headers=None):
if include_headers:
include_headers = tuple([h.lower() for h in sorted(include_headers)])
cache = _fingerprint_cache.setdefault(request, {})
if include_headers not in cache:
fp = hashlib.sha1()
fp.update(request.method)
fp.update(canonicalize_url(request.url))
fp.update(request.body or '')
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[include_headers] = fp.hexdigest()
return cache[include_headers]

默认的调用情况下,计算的内容包括 method、格式化后的 url、请求正文,还有就是 http headers 是可选的。

和通常情况下不一样的是,这里的计算指纹,不是单纯的比较了 url 是否一致。计算的结果是一串 hash 16 进制数字。

这里自然产生了一个疑问,如果说计算指纹不是单纯的比较 url,那么 request 对象是个什么东西?当调用 request_fingerprint() 时, request 经过了哪些计算,是不是 request 传递到这里的时候,url 已经被下载过了?还是说没有下载?如果说已经下载过了,就出现了重复下载的问题,那去重的意义就很小很小了;如果没有下载过,method、header、body 的内容又是如何得知的呢?


request 对象是什么?它的生命周期是如何的?