Scrayp-集成scrapy_redis和bloomfilter实现增量

前言

(备注一下,我的开发环境不是Linux就是MacOSX,Windows很多写法不是这样的)

在爬取数据的过程中,有时候需要用到定时、增量爬取。定时这里暂且不说,先说增量爬取。

  • 我想要的增量爬取目前只是简单的,根据url请求来判断是否爬过,如果爬过则不再爬。
  • 复杂一些的增量则是重复爬取,根据指定的几个字段判断是否值有变化,值有变化也算作增量,应当爬取且只更新变化部分(比如天猫商品数据,商品的价格有变化则更新价格,但是url是重复的,也应当爬取)

网上增量爬取的文章很多,包括看过慕课网Scrapy课的笔记,但是它还是不完善,我将在这个基础上进行实际集成。


布隆简介

Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

输入图片说明

具体的bloomfilter概念和原理应该查看这篇文章:传送,还有《海量数据处理算法》以及《大规模数据处理利器》

布隆优点

相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。另外, Hash 函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

布隆过滤器可以表示全集,其它任何数据结构都不能;

k 和 m 相同,使用同一组 Hash 函数的两个布隆过滤器的交并差运算可以使用位操作进行。

布隆缺点

但是布隆过滤器的缺点和优点一样明显。误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。

另外,一般情况下不能从布隆过滤器中删除元素. 我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

总的来说,布隆很适合来处理海量的数据,而且速度优势很强。


redis与bloom

去重”是日常工作中会经常用到的一项技能,在爬虫领域更是常用,并且规模一般都比较大。参考文章《基于Redis的Bloomfilter去重》,作者【九茶】还有另一篇文章可以参考《scrapy_redis去重优化,已有7亿条数据》

去重需要考虑两个点:去重的数据量、去重速度。

为了保持较快的去重速度,一般选择在内存中进行去重。

  • 数据量不大时,可以直接放在内存里面进行去重,例如python可以使用set()进行去重。
  • 当去重数据需要持久化时可以使用redis的set数据结构。
    
  • 当数据量再大一点时,可以用不同的加密算法先将长字符串压缩成 16/32/40 个字符,再使用上面两种方法去重;
    
  • 当数据量达到亿(甚至十亿、百亿)数量级时,内存有限,必须用“位”来去重,才能够满足需求。Bloomfilter就是将去重对象映射到几个内存“位”,通过几个位的 0/1值来判断一个对象是否已经存在。
    
  • 然而Bloomfilter运行在一台机器的内存上,不方便持久化(机器down掉就什么都没啦),也不方便分布式爬虫的统一去重。如果可以在Redis上申请内存进行Bloomfilter,以上两个问题就都能解决了。
    
  1. Bloomfilter算法如何使用位去重,这个百度上有很多解释。简单点说就是有几个seeds,现在申请一段内存空间,一个seed可以和字符串哈希映射到这段内存上的一个位,几个位都为1即表示该字符串已经存在。插入的时候也是,将映射出的几个位都置为1。

  2. 需要提醒一下的是Bloomfilter算法会有漏失概率,即不存在的字符串有一定概率被误判为已经存在。这个概率的大小与seeds的数量、申请的内存大小、去重对象的数量有关。下面有一张表,m表示内存大小(多少个位),n表示去重对象的数量,k表示seed的个数。例如我代码中申请了256M,即1<<31(m=2^31,约21.5亿),seed设置了7个。看k=7那一列,当漏失率为8.56e-05时,m/n值为23。所以n = 21.5/23 = 0.93(亿),表示漏失概率为8.56e-05时,256M内存可满足0.93亿条字符串的去重。同理当漏失率为0.000112时,256M内存可满足0.98亿条字符串的去重。

  3. 基于Redis的Bloomfilter去重,其实就是利用了Redis的String数据结构,但Redis一个String最大只能512M,所以如果去重的数据量大,需要申请多个去重块(代码中blockNum即表示去重块的数量)。

  4. 代码中使用了MD5加密压缩,将字符串压缩到了32个字符(也可用hashlib.sha1()压缩成40个字符)。它有两个作用,一是Bloomfilter对一个很长的字符串哈希映射的时候会出错,经常误判为已存在,压缩后就不再有这个问题;二是压缩后的字符为 0~f 共16中可能,我截取了前两个字符,再根据blockNum将字符串指定到不同的去重块进行去重

总结:基于Redis的Bloomfilter去重,既用上了Bloomfilter的海量去重能力,又用上了Redis的可持久化能力,基于Redis也方便分布式机器的去重。在使用的过程中,要预算好待去重的数据量,则根据上面的表,适当地调整seed的数量和blockNum数量(seed越少肯定去重速度越快,但漏失率越大)。


编写代码

安装依赖

根据github上的资源《BloomFilter_imooc》以及思路来编写bloomfilter的代码。

先前说过,bloom是一种算法,而不是插件也不是软件,它依赖于mmh3,所以需要在虚拟环境中安装mmh3.

然而当我在本机的anaconda虚拟环境内安装时,出现了报错:

g++: error trying to exec 'cc1plus': execvp: 没有那个文件或目录

网上查阅了很多文章,找到一个适合我的:传送,大致原因是电脑上的gcc版本与g++版本不一致引起的。可以打开终端用命令:

gcc -v

g++ -v

来查看两个东西的版本,最终发现用g++的时候报错,于是我安装它:

sudo apt-get install g++

如果是在阿里云服务器,命令改成:

yum install gcc-c++

安装成功后,再次到anaconda虚拟环境中安装mmh3,才成功安装。


编写bloom代码

根据文章《将bloomfilter(布隆过滤器)集成到scrapy-redis中》的指引,作者是将github代码下载到本地目录。

而我为了省事,我在site-package里面写。
在site-package下新建bloofilter_scrapy_redis的package包(带init那种),然后在里面新建文件bloomfilter.py,编写代码:

# -*- coding: utf-8 -*-
# 18-1-21 下午2:22
# RanboSpider

import mmh3
import redis
import math
import time


class PyBloomFilter():
    #内置100个随机种子,种子越多需要的内存就越大,内存小的服务器用30个种子就行了
    SEEDS = [543, 460, 171, 876, 796, 607, 650, 81, 837, 545, 591, 946, 846, 521, 913, 636, 878, 735, 414, 372,
             344, 324, 223, 180, 327, 891, 798, 933, 493, 293, 836, 10, 6, 544, 924, 849, 438, 41, 862, 648, 338,
             465, 562, 693, 979, 52, 763, 103, 387, 374, 349, 94, 384, 680, 574, 480, 307, 580, 71, 535, 300, 53,
             481, 519, 644, 219, 686, 236, 424, 326, 244, 212, 909, 202, 951, 56, 812, 901, 926, 250, 507, 739, 371,
             63, 584, 154, 7, 284, 617, 332, 472, 140, 605, 262, 355, 526, 647, 923, 199, 518]

    #capacity是预先估计要去重的数量
    #error_rate表示错误率
    #conn表示redis的连接客户端
    #key表示在redis中的键的名字前缀
    def __init__(self, capacity=1000000000, error_rate=0.00000001, conn=None, key='BloomFilter'):
        self.m = math.ceil(capacity*math.log2(math.e)*math.log2(1/error_rate))      #需要的总bit位数
        self.k = math.ceil(math.log1p(2)*self.m/capacity)                           #需要最少的hash次数
        self.mem = math.ceil(self.m/8/1024/1024)                                    #需要的多少M内存
        self.blocknum = math.ceil(self.mem/512)                                     #需要多少个512M的内存块,value的第一个字符必须是ascii码,所有最多有256个内存块
        self.seeds = self.SEEDS[0:self.k]
        self.key = key
        self.N = 2**31-1
        self.redis = conn
        # print(self.mem)
        # print(self.k)

    def add(self, value):
        name = self.key + "_" + str(ord(value[0])%self.blocknum)
        hashs = self.get_hashs(value)
        for hash in hashs:
            self.redis.setbit(name, hash, 1)

    def is_exist(self, value):
        name = self.key + "_" + str(ord(value[0])%self.blocknum)
        hashs = self.get_hashs(value)
        exist = True
        for hash in hashs:
            exist = exist & self.redis.getbit(name, hash)
        return exist

    def get_hashs(self, value):
        hashs = list()
        for seed in self.seeds:
            hash = mmh3.hash(value, seed)
            if hash >= 0:
                hashs.append(hash)
            else:
                hashs.append(self.N - hash)
        return hashs


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

这里的pool和conn都是单独连接的,实际上在分布式爬虫中是比较不友好的,多台机器的配置就会烦人,这里暂且这样,后期我再改。

是否配置密码

至于是否配置密码,如何配置密码,在bloomfilter.py文件中,有一句:

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

其中redis.StrictRedis方法,跟踪(ctrl+左键点击)进去,可以看到init初始化方法里面有个password=None

def __init__(self, host='localhost', port=6379,
                 db=0, password=None, socket_timeout=None,
                 socket_connect_timeout=None,
                 socket_keepalive=None, socket_keepalive_options=None,
                 connection_pool=None, unix_socket_path=None,
                 encoding='utf-8', encoding_errors='strict',
                 charset=None, errors=None,
                 decode_responses=False, retry_on_timeout=False,
                 ssl=False, ssl_keyfile=None, ssl_certfile=None,
                 ssl_cert_reqs=None, ssl_ca_certs=None,
                 max_connections=None):

这里应该是设置password,也就是将服务器redis的权限密码auth设置进来。

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

即可完成密码的设置。


集成到scrapy_redis中

上面的布隆过滤器代码写好后,需要集成到scrapy_redis中。完成去重任务的是dupefilter.py文件,就要对它进行改造,路径是site-package/scrapy_redis/目录内:

现将刚才编写的布隆选择器导入此文件

from bloomfilter_scrapy_redis.bloomfilter import conn,PyBloomFilter  # 从源码包导入布隆

然后在init方法中初始化布隆选择器(这里贴上整个init代码):

    def __init__(self, server, key, debug=False):
        """Initialize the duplicates filter.

        Parameters
        ----------
        server : redis.StrictRedis
            The redis server instance.
        key : str
            Redis key Where to store fingerprints.
        debug : bool, optional
            Whether to log filtered requests.

        """
        self.server = server
        self.key = key
        self.debug = debug
        self.logdupes = True

       """ 集成布隆过滤器,通过连接池连接redis """
        self.bf = PyBloomFilter(conn=conn, key=key)

接下来改动request_seen方法,在里面对request进行判断,如果此次request请求在redis中存在,则直接返回,如果不存在则添加到redis的队列里面去,让爬虫去爬:

    def request_seen(self, request):
        """
            ……
        """
       
        fp = self.request_fingerprint(request)

        """
        集成布隆过滤
            判断redis是否存在此指纹,如果存在则直接返回true
            如果不存在添加指纹到redis,同时返回false
        """
        if self.bf.is_exist(fp):
            return True
        else:
            self.bf.add(fp)
            return False

        """ 集成布隆过滤器,将下方2行代码注释 """
        # This returns the number of values added, zero if already exists.
        # added = self.server.sadd(self.key, fp)
        # return added == 0

到这里即完成了scrapy_redis对布隆过滤器的集成。


测试

在爬虫代码中编写:

# -*- coding: utf-8 -*-
import scrapy
from scrapy_redis.spiders import RedisSpider
from scrapy.http import Request
from urllib import parse


class JobboleSpider(RedisSpider):
    name = 'jobbole'
    allowd_domains = ["www.gxnhyd.com"]
    redis_key = 'jobbole:start_urls'

    def parse(self, response):
        """
        将当前列表页的每条标的链接拿到 并传给detail进行深入爬取
        通过已知列表页码数量 进行循环爬取 就不用翻页了
        """
        total = response.css('.item .tl.pl10 a')
        for x in total:
            title = x.css('::text').extract_first("")
            title_url = x.css('::attr(href)').extract_first("")
            yield Request(url=parse.urljoin(response.url, title_url), callback=self.parse_detail)

        for i in range(1, 10):
            next_pages = "http://www.gxnhyd.com/deals/p-%s" % (i)
            yield Request(url=next_pages, callback=self.parse)

    def parse_detail(self, response):
        """
        获取当前详情页的标的信息 包括金额 收益 期限 借款人
            投资人列表 - 投资人用户名/投资人投资金额/投资方式/投资时间等
        :param response:
        :return:
        """
        print(response.url)

通过print对爬取情况做观察

开启爬虫后,由于scrapy_redis的特性,需要给redis里面添加start_urls:

lpush jobbole:start_urls http://www.gxnhyd.com/deals [value ...]

爬虫监听到值之后,立即开始爬取,这一步没问题

但是爬完后它空跑了,不会结束,一直空跑。(事实证明,跑空了也不要紧)


二次测试

在第一次测试通过后,我加大了循环次数for i in range(1, 30),看看是否会出现重复的值,结果报错了。

报错信息与bloom是否重复无关,原因是我之前看到空跑,就主动停止了代码,导致redis报错:

MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist

解决办法在这里《redis异常解决:MISCONF Redis 》,在redis-cli用命令解决这个权限问题:

config set stop-writes-on-bgsave-error no

二次测试后,发现可以正常运行了。然后观察到bloom也生效了,但是还是有空跑的问题


解决空跑(这个办法其实不太好,不推荐)

空跑就是爬虫在爬取完所有的队列有,不会自动停止,而是一直请求请求,然后观察redis-server窗口有memory的提示一直在进行。

解决这个空跑问题参考了一些资料《scrapy-redis所有request爬取完毕,如何解决爬虫空跑问题? 》

输入图片说明

根据scrapy-redis分布式爬虫的原理,多台爬虫主机共享一个爬取队列。当爬取队列中存在request时,爬虫就会取出request进行爬取,如果爬取队列中不存在request时,爬虫就会处于等待状态.

可是,如果所有的request都已经爬取完毕了呢?这件事爬虫程序是不知道的,它无法区分结束和空窗期状态的不同,所以会一直处于上面的那种等待状态,也就是我们说的空跑。

那有没有办法让爬虫区分这种情况,自动结束呢?


  • 从背景介绍来看,基于scrapy-redis分布式爬虫的原理,爬虫结束是一个很模糊的概念,在爬虫爬取过程中,爬取队列是一个不断动态变化的过程,随着request的爬取,又会有新的request进入爬取队列。进进出出。
  • 爬取速度高于填充速度,就会有队列空窗期(爬取队列中,某一段时间会出现没有request的情况),爬取速度低于填充速度,就不会出现空窗期。所以对于爬虫结束这件事来说,只能模糊定义,没有一个精确的标准。

可以通过限定爬虫自动关闭时间来完成这个任务,在settings配置:

# 爬虫运行超过23.5小时,如果爬虫还没有结束,则自动关闭
CLOSESPIDER_TIMEOUT = 84600

特别注意 :如果爬虫在规定时限没有把request全部爬取完毕,此时强行停止的话,爬取队列中就还会存有部分request请求。那么爬虫下次开始爬取时,一定要记得在master端对爬取队列进行清空操作。


想象一下,爬虫已经结束的特征是什么?

那就是爬取队列已空,从爬取队列中无法取到request信息。那着手点应该就在从爬取队列中获取request和调度这个部分。查看scrapy-redis源码,我们发现了两个着手点,调度器site-packages\scrapy_redis\schedluer.py和site-packages\scrapy_redis\spiders.py爬虫。

但是爬虫在爬取过程中,队列随时都可能出现暂时的空窗期。想判断爬取队列为空,一般是设定一个时限,如果在一个时段内,队列一直持续为空,那我们可以基本认定这个爬虫已经结束了。

我选择更改调度器,site-packages\scrapy_redis\schedluer.py所以有了如下的改动:

首先在init里面设定一个初始次数

import datetime

    def __init__(self, server,
              ……
                ……

        """

        """ 为解决空跑问题:设定倒计次数 下方根据次数决定何时关闭爬虫,避免空跑"""
        self.lostGetRequest = 0

        if idle_before_close < 0:
            ……
                ……

完整的init方法代码为:

 def __init__(self, server,
                 persist=False,
                 flush_on_start=False,
                 queue_key=defaults.SCHEDULER_QUEUE_KEY,
                 queue_cls=defaults.SCHEDULER_QUEUE_CLASS,
                 dupefilter_key=defaults.SCHEDULER_DUPEFILTER_KEY,
                 dupefilter_cls=defaults.SCHEDULER_DUPEFILTER_CLASS,
                 idle_before_close=0,
                 serializer=None):
      

        """ 为解决空跑问题:设定倒计次数 下方根据次数决定何时关闭爬虫,避免空跑"""
        self.lostGetRequest = 0

        if idle_before_close < 0:
            raise TypeError("idle_before_close cannot be negative")

        self.server = server
        self.persist = persist
        self.flush_on_start = flush_on_start
        self.queue_key = queue_key
        self.queue_cls = queue_cls
        self.dupefilter_cls = dupefilter_cls
        self.dupefilter_key = dupefilter_key
        self.idle_before_close = idle_before_close
        self.serializer = serializer
        self.stats = None

然后到next_request方法中进行修改:

    def next_request(self):
        block_pop_timeout = self.idle_before_close
        request = self.queue.pop(block_pop_timeout)
        if request and self.stats:
            """ 解决空跑问题,这里判断如果获取到request则重置倒计时lostGetRequest """
            self.lostGetRequest = 0
            self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
        if request is None:
            """ 
            scrapy_reids跑完数据后不会自动停止,会产生空跑情况,一直空跑 
                每次调度Schedule时如果队列没有数据  则倒计时+1
                50次空跑大约费时5分钟,根据项目需求设定次数,满足空跑次数则主动停止并填写停止原因
            """
            self.lostGetRequest += 1
            if self.lostGetRequest > 10:
                self.spider.crawler.engine.close_spider(self.spider, 'Queue is empty,So active end')
        return request

这样就可以解决空跑的问题了。(事实证明,高兴得太早)


真正解决空跑(这个也不好,不建议。因为scrapy_redis已处理空跑问题(我也不确定))

真是太年轻,不懂事,我以为按照别人的想法实施,就可以解决空跑的问题了。然后当自己亲自测试的时候,发现并不是那么回事。

scrapy是异步的,而且request队列确实会有空闲状态,如果有空闲状态就会+1,用数字进行累加的话,虽然上编写了重置为0的操作,但貌似是不行的,测试没有那么细致,反正当空闲状态达到N次(关闭条件)的时候,就会自动关闭(request队列还在抽取,也会被关闭),那这就是个bug。

首先

思路是对的,然而用+1的方式出错了。我换了个思路,用时间差来决定是否关闭爬虫。逻辑:

  1. 时间差是不会存在累加的情况,所以不会有刚才的bug
  2. 先初始化一个起始时间
  3. 在每次请求队列的时候刷新起始时间
  4. 在每次队列为空的时候开始计时
  5. 计算时间差,如果队列为空的时间减去起始时间的秒数结果大于设定值,则判定为空跑,关闭爬虫

优点

  1. 通过时间差来判断空跑,解决了刚才的bug;
  2. 可以根据时间来关闭爬虫,而不是次数,这样对于日后爬虫的监控更精准

具体的代码如下:

现在init方法设定起始时间

        为解决空跑问题:设定起始时间 
        下方根据记录空跑时间end_times与起始时间的时间差来决定何时关闭爬虫,避免空跑
        """
        self.strat_times = datetime.datetime.now()

然后到next_request方法进行具体的时间差计算和空跑判断,还有爬虫的关闭操作:

    def next_request(self):
        block_pop_timeout = self.idle_before_close
        request = self.queue.pop(block_pop_timeout)
        if request and self.stats:
          """ 解决空跑问题,这里判断如果获取到request则重置起始时间strat_times """
            self.strat_times = datetime.datetime.now()
            self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
        if request is None:
           """ 
            scrapy_reids跑完数据后不会自动停止,会产生空跑情况,一直空跑 
                每次调度Schedule时如果队列没有数据  则计算end_times
                当end_times与start_times的时间差close_times超过N秒,就判定为空跑且进行关闭爬虫的操作
            """
            self.end_times = datetime.datetime.now()
            self.close_times = (self.end_times - self.strat_times).seconds
            print("tihs close_times is : ")
            print(self.close_times)
            if self.close_times > 180:
                self.spider.crawler.engine.close_spider(self.spider, 'Queue is empty,So active end')
        return request

看到下图,跑完数据后会根据时间差关闭爬虫

输入图片说明

这样才是真正的解决了空跑的问题


最后运行,可以正常关闭爬虫了。但是结束的时候还会有报错信息:

builtins.AttributeError: 'NoneType' object has no attribute 'start_requests'

2017-12-14 16:18:56 [twisted] CRITICAL: Unhandled Error
Traceback (most recent call last):
  File "E:\Miniconda\lib\site-packages\scrapy\commands\runspider.py", line 89, in run
    self.crawler_process.start()
  File "E:\Miniconda\lib\site-packages\scrapy\crawler.py", line 285, in start
    reactor.run(installSignalHandlers=False)  # blocking call
  File "E:\Miniconda\lib\site-packages\twisted\internet\base.py", line 1243, in run
    self.mainLoop()
  File "E:\Miniconda\lib\site-packages\twisted\internet\base.py", line 1252, in mainLoop
    self.runUntilCurrent()
--- <exception caught here> ---
  File "E:\Miniconda\lib\site-packages\twisted\internet\base.py", line 878, in runUntilCurrent
    call.func(*call.args, **call.kw)
  File "E:\Miniconda\lib\site-packages\scrapy\utils\reactor.py", line 41, in __call__
    return self._func(*self._a, **self._kw)
  File "E:\Miniconda\lib\site-packages\scrapy\core\engine.py", line 137, in _next_request
    if self.spider_is_idle(spider) and slot.close_if_idle:
  File "E:\Miniconda\lib\site-packages\scrapy\core\engine.py", line 189, in spider_is_idle
    if self.slot.start_requests is not None:
builtins.AttributeError: 'NoneType' object has no attribute 'start_requests'

当通过engine.close_spider(spider, ‘reason’)来关闭spider时,有时会出现几个错误之后才能关闭。可能是因为scrapy会开启多个线程同时抓取,然后其中一个线程关闭了spider,其他线程就找不到spider才会报错。


注意事项

编写代码的schedule.py有个next_request方法有这么一句代码:

        request = self.queue.pop(block_pop_timeout)

打开同目录的queue.py文件

输入图片说明

所以,PriorityQueue和另外两种队列FifoQueue,LifoQueue有所不同,特别需要注意。

如果会使用到timeout这个参数,那么在setting中就只能指定爬取队列为FifoQueue或LifoQueue

# 指定排序爬取地址时使用的队列,
# 默认的 按优先级排序(Scrapy默认),由sorted set实现的一种非FIFO、LIFO方式。
# 'SCHEDULER_QUEUE_CLASS': 'scrapy_redis.queue.SpiderPriorityQueue',
# 可选的 按先进先出排序(FIFO)
'SCHEDULER_QUEUE_CLASS': 'scrapy_redis.queue.SpiderQueue',
# 可选的 按后进先出排序(LIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderStack'

数据入库测试

经过多次 的mysql入库测试,发现bloomfilter是生效的,而且增量开始之前,对于那么重复的数据对比过滤是非常快的(仅用了500条数据测试),正常爬取500条数据大约1分钟多一点。在爬取过500多数据后,bloomfilter的略过只用了几秒钟,很短的时间。

这个还是很强的,我很高兴

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值