Scrapy-redis之终结篇

本文详细介绍了Scrapy-Redis的使用,包括为何选择Scrapy-Redis、安装步骤、执行流程,以及如何实现分布式爬虫。通过注册自定义pipeline和设置,可将爬取数据存入Redis。Scrapy-Redis提供了请求去重、爬虫持久化和分布式功能,通过连接Redis数据库实现高效爬取。在Spider中,利用scrapy-redis的scheduler和pipeline组件,实现了请求调度和Item的分布式处理。
摘要由CSDN通过智能技术生成

scrapy-redis

为什么要用scrapy-redis?

Scrapy_redis在scrapy的基础上实现了更多,更强大的功能,具体体现在:reqeust去重,爬虫持久化,和轻松实现分布式,实现一个任务多台服务器执行,大大的提高了效率

安装
pip3 install scrapy-redis
scrapy-redis 执行流程

在这里插入图片描述

源码

scrapy-redis .connrction
connect 文件引入了redis 模块,这个是 redis-python库的接口,用于通过python访问redis数据库,可见,这个文件主要是实现连接redis数据库的功能(返回的是redis库的Redis对象或者StrictRedis对象,这俩都是可以直接用来进行数据操作的对象)。这些连接接口在其他文件中经常被用到。其中,我们可以看到,要想连接到redis数据库,和其他数据库差不多,需要一个ip地址、端口号、用户名密码(可选)和一个整形的数据库编号,同时我们还可以在scrapy工程的setting文件中配置套接字的超时时间、等待时间等。

其实这个模块的功能:

 1. 从 settings 里面获取 redis 的链接配置
 2. 获取 redis 的 链接 实例
import six
from scrapy.utils.misc import load_object
from . import defaults
#Shortcut maps 'setting name' -> 'parmater name'.
#redis://127.0.0.1:6379/0
SETTINGS_PARAMS_MAP = {
   
    'REDIS_URL': 'url',  #以url的方式链接数据库
    'REDIS_HOST': 'host', #指定redis数据库的host
    'REDIS_PORT': 'port', #指定redis数据库的port
    'REDIS_ENCODING': 'encoding', #指定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 :scrapy设置文件
        A scrapy settings object. See the supported settings below.

    Returns
    -------
    server:redis客户端链接对象
        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 = defaults.REDIS_PARAMS.copy()
    #从scrapy的设置文件中获取REDIS_PARAMS,覆盖defaults.REDIS_PARAMS的默认值
    params.update(settings.getdict('REDIS_PARAMS'))
    # XXX: Deprecate REDIS_* settings.
    #
    # SETTINGS_PARAMS_MAP = {
   
    #     'REDIS_URL': 'url',  # 以url的方式链接数据库
    #     'REDIS_HOST': 'host',  # 指定redis数据库的host
    #     'REDIS_PORT': 'port',  # 指定redis数据库的port
    #     'REDIS_ENCODING': 'encoding',  # 指定redis数据库的编码
    # }
    for source, dest in SETTINGS_PARAMS_MAP.items():
        val = settings.get(source)
        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'])

    ##调用get_redis方法返回redis数据库链接
    return get_redis(**params)


 #Backwards compatible alias.
from_settings = get_redis_from_settings


def get_redis(**kwargs):
    # 方法返回redis数据库链接对象
    """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.StrictRedis
    redis_cls = kwargs.pop('redis_cls', defaults.REDIS_CLS)
    url = kwargs.pop('url', None)
    if url:
        #根据url方式创建redis数据库链接,并返回
        return redis_cls.from_url(url, **kwargs)
    else:
        #根据host、port创建redis数据库链接,并返回
        return redis_cls(**kwargs)

scrapy-redis .defaults
redis 的一些基础的默认的设置。其实就是一些默认配置:

import redis

# redis.StrictRedis()

# For standalone use.

DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'
#redis数据库中保留item的key
PIPELINE_KEY = '%(spider)s:items'

#REDIS_CLS: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,
}

#redis数据库中保存待爬取任务(request)的key
SCHEDULER_QUEUE_KEY = '%(spider)s:requests'
#默认指定了有优先级的任务队列存储方式(PriorityQueue)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
#redis数据库中保留去重指纹的key
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'
#默认设置了scrapy_redis的去重组件
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
#默认起始任务的key
START_URLS_KEY = '%(name)s:start_urls'
START_URLS_AS_SET = False

scrapy-redis .dupefilter
这个主要是用来去重的。RFPDupeFilter继承自 Scrapy 的BaseDupeFilter,实现了 request 去重功能,基于 Scrapy 的 request_fingerprint 生成指纹,并在 Redis 上存储。当收到新的 request,首先生成指纹判断是否存在于已爬取的指纹库内(Redis set),若存在则返回 False,不存在返回 True.总得来说是这样的,这个文件首先获取到redis的server,然后从scrapy的request中获取request的指纹,将这个指纹进行存到redis的去重库中。达到去重的目的。

这个文件看起来比较复杂,重写了scrapy本身已经实现的 request 判重功能。因为本身 scrapy 单机跑的话,只需要读取内存中的request 队列 或者 持久化的 request 队列(scrapy默认的持久化似乎是json格式的文件,不是数据库)就能判断这次要发出的request url是否已经请求过或者正在调度(本地读就行了)。而 分布式跑的话,就需要各个主机上的scheduler都连接同一个数据库的同一个 request池 来判断这次的请求是否是重复的了。

在这个文件中,通过继承 BaseDupeFilter 重写他的方法,实现了基于redis的判重。根据源代码来看,scrapy-redis 使用了scrapy本身的一个 fingerprint 接口 request_fingerprint,这个接口很有趣,根据scrapy文档所说,他通过hash来判断两个url是否相同(相同的url会生成相同的hash结果),但是当两个url的地址相同,get型参数相同但是顺序不同时,也会生成相同的hash结果(这个真的比较神奇。。。)所以 scrapy-redis 依旧使用 url 的 fingerprint 来判断 request 请求是否已经出现过。这个类通过连接 redis,使用一个key来向redis的一个set中插入fingerprint(这个key对于同一种spider是相同的,redis 是一个key-value的数据库,如果key是相同的,访问到的值就是相同的,这里使用 spider名字+DupeFilter 的 key 就是为了在不同主机上的不同爬虫实例,只要属于同一种 spider,就会访问到同一个set,而这个 set 就是他们的url判重池 ),如果返回值为0,说明该set中该fingerprint 已经存在(因为集合是没有重复值的),则返回 False,如果返回值为 1,说明添加了一个fingerprint到set中,则说明这个 request 没有重复,于是返回True,还顺便把新fingerprint加入到数据库中了。

DupeFilter 判重会在 scheduler 类中用到,每一个 request 在进入调度之前都要进行判重,如果重复就不需要参加调度,直接舍弃就好了,不然就是白白浪费资源。

import logging
import time

from scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import request_fingerprint

from . import defaults
from .connection import get_redis_from_settings


logger = logging.getLogger(__name__)


#TODO: Rename class to RedisDupeFilter.
class RFPDupeFilter(BaseDupeFilter):
    """Redis-based request duplicates filter.

    This class can also be used with default Scrapy's scheduler.

    """

    logger = logger

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

        Parameters
        ----------
        server : redis.StrictRedis
        #redis客户端链接
            The redis server instance.
        key : str
            #redis数据库中保留去重指纹的key
            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

    @classmethod
    def from_settings(cls, settings):
        """Returns an instance from given settings.

        This uses by default the key ``dupefilter:<timestamp>``. When using the
        ``scrapy_redis.scheduler.Scheduler`` class, this method is not used as
        it needs to pass the spider name in the key.

        Parameters
        ----------
        settings : scrapy.settings.Settings
        Returns
        -------
        RFPDupeFilter
            A RFPDupeFilter instance.
        """
        #
        server = get_redis_from_settings(settings)
        # XXX: This creates one-time key. needed to support to use this
        # class as standalone dupefilter with scrapy's default scheduler
        # if scrapy passes spider on open() method this wouldn't be needed
        # TODO: Use SCRAPY_JOB env as default and fallback to timestamp.
        key = defaults.DUPEFILTER_KEY % {
   'timestamp': int(time.time())}
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(server, key=key, debug=debug)

    @classmethod
    def from_crawler(cls, crawler):
        """Returns instance from crawler.

        Parameters
        ----------
        crawler : scrapy.crawler.Crawler

        Returns
        -------
        RFPDupeFilter
            Instance of RFPDupeFilter.

        """
        return cls.from_settings(crawler.settings)

    def request_seen(self, request):
        """Returns True if request was already seen.

        Parameters
        ----------
        request : scrapy.http.Request  #请求对象

        Returns
        -------
        bool :True:表示已添加到任务队列, False:表示未添加到任务队列

        """
        #根据request对象生成指纹
        fp = self.request_fingerprint(request)
        # This returns the number of values added, zero if already exists.
        # 返回0:已存在集合中    返回1:指纹不存在集合中
        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)

    def close(self, reason=''):
        """Delete data on close. Called by Scrapy's scheduler.

        Parameters
        ----------
        reason : str, optional

        """
        self.clear()

    def clear(self)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值