Scrapy-Redis原理和源码解析

Scrapy-Redis原理和源码解析

一两个月没有发博客了。。。虽然没有发博客,但是一直在努力学习哇。暑假的时候会把一些爬虫的知识点和js逆向的知识点上传。

一级目录

二级目录

三级目录

1.scrapy-reids的原理和源码解析:

Scrapy-Redis库已经为我们实现了Scrapy分布式的队列、调度器、去重等功能,github地址:https://github.com/rmax/scrapy-redis

1.爬取队列:

queue.py文件中有三个队列的实现,分别是:FifoQueue, PriorityQueue, LifoQueue。但是它们都继承了一个父类Base,代码如下:

1.Base:
class Base(object):
    def __init__(self, server, spider, key, serializer=None):
        if serializer is None:
            # Backward compatibility.
            # TODO: deprecate pickle.
            serializer = picklecompat
        if not hasattr(serializer, 'loads'):
            raise TypeError(f"serializer does not implement 'loads' function: {serializer}")
        if not hasattr(serializer, 'dumps'):
            raise TypeError(f"serializer does not implement 'dumps' function: {serializer}")

        self.server = server
        self.spider = spider
        self.key = key % {'spider': spider.name}
        self.serializer = serializer

    def _encode_request(self, request):
        """Encode a request object"""
        try:
            obj = request.to_dict(spider=self.spider)
        except AttributeError:
            obj = request_to_dict(request, self.spider)
        return self.serializer.dumps(obj)

    def _decode_request(self, encoded_request):
        """Decode an request previously encoded"""
        obj = self.serializer.loads(encoded_request)
        return request_from_dict(obj, spider=self.spider)

    def __len__(self):
        """Return the length of the queue"""
        raise NotImplementedError

    def push(self, request):
        """Push a request"""
        raise NotImplementedError

    def pop(self, timeout=0):
        """Pop a request"""
        raise NotImplementedError

    def clear(self):
        """Clear queue/stack"""
        self.server.delete(self.key)

值得注意的是 _encode_request_ decode_request 方法,因为通过 scrapy-redis 的原理可知,我们需要把一个request对象push到数据库中,但是数据库没有办法直接存储对象啊,怎么办?这里 _encode_request 就发挥作用了,它先是把一个 request 对象转成 dict 对象赋值给 obj,然后self.serializer.dumps(obj)obj序列化为一个字符串,用于redis存储。同样,如果我们需要从数据库中pop出数据库,我们会调用 _decode_request 方法来反序列化,把一个存储在redis的字符串变成request对象。

细心的同学会发现父类中的 len push pop方法都是没有实现的,直接调用会抛出NotImplementedError的错误,因此这个类是不能直接被使用的,需要我们实现子类来对父类方法进行重写。

2.FifoQueue:
class FifoQueue(Base):
    """Per-spider FIFO queue"""

    def __len__(self):
        """Return the length of the queue"""
        return self.server.llen(self.key)

    def push(self, request):
        """Push a request"""
        self.server.lpush(self.key, self._encode_request(request))

    def pop(self, timeout=0):
        """Pop a request"""
        if timeout > 0:
            data = self.server.brpop(self.key, timeout)
            if isinstance(data, tuple):
                data = data[1]
        else:
            data = self.server.rpop(self.key)
        if data:
            return self._decode_request(data)

这里我们发现FifoQueue方法重写了 len push pop方法,这三个方法都是对server对象的重写,这里的server对象其实就是一个redis的连接对象。在这里我们看到对redis的操作有:llen,lpush 和 rpop 等方法,这表明爬取的队列使用的是redis的列表。序列化的request存入列表后,成为列表的一个元素。 __len__方法是获取列表的长度,push方法是从列表左侧插入数据,pop方法是从列表右侧获取数据。

因此request 在 FifoQueue中的存取顺序是先进先出(First input first out)。

3.LifoQueue:

LifoQueue这个类与FifoQueue恰好相反,存取方式类似于栈(Last input first out)。

class LifoQueue(Base):
    """Per-spider LIFO queue."""

    def __len__(self):
        """Return the length of the stack"""
        return self.server.llen(self.key)

    def push(self, request):
        """Push a request"""
        self.server.lpush(self.key, self._encode_request(request))
4. PriorityQueue:

顾名思义就是优先级队列,代码如下:

class PriorityQueue(Base):
    """Per-spider priority queue abstraction using redis' sorted set"""

    def __len__(self):
        """Return the length of the queue"""
        return self.server.zcard(self.key)

    def push(self, request):
        """Push a request"""
        data = self._encode_request(request)
        score = -request.priority
        # We don't use zadd method as the order of arguments change depending on
        # whether the class is Redis or StrictRedis, and the option of using
        # kwargs only accepts strings, not bytes.
        self.server.execute_command('ZADD', self.key, score, data)

    def pop(self, timeout=0):
        """
        Pop a request
        timeout not support in this queue class
        """
        # use atomic range/remove using multi/exec
        pipe = self.server.pipeline()
        pipe.multi()
        pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
        results, count = pipe.execute()
        if results:
            return self._decode_request(results[0])

PriorityQueue队列是默认使用的队列,也就是爬取队列的时候默认使用有序集合来存储。

在这里我们看到__len__,pushpop方法中使用了server对象的zcard,zadd,zrange操作。zcard是来统计有序集合的大小,即爬取队列的长度。zadd操作就是向集合里添加元素,学过redis的有序集合就能知道,在这个集合中,每个元素需要设定一个分数作为优先级,分数越小,优先级越高。这里 score = -request.priority这句话是把分数指定成Request优先级的相反数,所以高优先级的Request会排在集合最前面。pop方法首先调用zrange方法取出集合中第一个元素,并zremrangebyrank将这个元素删除。完成了取出并且删除的操作。

2.去重过滤:

class RFPDupeFilter(BaseDupeFilter):
    logger = logger

    def __init__(self, server, key, debug=False):
        self.server = server
        self.key = key
        self.debug = debug
        self.logdupes = True

    @classmethod
    def from_settings(cls, settings):
        server = get_redis_from_settings(settings)
        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):
        return cls.from_settings(crawler.settings)

    def request_seen(self, request):
        fp = self.request_fingerprint(request)
        added = self.server.sadd(self.key, fp)
        return added == 0

    def request_fingerprint(self, request):
        fingerprint_data = {
            "method": to_unicode(request.method),
            "url": canonicalize_url(request.url),
            "body": (request.body or b"").hex(),
        }
        fingerprint_json = json.dumps(fingerprint_data, sort_keys=True)
        return hashlib.sha1(fingerprint_json.encode()).hexdigest()
    
    @classmethod
    def from_spider(cls, spider):
        settings = spider.settings
        server = get_redis_from_settings(settings)
        dupefilter_key = settings.get("SCHEDULER_DUPEFILTER_KEY", defaults.SCHEDULER_DUPEFILTER_KEY)
        key = dupefilter_key % {'spider': spider.name}
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(server, key=key, debug=debug)

    def close(self, reason=''):
        self.clear()

    def clear(self):
        self.server.delete(self.key)

    def log(self, request, spider):
        if self.debug:
            msg = "Filtered duplicate request: %(request)s"
            self.logger.debug(msg, {'request': request}, extra={'spider': spider})
        elif self.logdupes:
            msg = ("Filtered duplicate request %(request)s"
                   " - no more duplicates will be shown"
                   " (see DUPEFILTER_DEBUG to show all duplicates)")
            self.logger.debug(msg, {'request': request}, extra={'spider': spider})
            self.logdupes = False

这个类的重点方法是request_seenrequest_fingerprint方法,这两个方法和scrapy内置的去重中间件极为类似(scrapy.dupefilters.RFPDupeFilter)。

request_fingerprint:这个函数首先创建一个字典fingerprint_data,然后,这个字典被转化为JSON字符串,并被编码为字节字符串。该字节字符串被用作输入数据计算SHA1哈希。结果是一个40个字符长度的十六进制字符串,代表了原始请求的唯一"fingerprint"(或散列)。

request_seen:这个函数检查一个请求是否在服务器上已经被"看过"。首先,它计算请求的"fingerprint"。然后,这个"fingerprint"尝试被添加到Redis服务器的一个set中(此处键为self.key)。Redis的SADD命令会添加元素到set,如果set添加成功,add值便会是1,表明这个指纹不存在于原先的set中,那么代码最后判断结果就是False,换句话说就是不重复,否则判断为重复。

3.调度器:

Scrapy-Redis帮助我们实现了调度器Scheduler来配合Queue和DupeFilter。我们可以在scrapy的settings文件中指定一些配置,如SCHEDULER_FLUSH_ON_START 即是否在爬取开始的时候清空爬取队列,默认是False,一般不配置,在分布式中使用重爬机制会导致数据混乱。如SCHEDULER_PERSIST 表示是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空。

class Scheduler(object):
    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):

        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

    def __len__(self):
        return len(self.queue)

    @classmethod
    def from_settings(cls, settings):
        kwargs = {
            'persist': settings.getbool('SCHEDULER_PERSIST'),
            'flush_on_start': settings.getbool('SCHEDULER_FLUSH_ON_START'),
            'idle_before_close': settings.getint('SCHEDULER_IDLE_BEFORE_CLOSE'),
        }

        # If these values are missing, it means we want to use the defaults.
        optional = {
            # TODO: Use custom prefixes for this settings to note that are
            # specific to scrapy-redis.
            'queue_key': 'SCHEDULER_QUEUE_KEY',
            'queue_cls': 'SCHEDULER_QUEUE_CLASS',
            'dupefilter_key': 'SCHEDULER_DUPEFILTER_KEY',
            # We use the default setting name to keep compatibility.
            'dupefilter_cls': 'DUPEFILTER_CLASS',
            'serializer': 'SCHEDULER_SERIALIZER',
        }
        for name, setting_name in optional.items():
            val = settings.get(setting_name)
            if val:
                kwargs[name] = val

        # Support serializer as a path to a module.
        if isinstance(kwargs.get('serializer'), six.string_types):
            kwargs['serializer'] = importlib.import_module(kwargs['serializer'])

        server = connection.from_settings(settings)
        # Ensure the connection is working.
        server.ping()

        return cls(server=server, **kwargs)

    @classmethod
    def from_crawler(cls, crawler):
        instance = cls.from_settings(crawler.settings)
        # FIXME: for now, stats are only supported from this constructor
        instance.stats = crawler.stats
        return instance

    def open(self, spider):
        self.spider = spider

        try:
            self.queue = load_object(self.queue_cls)(
                server=self.server,
                spider=spider,
                key=self.queue_key % {'spider': spider.name},
                serializer=self.serializer,
            )
        except TypeError as e:
            raise ValueError(f"Failed to instantiate queue class '{self.queue_cls}': {e}")

        self.df = load_object(self.dupefilter_cls).from_spider(spider)

        if self.flush_on_start:
            self.flush()
        # notice if there are requests already in the queue to resume the crawl
        if len(self.queue):
            spider.log(f"Resuming crawl ({len(self.queue)} requests scheduled)")

    def close(self, reason):
        if not self.persist:
            self.flush()

    def flush(self):
        self.df.clear()
        self.queue.clear()

    def enqueue_request(self, request):
        if not request.dont_filter and self.df.request_seen(request):
            self.df.log(request, self.spider)
            return False
        if self.stats:
            self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
        self.queue.push(request)
        return True

    def next_request(self):
        block_pop_timeout = self.idle_before_close
        request = self.queue.pop(block_pop_timeout)
        if request and self.stats:
            self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
        return request

    def has_pending_requests(self):
        return len(self) > 0

Scheduler有两个核心的方法enqueue_requestnext_request。两个方法的核心操作分别是调用Queue的push和pop操作。对于pop操作,如果队列中还有Request,则Request会直接被取出来,爬取继续;如果队列为空,则爬取会重新开始。

4.总结:

1.爬取队列的实现:提供了三种队列,使用redis的列表或者有序集合来维护。

2.去重的实现:使用redis的集合来保存request指纹,以实现重复过滤。

3.中断后重新爬取的实现:中断后的redis队列并没有清空,再次启动时调度器的next_request会从队列中取到下一个request,继续爬取。

Redis Queue(队列):在Scrapy-Redis中,请求是储存在Redis的队列中的。每当Spider产生新的请求时,它们被添加到队列里。每当需要新的请求去下载时,就会从队列里取出。在分布式环境中,多个Spider实例可以共享这个队列。

Redis Dupefilter(去重过滤器):在每次将请求加入队列之前,Scrapy-Redis会先通过Dupefilter来检查这个请求是否存在重复。这是为了避免重复执行相同的请求,节约资源。在Scrapy-Redis中,请求的去重信息储存在Redis中,这使得多个Spider实例可以共享这个去重信息。

Redis Scheduler(调度器):Scheduler是Scrapy-Redis中的核心组件。它从队列中获取请求并交给Downloader去下载,然后把新产生的请求加入队列,同时还会使用Dupefilter来过滤掉重复的请求。Scheduler是Scrapy-Redis实现分布式爬虫的关键,因为它协调了Spider、Downloader、Queue和Dupefilter之间的工作。

  • 31
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值