zoukankan      html  css  js  c++  java
  • crapy 去重与 scrapy_redis 去重与 布隆过滤器

    在开始介绍 scrapy 的去重之前,先想想我们是怎么对 requests 对去重的。

    requests 只是下载器,本身并没有提供去重功能。所以我们需要自己去做。

    很典型的做法是事先定义一个去重队列,判断抓取的 url 是否在其中,如

    crawled_urls = set()
    
    def check_url(url):
     if url not in crawled_urls:
         return True
     return False
    

    此时的集合是保存在内存中的,随着爬虫抓取内容变多,该集合会越来越大,有什么办法呢?

    接着往下看,你会知道的

    scrapy 的去重

    scrapy 对 request 不做去重很简单,只需要在 request 对象中设置dont_filter为 True,如

    yield Request('https://www.baidu.com/?w=python',dont_filter=True)
    

    看看源码是如何做的

    _fingerprint_cache = weakref.WeakKeyDictionary()
    def request_fingerprint(request, include_headers=None):
     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]
    

    翻译

    返回请求指纹
    
    请求指纹是唯一标识请求指向的资源的哈希。 例如,请使用以下两个网址:
    
    http://www.example.com/query?id=111&cat=222
    http://www.example.com/query?cat=222&id=111
    
    即使这两个不同的 URL 都指向相同的资源并且是等价的(即,它们应该返回相同的响应)
    
    另一个例子是用于存储会话 ID 的 cookie。 假设以下页面仅可供经过身份验证的用户访问:
    
    http://www.example.com/members/offers.html
    
    许多网站使用 cookie 来存储会话 ID,这会随机添加字段到 HTTP 请求,因此在计算时应该被忽略指纹。
    
    因此,计算时默认会忽略 request headers。 如果要包含特定 headers,请使用 include_headers 参数,它是要计算 Request headers 的列表
    

    其实就是说:scrapy 使用 sha1 算法,对每一个 request 对象加密,生成 40 位十六进制数

    我们看源码,重点是以下三行

         fp = hashlib.sha1()
         fp.update(to_bytes(request.method))
         fp.update(to_bytes(canonicalize_url(request.url)))
         fp.update(request.body or b'')
    

    如果没有自定义 headers,只计算 method、url、和二进制 body,我们来计算下,代码

    print(request_fingerprint(scrapy.Request('http://www.example.com/query?id=111&cat=222')))
    print(request_fingerprint(scrapy.Request('http://www.example.com/query?cat=222&id=111')))
    print(request_fingerprint(scrapy.Request('http://www.example.com/query')))
    

    输出

    fad8cefa4d6198af8cb1dcf46add2941b4d32d78
    fad8cefa4d6198af8cb1dcf46add2941b4d32d78
    b64c43a23f5e8b99e19990ce07b75c295165a923
    

    可以看到第一条和第二条的密码是一样的,是因为调用了canonicalize_url方法

    scrapy 的去重默认会保存到内存中,如果任务重启,会导致内存中所有去重队列消失


    scrapy-redis 去重

    scrapy-redis 重写了 scrapy 的调度器和去重队列,所以需要在 settings 中修改如下两列

    # Enables scheduling storing requests queue in redis.
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    
    # Ensure all spiders share same duplicates filter through redis.
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    

    一般我们会在 redis 中看到这两个,分别是去重队列和种子链接

    源码

     def request_seen(self, request):
         """Returns True if request was already seen.
         Parameters
         ----------
         request : scrapy.http.Request
         Returns
         -------
         bool
         """
         fp = self.request_fingerprint(request)
         # This returns the number of values added, zero if already exists.
         added = self.server.sadd(self.key, fp)
         return added == 0
    
     def request_fingerprint(self, request):
         """Returns a fingerprint for a given request.
         Parameters
         ----------
         request : scrapy.http.Request
         Returns
         -------
         str
         """
         return request_fingerprint(request)
    

    首先拿到 scrapy.http.Request 会先调用 self.request_fingerprint 去计算,也就是 scrapy 的 sha1 算法去加密,然后会向 redis 中添加该指纹。

    该函数的作用是:计算该请求指纹,添加到 redis 的去重队列,如果已经存在该指纹,返回 True。

    我们可以看到,只要有在 settings 中添加DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter",就会在 redis 中新加一列去重队列,说下这样做的优劣势:

    1. 优点:将内存中的去重队列序列化到 redis 中,就算爬虫重启或者关闭,也可以再次使用,你可以使用 SCHEDULER_PERSIST 来调整缓存
    2. 缺点:如果你需要去重的指纹过大,redis 占用空间过大。8GB=8589934592Bytes,平均一个去重指纹 40Bytes,约可以存储 214,748,000 个(2 亿)。所以在做关系网络爬虫中,序列化到 redis 中可能并不是很好,保存在内存中也不好,所以就产生了布隆过滤器。

    自定义过滤

    如果想自定义 Filter,可以通过继承,重写 request_seen

    from scrapy.dupefilter import RFPDupeFilter
    class SeenURLFilter(RFPDupeFilter):
          """A dupe filter that considers the URL"""
          def __init__(self, path=None):
            self.urls_seen = set()
            RFPDupeFilter.__init__(self, path)
          def request_seen(self, request):
            if request.url in self.urls_seen:
                  return True
            else:
                  self.urls_seen.add(request.url)
    
    
    # 修改settings设置
    DUPEFILTER_CLASS ='scraper.custom_filters.SeenURLFilter'
    

    布隆过滤器

    bloomfilter:是一个通过多哈希函数映射到一张表的数据结构,能够快速的判断一个元素在一个集合内是否存在,具有很好的空间和时间效率。(典型例子,爬虫 url 去重)

    原理:
    BloomFilter 会开辟一个m位的bitArray(位数组),开始所有数据全部置 0 。当一个元素过来时,能过多个哈希函数(h1,h2,h3....)计算不同的在哈希值,并通过哈希值找到对应的bitArray下标处,将里面的值 0 置为 1 。

    在验证的时候只需要验证这些比特位是否都是 1 即可,如果其中有一个为 0,那么元素一定不在集合里,如果全为 1,则很可能在集合里。(因为可能会有其它的元素也映射到相应的比特位上)

    另外说明一下,当来查找对应的值时,同样通过哈希函数求值,再去寻找数组的下标,如果所有下标都为1时,元素存在。当然也存在错误率。(如:当数组全部为1时,那么查找什么都是存在的),但是这个错误率的大小,取决于数组的位数和哈希函数的个数。


    python 中使用布隆过滤器

    #python3.6 安装
    #需要先安装bitarray
    pip3 install bitarray-0.8.1-cp36-cp36m-win_amd64.whl(pybloom_live依赖这个包,需要先安装)
    #下载地址:https://www.lfd.uci.edu/~gohlke/pythonlibs/
    pip3 install pybloom_live
    

    示例一

    #ScalableBloomFilter 可以自动扩容
    from pybloom_live import ScalableBloomFilter
    
    bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
    
    url = "www.cnblogs.com"
    url2 = "www.liuqingzheng.top"
    
    bloom.add(url)
    
    print(url in bloom)
    print(url2 in bloom)
    

    示例二

    #BloomFilter 是定长的
    from pybloom_live import BloomFilter
    
    bf = BloomFilter(capacity=1000)
    url='www.baidu.com'
    bf.add(url)
    
    print(url in bf)
    print("www.liuqingzheng.top" in bf)
    

    用了 布隆过滤器 后,就可以判断 url 是否爬过,从而可以实现增量式爬取

    布隆过滤器的结果是存在内存中的,后期可以存进 redis 中

  • 相关阅读:
    论文笔记:目标检测算法(R-CNN,Fast R-CNN,Faster R-CNN,FPN,YOLOv1-v3)
    论文笔记:IRGAN——A Minimax Game for Unifying Generative and Discriminative Information
    springer论文模板参考文献的顺序问题
    CIFAR和SVHN在各CNN论文中的结果
    论文笔记:CNN经典结构2(WideResNet,FractalNet,DenseNet,ResNeXt,DPN,SENet)
    latex常用符号
    python中的引用传递,可变对象,不可变对象,list注意点
    ImageNet历年冠军和相关CNN模型
    matplotlib 的颜色
    调整matplotlib的图例legend的位置
  • 原文地址:https://www.cnblogs.com/kai-/p/12686579.html
Copyright © 2011-2022 走看看