分布式爬虫Scrapy-redis框架源码解析


scrapy-redis的官方文档写的比较简洁,没有提及其运行原理,所以如果想全面的理解分布式爬虫的运行原理,还是得看scrapy-redis的源代码才行。

官方站点:https://github.com/rolando/scrapy-redis

scrapy-redis工程的主体还是是redis和scrapy两个库,工程本身实现的东西不是很多,这个工程就像胶水一样,把这两个插件粘结了起来。下面我们来看看,scrapy-redis的每一个源代码文件都实现了什么功能,最后如何实现分布式的爬虫系统。

一、scrapy-redis架构原理

在这里插入图片描述

Scrapy-redis提供了下面四种组件(components):

四种组件意味着这四个模块都要做相应的修改

  • Scheduler(调度器)
  • Duplication Filter(重复过滤)
  • Item Pipeline(管道)
  • Base Spider(继承类)

下面分别介绍四个组件:

1. Scheduler(调度器):

Scrapy改造了python本来的collection.deque(双向队列)形成了自己的Scrapy queue(https://github.com/scrapy/queuelib/blob/master/queuelib/queue.py)),但是Scrapy多个spider不能共享待爬取队列Scrapy queue, 即Scrapy本身不支持爬虫分布式,scrapy-redis 的解决是把这个Scrapy queue换成redis数据库(也是指redis队列),从同一个redis-server存放要爬取的request,便能让多个spider去同一个数据库里读取。

Scrapy中跟“待爬队列”直接相关的就是调度器Scheduler,它负责对新的request进行入列操作(加入Scrapy queue),取出下一个要爬取的request(从Scrapy queue中取出)等操作。它把待爬队列按照优先级建立了一个字典结构,比如:

 {
        优先级0 : 队列0
        优先级1 : 队列1
        优先级2 : 队列2
    }

然后根据request中的优先级,来决定该入哪个队列,出列时则按优先级较小的优先出列。为了管理这个比较高级的队列字典,Scheduler需要提供一系列的方法。但是原来的Scheduler已经无法使用,所以使用Scrapy-redis的scheduler组件。

2. Duplication Filter(过滤工具):

Scrapy中用集合实现这个request去重功能,Scrapy中把已经发送的request指纹放入到一个集合中,把下一个request的指纹拿到集合中比对,如果该指纹存在于集合中,说明这个request发送过了,如果没有则继续操作。这个核心的判重功能是这样实现的:

def request_seen(self, request):
        # self.request_figerprints 就是一个指纹集合  
        fp = self.request_fingerprint(request)
        # 这就是判重的核心操作  
        if fp in self.fingerprints:
            return True
        self.fingerprints.add(fp)
        if self.file:
            self.file.write(fp + os.linesep)

在scrapy-redis中去重是由Duplication Filter组件来实现的,它通过redis的set 不重复的特性,巧妙的实现了Duplication Filter去重。scrapy-redis调度器从引擎接受request,将request的指纹存redis的set检查是否重复,并将不重复的request push写redis的 request queue。

引擎请求request(Spider发出的)时,调度器从redis的request queue队列里根据优先级pop 出个request 返回给引擎,引擎将此request发给spider处理。

3. Item Pipeline(管道):

引擎将(Spider返回的)爬取到的Item给Item Pipeline,scrapy-redis 的Item Pipeline将爬取到的 Item 存redis的 items queue。

修改过Item Pipeline可以很方便的根据 key 从 items queue 提取item,从而实现items processes集群。

4. Base Spider:

不在使用scrapy原有的Spider类,重写的RedisSpider继承了Spider和RedisMixin这两个类,RedisMixin是用来从redis读取url的类。

当我们生成一个Spider继承RedisSpider时,调用setup_redis函数,这个函数会去连接redis数据库,然后会设置signals(信号):

  • 一个是当spider空闲时候的signal,会调用spider_idle函数,这个函数调用schedule_next_request函数,保证spider是一直活着的状态,并且抛出DontCloseSpider异常。
  • 一个是当抓到一个item时的signal,会调用item_scraped函数,这个函数会调用schedule_next_request函数,获取下一个request。

Scrapy-redis框架执行过程总结:

  1. 最后总结一下scrapy-redis的总体思路:这套组件通过重写scheduler和 spider类,实现了调度、spider启动和redis的交互。

  2. 实现新的dupefilter和queue类,达到了判重和调度容器和redis 的交互,因为每个主机上的爬虫进程都访问同一个redis数据库,所以调度和判重都统一进行统一管理,达到了分布式爬虫的目的。

  3. 当spider被初始化时,同时会初始化一个对应的scheduler对象,这个调度器对象通过读取settings,配置好自己的调度容器queue和判重工具dupefilter。

  4. 每当一个spider产出一个request的时候,scrapy引擎会把这个reuqest递交给这个spider对应的scheduler对象进行调度,scheduler对象通过访问redis对request进行判重,如果不重复就把他添加进redis中的调度器队列里。当调度条件满足时,scheduler对象就从redis的调度器队列中取出一个request发送给spider,让他爬取。

  5. 当spider爬取的所有暂时可用url之后,scheduler发现这个spider对应的redis的调度器队列空了,于是触发信号spider_idle,spider收到这个信号之后,直接连接redis读取start_urls池,拿取新的一批url入口,然后再次重复上边的工作。

二、源码解析

下面的源码的注释基本都有,我就重要的代码进行解释

1.connection.py

这个文件是用于连接redis的文件,用到比较多,也是最重要的文件

import six

from scrapy.utils.misc import load_object

from . import defaults


# Shortcut maps 'setting name' -> 'parmater name'.
#关系
SETTINGS_PARAMS_MAP = {
    'REDIS_URL': 'url',
    'REDIS_HOST': 'host',
    'REDIS_PORT': 'port',
    'REDIS_ENCODING': 'encoding',
}

#获取一个redis连接实例
#生成连接redis的参数
def get_redis_from_settings(settings):
    """Returns a redis client instance from given Scrapy settings object.

    This function uses ``get_client`` to instantiate the client and uses
    ``defaults.REDIS_PARAMS`` global as defaults values for the parameters. You
    can override them using the ``REDIS_PARAMS`` setting.

    Parameters
    ----------
    settings : Settings
        A scrapy settings object. See the supported settings below.

    Returns
    -------
    server
        Redis client instance.

    Other Parameters
    ----------------
    REDIS_URL : str, optional
        Server connection URL.
    REDIS_HOST : str, optional
        Server host.
    REDIS_PORT : str, optional
        Server port.
    REDIS_ENCODING : str, optional
        Data encoding.
    REDIS_PARAMS : dict, optional
        Additional client parameters.
    """
    #浅拷贝 是为了防止params的改变,会导致默认SETTINGS_PARAMS被改变
    params = defaults.REDIS_PARAMS.copy()
    #将设置中的参数更新进入params中
    params.update(settings.getdict('REDIS_PARAMS'))
    # XXX: Deprecate REDIS_* settings.
    #遍历映射表,获取指定的参数
    for source, dest in SETTINGS_PARAMS_MAP.items():
        #优先使用设置中的参数
        val = settings.get(source)
        #如果设置中没有进行设置,则params不更新
        if val:
            params[dest] = val

    # Allow ``redis_cls`` to be a path to a class.
    if isinstance(params.get('redis_cls'), six.string_types):
        params['redis_cls'] = load_object(params['redis_cls'])

    return get_redis(**params)


# Backwards compatible alias.
from_settings = get_redis_from_settings


def get_redis(**kwargs):
    """Returns a redis client instance.

    Parameters
    ----------
    redis_cls : class, optional
        Defaults to ``redis.StrictRedis``.
    url : str, optional
        If given, ``redis_cls.from_url`` is used to instantiate the class.
    **kwargs
        Extra parameters to be passed to the ``redis_cls`` class.

    Returns
    -------
    server
        Redis client instance.

    """
    #没有redis_cls 则用默认的redis连接
    redis_cls = kwargs.pop('redis_cls', defaults.REDIS_CLS)
    #判断kwarg有没有url
    url = kwargs.pop('url', None)
    if url:
        return redis_cls.from_url(url, **kwargs)
    else:
        #走这里
        return redis_cls(**kwargs)

Connection提供了一个很重要的函数,from_settings = get_redis_from_settings这个函数引入defualt.py文件,定义了我们访问过的指纹。pipline,queue,schedule文件都会调用。

2.defaults.py

主要存放默认的参数

import redis


# For standalone use.
#去重的键名key
DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'

#定义的存储items的键名,spiders是爬虫的名称
PIPELINE_KEY = '%(spider)s:items'
#redis连接对象,是用于连接redis
REDIS_CLS = redis.StrictRedis
#字符集编码
REDIS_ENCODING = 'utf-8'
# Sane connection defaults.
# redis的连接的参数
REDIS_PARAMS = {
    'socket_timeout': 30,
    'socket_connect_timeout': 30,
    'retry_on_timeout': True,
    'encoding': REDIS_ENCODING,
}
# 队列的变量名,用于存储爬取的url队列
SCHEDULER_QUEUE_KEY = '%(spider)s:requests'
# 优先级队列,用于规定队列的进出方式
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# 用于去重的key,给request加指纹存储的地方
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter&#
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值