总结 - Python 对增量式爬虫的思考

前言

所谓增量式爬虫并不是新型的爬虫架构,而是根据项目需求而产生的一种爬虫类型。例如我们想爬取某某的职位信息,可是我们只想爬取每天更新的职位信息,不想全部都爬取,
这就需要增量式爬虫。增量式爬虫的核心在于快速去重,我们必须判断 哪些是已经爬取过的,哪些是新产生的。
 

去重方案

       去重一般的情况是对 URL 进行去重,也就说我们访问过的页面下次不再访问。但是也有一些情况,例如贴吧和论坛等社交网站,同一个 URL,由于用户评论的存在,页面内容是一直变化的,如果想抓取评论内容,那就不能以URL 为去重标准,但是本质上都可以看做是针对字符 串的去重方式。
       对于爬虫来说,由于网络间的链接错综复杂,爬虫在网络间爬行很可能会形成“ ,这对爬虫来说是非常可怕的事情,会一直做无用功。为了避免形成“ ,就需要知道 Spider 已经访问过哪些 URL ,基本上有如下几种方案:
      1 )关系型数据库去重。
      2 )缓存数据库去重。
      3 )内存去重。
 
关系型数据库去重 ,需要将 URL 存入到数据库中,每来一个 URL就启动一次数据库查询,但缺点是当数据量变得非常庞大后,关系型数据库查询的效率会变得很低,所以不推荐。
 
缓存数据库去重 ,比如现在比较流行的 Redis ,去重方式是使用其中的Set数据类型,类似于 Python 中的 Set ,也是一种内存去重方式,但是它可以将内存中的数据持久化到硬盘中,应用非常广泛,推荐。
 
内存去重 ,可以细分出三种不同的实现方式:
1、将URL直接存储到HashSet中,也就是Python中的Set数据结构中,但是这种方式最明显的缺点是太消耗内存。随着URL的增多,占用的内存会越来越多。大家可以计算一下假如存储了1亿个链接,每个链接平均40个字符,这就占用了4G内存
2、将URL经过MD5或者SHA-1等单向哈希算法生成摘要,再存储到HashSet中。由于字符串经过MD5处理后的信息摘要长度只有128位,SHA-1处理后也只有160位,所以占用的内存将比第一种方式小很多
倍。
3、采用 Bit-Map 方法,建立一个 BitSet ,将每个 URL 经过一个哈希函数映射到某一位。这种方式消耗内存是最少,但缺点是单一哈希函数发生冲突的概率太高,极易发生误判。
 
内存去重方案的这三种实现方式各有优缺点,但是对于整个内存去重方案来说,比较致命的是内存大小的制约和掉电易丢失的特性,万一服务器宕机了,所有内存数据将不复存在。
 

总结:

通过对以上去重方式的分析,我们可以确定相对比较好的方式是内存去重方案+缓存数据库,更准确地说是内存去重方案的第二种实现方式+缓存数据库,这种方式基本上可以满足大多数中型爬虫的需要。但当数据量上亿甚至几十亿时这种海量数据的去重方案,这就需要用到BloomFilter算法。
 
 

scrapy实现增量爬取的方法

1.通过开启缓存,将每个请求缓存至本地,下次爬取时,scrapy会优先从本地缓存中获得response,这种模式下,再次请求已爬取的网页不用从网络中获得响应,所以不受带宽影响,对服务器也不会造成额外的压力,但是无法获取网页变化的内容,速度也没有第二种方式快,而且缓存的文件会占用比较大的内存,在setting.py的以下注释用于设置缓存。

2.对item实现去重,通过item来封装

实现方法是在pipelines.py中进行设置,即在持久化数据之前判断数据是否已经存在

3.对url实现去重

速度快,对网站服务器的压力也比较小

scrapy可以自定义下载中间件

scrapy_reids实现增量式爬虫

在settings.py中添加如下代码

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"        # 指定了去重的类
SCHEDULER = "scrapy_redis.scheduler.Scheduler"                    # 指定了调度器的类
SCHEDULER_PERSIST = True                                          # 调度器的内容是否持久化
REDIS_URL = "redis://127.0.0.1:6379"                              # redis的url
ITEM_PIPELINES = {'scrapy_redis.pipelines.RedisPipeline': 400, }  # 如果数据需要保存到redis中,选配的

生成指纹 RFPDupeFilter.py

 def request_seen(self, request):
    """
    生成请求指纹,并判断请求在不在指纹集合中,
    如果返回True,表示已经放入请求队列中了
    返回False,表示该请求还未做过
    """
    fp = self.request_fingerprint(request) #生成指纹
    # 尝试将指纹放入指纹集合中,如果返回值为0,代表已经存在
    added = self.server.sadd(self.key, fp)
    return added == 0

def request_fingerprint(request):
    """
    对请求生成指纹,利用hashlib的sha1对象,对request的url、method、body进行哈希,会产生一 
    个40位16进制的字符串,作为request的指纹
    """
    fp = hashlib.sha1()
    fp.update(to_bytes(request.method))
    fp.update(to_bytes(canonicalize_url(request.url)))
    fp.update(request.body or b'')
    return fp.hexdigest()

 进入队列

def enqueue_request(self, request):
    if not request.dont_filter and self.df.request_seen(request):
        return False
    self.queue.push(request)
    return True
  1. 如果请求设置的过滤并且请求的指纹在指纹集合中存在的,不进入队列

  2. 其他情况会进入队列

  3. start_url中由于yield请求时,默认设置了dont_filter为True,不过滤,所以肯定会进入队列

为啥设置start_urls为不过滤?

start_url是起始页,其他请求需要靠start_url对应的响应才能保证抓取

一般正常去重的思路

错误:将所有访问过的URL和其对应的内容保存下来,然后过一段时间重新爬取一次并进行比较,然后决定是否需要覆盖。

原因:消耗很多资源

正确:给URL或者其内容(取决于这个网站采用哪种更新方式)上一个数据指纹标识,所以一般用set和redis

哈希值,根据哈希函数的特性,我们可以为任意内容生成一个独一无二的定长字符串,计算机只要经过简单的计算就可以得到唯一的特征值,这个计算过程的开销基本可以忽略不计

对数据做持久化就用redis ,Redis的集合就是Redis数据库中的集合类型,它具有无序不重复的特点。

 
 
 
 

猜你喜欢

转载自blog.csdn.net/weixin_42277380/article/details/112345955