Python爬虫之url去重方案

url去重方案

1.去重方案

Python爬虫之url去重方案_第1张图片

  1. 将url保存到数据库中,检查时在数据库中查找。效率太低,频繁的切换内外存。

  2. 将url保存到程序内存set集合中,查询速度快,但是占用内存太大。

  3. 与第二种方法类似,只是进一步改进之后,将url通过哈希编码压缩在保存在程序内存set集合中,相较于第二种方法直接保存,可以大大压缩存储空间。scrapy采用此方法。

  4. 这个方法将url通过哈希算法进一步压缩空间至某位上,存储空间大大减小,但是冲突率很高,很有可能两个不同的url哈希到同一个位,导致第二个没有的url被判断为已存在。

  5. 布隆过滤器:通过boolmfilter算法压缩url,在压缩存储空间的同时,也 大大降低冲突率,一亿url经过布隆过滤器后大约为11 M存储空间。

2.scrapy url去重原理

1)scrapy自带去重器路径:scrapy.dupefilters.py
2) 文件内容
a) BaseDupeFilter(object)
b) RFPDupeFilter(BaseDupeFilter)
3) 去重原理分析
通过源码分析可以看出,RFPDupeFilter(BaseDupeFilter)继承自BaseDupeFilter(object),是BaseDupeFilter(object)的具体实现。所以可以得到两点信息:

  • scrapy中去重逻辑的实现依靠RFPDupeFilter(BaseDupeFilter)这个类。
  • 想要自定义去重器CustomFilter,同样可以继承BaseDupeFilter(object)这个类,实现其提供的具体方法。
a) 通过RFPDupeFilter.request_seen(request)函数完成去重
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
b) 如何生成request指纹fp——request_fingerprint(request)

跟踪进来发现是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对象进行哈希编码保存去重的。

c)调试验证

运行自己的scrapy项目,在return cache[include_headers]这里打断点调试。
Python爬虫之url去重方案_第2张图片
可以查看到生成的各个对象的值和内容。
继续单步执行,单步执行,就返回到了request_seen(request)函数里面。
Python爬虫之url去重方案_第3张图片
从这里可以看到返回的值是一串哈希之后的值。

d) RFPDupFilter对象初始化
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)

3.Boolmfilter

Boolmfilter去重,存储空间占用小,查询效率高,但是存在一定的误判率。所以一般的应用场景是:需要对海量的数据进行去重时且可以忍受一定的误判。
Python爬虫之url去重方案_第4张图片
参考:

  • Bloom Filter的基本原理和变种
  • Bloom Filter 算法具体解释

你可能感兴趣的:(爬虫填坑)