Scrapy 结合布隆过滤器

背景:

因为最近在写scrapy项目碰到了需要实现爬虫重启之后可以自动过滤上一次已经爬过的网址,以避免重复爬,所以就在网上找有关scrapy结合布隆过滤器的一些方法,主要有两个不错的方法:

  • 一个是崔庆才大佬写的scrapy-redis-bloomfilter博客链接,这个是在分布式爬虫scrapy-redis的基础上加上BloomFiler实现的,重写了scrapy中的一些类:dupefilter、pipelines、queue、scheduler、spider…属实厉害,但是可能是我使用方法不对,我发现它好像只能是实现在爬的过程中的去重,不能实现增量去重,而且我还没有用到分布式爬虫scrapy-redis,于是放弃直接使用。
  • 另一个方法是我不认识的大佬写的,github地址 ,但是我在安装mmh3时出现了问题,找了一圈好像没有省事的解决方法(我懒),遂弃之。

所以我最后参照崔大佬的方法,改成我想要的样子。

一、实现布隆过滤器

  1. 这部分基本使用崔大佬的就好,大佬写的都是通过传参来实现低耦合,可以自定义bit数组长度,hash函数个数,我直接写死在里面了。(关于布隆过滤器原理,不懂的最好去看看,个人觉得这个写的还可以)
  2. 连接redis这部分,大佬写的各个地方都有连接,还有各种参数,由于我属实太菜,理不清,直接用了最蠢的方法
  3. 附上这两部分代码:
import redis

class HashMap(object):
    def __init__(self, m, seed):
        self.m = m
        self.seed = seed

    def hash(self, value):
        """
        Hash Algorithm
        :param value: Value
        :return: Hash Value
        """
        ret = 0
        for i in range(len(value)):
            ret += self.seed * ret + ord(value[i])
        return (self.m - 1) & ret


class BloomFilter(object):
    def __init__(self):
        """
        Initialize BloomFilter
        :param server: Redis Server
        :param key: BloomFilter Key
        :param bit: m = 2 ^ bit
        :param hash_number: the number of hash function
        """
        # default to 1 << 30 = 10,7374,1824 = 2^30 = 128MB, max filter 2^30/hash_number = 1,7895,6970 fingerprints
        self.m = 1 << 30
        self.seeds = range(6)
        self.server = conn
        self.key = 'BloomFilter' # 作为存储在redis中的30bit数组key
        self.maps = [HashMap(self.m, seed) for seed in self.seeds]

    def exists(self, value):
        """
        if value exists
        :param value:
        :return:
        """
        if not value:
            return False
        exist = True
        for map in self.maps:
            offset = map.hash(value)
            exist = exist & self.server.getbit(self.key, offset)
        return exist

    def insert(self, value):
        """
        add value to bloom
        :param value:
        :return:
        """
        for f in self.maps:
            offset = f.hash(value)
            self.server.setbit(self.key, offset, 1)


pool = redis.ConnectionPool(host='127.0.0.1', port=6379, password='yourpass', db=0)
conn = redis.StrictRedis(connection_pool=pool)

二、重写scrapy的dupefilter

  1. 建立一个py文件new_dupefilter.py,我是在scrapy项目中先新建了一个bloomfilter文件夹,再建这个py文件的。之后把scrapy的dupefilter.py原本的代码copy过来,接下来就是改其中的代码就好

(补:布隆过滤器实现代码在new_bloomfilter.py中)

springboot 布隆过滤器工具类 scrapy布隆过滤器_redis

  1. 关于scrapy去重原理最好去了解一下,不然你也不知道我重写这个干嘛。随便找的还可以的博客
  2. 重写dupefilter.py
  • 原本scrapy使用的是dupefilter中的request_seen()方法,来过滤请求的url。如果发现请求指纹已经(请求url经过算法加密之后成为指纹)存在就返回Ture,就告诉框架这个网址请求过了,过滤吧。否则加上新的请求的指纹。
  • 因为把这部分替换成从redis中找这次来的请求url之前有没有存放在布隆过滤器中,如果存在了返回True,不存在就在布隆过滤器中加上新的请求。

springboot 布隆过滤器工具类 scrapy布隆过滤器_布隆过滤器_02

  1. 附上这部分代码:
  • 导入写好的布隆过滤器:
from 你的项目名称.bloomfilter.new_bloomfilter import BloomFilter
# bloomfilter是文件夹,new_bloomfilter是.py文件 BloomFilter是里面的class
  • 在重写的new_dupefilter.py中的初始化init方法中,加上初始化redis的建立与连接
self.bf = BloomFilter()

springboot 布隆过滤器工具类 scrapy布隆过滤器_python_03

  • 重写过滤request_seen()方法
def request_seen(self, request):
        fp = self.request_fingerprint(request)
        if self.bf.exists(fp):
            print("这个网址重复啦:" + request.url)
            return True
        else:
            self.bf.insert(fp)
            return False

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P05D108v-1585802692415)(截图/1585798378713.png)]

三、启用我重写的新过滤器dupefilter

  1. 在settings中启用,加上这行代码就行
# Ensure all spiders share same duplicates filter through redis
DUPEFILTER_CLASS = "你的项目名称.bloomfilter.new_dupefilter.RFPDupeFilter"
# 项目名称之后是自己创建的文件夹名和文件名
  1. 排坑:我原以为这样就好了,结果发现一点效果没有,通过在scheduler.py中心打断点发现,scrapy中控制是否去重的参数dont_filter在各个地方的默认值还不一样,有的是False,有的是True(就贼离谱,感觉它在自己搞自己),所以需要重新爬虫文件中的start_requests()方法
# 因为原本的方法源码中请求发出去时设置dont_filter为True,所以要重写
    # Requst的init函数中初始化dont_filter为False
    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url)
  1. 至此大功告成啦!

关于分布式scrapy-redis,之后我用到了再试着重写有关的任务调度中心啥的的吧。