将url保存到数据库中,检查时在数据库中查找。效率太低,频繁的切换内外存。
将url保存到程序内存set集合中,查询速度快,但是占用内存太大。
与第二种方法类似,只是进一步改进之后,将url通过哈希编码压缩在保存在程序内存set集合中,相较于第二种方法直接保存,可以大大压缩存储空间。scrapy采用此方法。
这个方法将url通过哈希算法进一步压缩空间至某位上,存储空间大大减小,但是冲突率很高,很有可能两个不同的url哈希到同一个位,导致第二个没有的url被判断为已存在。
布隆过滤器:通过boolmfilter算法压缩url,在压缩存储空间的同时,也 大大降低冲突率,一亿url经过布隆过滤器后大约为11 M存储空间。
1)scrapy自带去重器路径:scrapy.dupefilters.py
2) 文件内容:
a) BaseDupeFilter(object)
b) RFPDupeFilter(BaseDupeFilter)
3) 去重原理分析
通过源码分析可以看出,RFPDupeFilter(BaseDupeFilter)继承自BaseDupeFilter(object),是BaseDupeFilter(object)的具体实现。所以可以得到两点信息:
def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + os.linesep)
从源码中可以看到其大概思路是:
首先通过request_fingerprint(request)函数将request转换为标识指纹fp(经过嘻哈算法之后的值)。
然后判断fp是否在fingerprints指纹集合(推测应该是个集合之类的数据结构)中。
如果在,就返回True。如果不在就说明之前没有这个request的记录,继续执行函数逻辑。之后将fp添加到指纹集合中。
然后判断file,如果存在这个file,就把fp写入本地文件。
同时还可以看到是在哪里调用了’‘request_seen(self, request)’‘这个函数。
这个是scheduler.py文件下Scheduler类中的一个方法,可以看到在这个方法中执行了去重逻辑,调用了上面的具体的去重器中的去重方法。
def enqueue_request(self, request):
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
return False
dqok = self._dqpush(request)
if dqok:
self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
else:
self._mqpush(request)
self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
self.stats.inc_value('scheduler/enqueued', spider=self.spider)
return True
跟踪进来发现是RFPDupeFilter对象的另一个方法。继续跟踪这里面的request——fingerpirnt(request)函数。
def request_fingerprint(self, request):
return request_fingerprint(request)
再跟踪来到了一个叫‘request.py’的文件,这个文件的路径是:scrapy/utils/request.py。
def request_fingerprint(request, include_headers=None):
"""
Return the request fingerprint.
The request fingerprint is a hash that uniquely identifies the resource the
request points to. For example, take the following two urls:
http://www.example.com/query?id=111&cat=222
http://www.example.com/query?cat=222&id=111
Even though those are two different URLs both point to the same resource
and are equivalent (ie. they should return the same response).
Another example are cookies used to store session ids. Suppose the
following page is only accesible to authenticated users:
http://www.example.com/members/offers.html
Lot of sites use a cookie to store the session id, which adds a random
component to the HTTP Request and thus should be ignored when calculating
the fingerprint.
For this reason, request headers are ignored by default when calculating
the fingeprint. If you want to include specific headers use the
include_headers argument, which is a list of Request headers to include.
"""
if include_headers:
include_headers = tuple(to_bytes(h.lower())
for h in sorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {})
if include_headers not in cache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url)))
fp.update(request.body or b'')
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]
看源码好多地方也不懂其原理,但重点应该就是后面这几句了。
进入这个if语句之后,也是通过哈希函数生成一个固定长度的哈希值,然后将include_headers作为键,生成的哈希值作为值存入字典类型的cache中。最后返回刚才生成的哈希值。
注意scrapy中是将整个request对象进行哈希编码保存去重的。
运行自己的scrapy项目,在return cache[include_headers]这里打断点调试。
可以查看到生成的各个对象的值和内容。
继续单步执行,单步执行,就返回到了request_seen(request)函数里面。
从这里可以看到返回的值是一串哈希之后的值。
class RFPDupeFilter(BaseDupeFilter):
"""Request Fingerprint duplicates filter"""
def __init__(self, path=None, debug=False):
self.file = None
self.fingerprints = set()
self.logdupes = True
self.debug = debug
self.logger = logging.getLogger(__name__)
if path:
self.file = open(os.path.join(path, 'requests.seen'), 'a+')
self.file.seek(0)
self.fingerprints.update(x.rstrip() for x in self.file)
@classmethod
def from_settings(cls, settings):
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(job_dir(settings), debug)
初始化方法中有个参数‘path’,默认位None。如果这个参数不为None,将创建文件‘request.seen’,将作为在’request_seen()‘函数中保存fp到本地的文件。
类初始化时在from_settings中加载settings中的’DUPEFILTER_DEBUG’值。这个值将作为log()方法中输出去重日志的启动开关。运行爬虫时,将在控制台输出去重日志。
def log(self, request, spider):
if self.debug:
msg = "Filtered duplicate request: %(request)s (referer: %(referer)s)"
args = {'request': request, 'referer': referer_str(request) }
self.logger.debug(msg, args, extra={'spider': spider})
elif self.logdupes:
msg = ("Filtered duplicate request: %(request)s"
" - no more duplicates will be shown"
" (see DUPEFILTER_DEBUG to show all duplicates)")
self.logger.debug(msg, {'request': request}, extra={'spider': spider})
self.logdupes = False
spider.crawler.stats.inc_value('dupefilter/filtered', spider=spider)
Boolmfilter去重,存储空间占用小,查询效率高,但是存在一定的误判率。所以一般的应用场景是:需要对海量的数据进行去重时且可以忍受一定的误判。
参考: