scrapy 去重与 scrapy_redis 去重与布隆过滤器 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
zhangslob669
V2EX    Python

scrapy 去重与 scrapy_redis 去重与布隆过滤器

  •  
  •   zhangslob669 2019-03-26 17:50:22 +08:00 4278 次点击
    这是一个创建于 2396 天前的主题,其中的信息可能已经有所发展或是发生改变。

    这是从博客地址搬过来的

    在开始介绍 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 scrapy.Request(url, callback=self.get_response, 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 为十六进制数,如:'fad8cefa4d6198af8cb1dcf46add2941b4d32d78'。

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

     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方法,该方法返回如下

    >>> import w3lib.url >>> >>> # sorting query arguments >>> w3lib.url.canonicalize_url('http://www.example.com/do?c=3&b=5&b=2&a=50') 'http://www.example.com/do?a=50&b=2&b=5&c=3' >>> >>> # UTF-8 conversion + percent-encoding of non-ASCII characters >>> w3lib.url.canonicalize_url(u'http://www.example.com/r\u00e9sum\u00e9') 'http://www.example.com/r%C3%A9sum%C3%A9' >>> 

    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 valuesadded, 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 中可能并不是很好,保存在内存中也不好,所以就产生了布隆过滤器。

    布隆过滤器

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

    同时这也导致不能从 Bloom filter 中删除某个元素,无法确定这个元素一定在集合中。以及带来了误报的问题,当里面的数据越来越多,这个可能在集合中的靠谱程度就越来越低。(由于哈希碰撞,可能导致把不属于集合内的元素认为属于该集合)

    布隆过滤器的缺点是错判,就是说,不在里面的,可能误判成在里面,但是在里面的,就一定在里面,而且无法删除其中数据。

    >>> import pybloomfilter >>> fruit = pybloomfilter.BloomFilter(100000, 0.1, '/tmp/words.bloom') >>> fruit.update(('apple', 'pear', 'orange', 'apple')) >>> len(fruit) 3 >>> 'mike' in fruit False >>> 'apple' in fruit True 

    python3 使用pybloomfilter的例子。

    那么如何在 scrapy 中使用布隆过滤器呢,崔大大已经写好了,地址:ScrapyRedisBloomFilter,已经打包好,可以直接安装

    pip install scrapy-redis-bloomfilter 

    在 settings 中这样配置:

    # Ensure use this Scheduler SCHEDULER = "scrapy_redis_bloomfilter.scheduler.Scheduler" # Ensure all spiders share same duplicates filter through redis DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter" # Redis URL REDIS_URL = 'redis://localhost:6379/0' # Number of Hash Functions to use, defaults to 6 BLOOMFILTER_HASH_NUMBER = 6 # Redis Memory Bit of Bloomfilter Usage, 30 means 2^30 = 128MB, defaults to 30 BLOOMFILTER_BIT = 30 # Persist SCHEDULER_PERSIST = True 

    其实也是修改了调度器与去重方法,有兴趣的可以了解下。

    4 条回复    2019-03-27 10:35:19 +08:00
    bxb100
        1
    bxb100  
       2019-03-26 20:17:55 +08:00
    > 布隆过滤器的缺点是错判,就是说,不在里面的,可能误判成在里面,但是在里面的,就一定在里面,而且无法删除其中数据。
    -------------------------
    判断不在里面的一定不在吧
    adrianyoung
        2
    adrianyoung  
       2019-03-27 09:21:14 +08:00
    False Postive
    zhangslob669
        3
    zhangslob669  
    OP
       2019-03-27 09:24:49 +08:00
    @bxb100 布隆过滤器有宁可错杀一百,也不能放过一个的性质。讲人话就是在的就一定在,但不存在的可能在、也可能不在,存在一定的失误率。
    petelin
        4
    petelin  
       2019-03-27 10:35:19 +08:00 via iPhone
    @zhangslob669 不在的一定不在 在的不一定在 里嗦还是错的
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5470 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 43ms UTC 08:59 PVG 16:59 LAX 01:59 JFK 04:59
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86