zoukankan      html  css  js  c++  java
  • 数据采集: scrapy-redis源码分析(一)

    一般我们使用python来写爬虫会优先选择scrapy框架, 框架本身基于异步网络请求性能比较高, 另外对并发控制, 延迟请求支持的比较好, 可以使我们专注于爬虫的逻辑. 但是scrapy仅仅支持单机的爬虫, 如果要支持分布式的话还需要借助scrapy-redis来实现. 接下来我们主要关注scrapy-redis的实现方式, 了解实现原理使用起来会更加顺手.

    scrapy-redis的目录结构如下, 各个模块功能见注释

    ├── __init__.py
    ├── connection.py   # 负责redis的连接
    ├── defaults.py    # 一些默认参数配置
    ├── dupefilter.py    # 用于请求队列的去重, 继承了scrapy本身的去重器
    ├── picklecompat.py    # 使用pickle进行序列化
    ├── pipelines.py    # 会把item丢进redis中去
    ├── queue.py    # 调度队列, 调度器会使用该队列
    ├── scheduler.py    # 调度器, 负责任务的调度工作
    ├── spiders.py    # spider基类, 加入了信号等
    └── utils.py
    

    queue.py

    支持三种队列, 都继承自Base

    1. FIFO Queue

    使用了redis的list结构

    class FifoQueue(Base):
        def __len__(self):
            """返回队列长度大小"""
            return self.server.llen(self.key)
    
        def push(self, request):
            """发送请求到队列左边"""
            self.server.lpush(self.key, self._encode_request(request))
    
        def pop(self, timeout=0):
            """从队列右边抛出请求"""
            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)
    

    2. PriorityQueue

    使用了redis的有序集合结构

    class PriorityQueue(Base):
    
        def __len__(self):
            """返回队列内长度大小"""
            return self.server.zcard(self.key)
    
        def push(self, request):
            """放入请求到zset中"""
            data = self._encode_request(request)
            score = -request.priority
            self.server.execute_command('ZADD', self.key, score, data)
    
        def pop(self, timeout=0):
            """从zset中抛出请求. 此处不支持timeout参数"""
            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])
    

    使用redis的sorted set实现, 如果在spider脚本中需要指定priority的话, 一定要在settings中来声明使用的是PriorityQueue.

    3. LIFO Queue

    后入先出, 使用list结构实现

    
    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))
    
        def pop(self, timeout=0):
            """Pop a request"""
            if timeout > 0:
                data = self.server.blpop(self.key, timeout)
                if isinstance(data, tuple):
                    data = data[1]
            else:
                data = self.server.lpop(self.key)
    
            if data:
                return self._decode_request(data)
    

    和先进先出队列基本一样, 实现了栈结构

    dupefilter.py

    scrapy默认使用了集合结构来进行去重, 在scrapy-redis中使用redis的集合(set)进行了替换, 请求指纹的计算方法还是用的内置的.

    def request_seen(self, request):
        """获取请求指纹并添加到redis的去重集合中去"""
        fp = self.request_fingerprint(request)    # 得到请求的指纹
        added = self.server.sadd(self.key, fp)    # 把指纹添加到redis的集合中
        return added == 0
    
    def request_fingerprint(self, request):
        return request_fingerprint(request)    # 得到请求指纹
    

    去重指纹计算使用的是sha1算法, 计算值包括请求方法, url, body等信息

    scheduler.py

    替换了scrapy原生的scheduler, 所有方法名称和原生scheduler保持一致, 在爬虫开启后会连接待抓取队列和去重集合, 然后就是不断把新的请求去重后放入待抓取队列, 然后从待抓取队列拿出请求给下载器

        def open(self, spider):
          """爬虫启动时触发, 主要是连接待抓取和去重模块"""
          pass
          
        def enqueue_request(self, request):
            """
            把请求去重后放入 待抓取队列中
            Parameters
            ----------
            request
    
            Returns
            -------
    
            """
            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):
            """
            从请求队列拿出下一个请求并返回
            Returns
            -------
    
            """
            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
    

    调度器肯定要和请求队列去重队列进行交互, 所以初始化要获取使用的queuedupfilter的类, 并在open方法中完成实例化

    open方法

    
        def open(self, spider):
            self.spider = spider
    
            try:
                # 得到队列queue的实例化对象
                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("Failed to instantiate queue class '%s': %s",
                                 self.queue_cls, e)
    
            try:
                # 得到去重的实例化对象
                self.df = load_object(self.dupefilter_cls)(
                    server=self.server,
                    key=self.dupefilter_key % {'spider': spider.name},
                    debug=spider.settings.getbool('DUPEFILTER_DEBUG'),
                )
            except TypeError as e:
                raise ValueError("Failed to instantiate dupefilter class '%s': %s",
                                 self.dupefilter_cls, e)
    
            if self.flush_on_start:    # 如果为True, 要在爬虫开启前删除对应爬虫request队列和dupfilter队列
                self.flush()
            # notice if there are requests already in the queue to resume the crawl
            if len(self.queue):
                spider.log("Resuming crawl (%d requests scheduled)" % len(self.queue))
    

    spider.py

    spider空闲的时候会从start_urls队列中读取url, 默认一次读取CONCURRENT_REQUESTS个url, 可以在settings中设置REDIS_START_URLS_BATCH_SIZE来改变每次的读取数量, 一般我会在使用的时候增大这个值, 可以降低spide进入idle的次数, 从而适当提升抓取性能

        def setup_redis(self, crawler=None):
            """初始化了redis参数, 包括使用的种子url的key, 批量读取url的数量等信息"""
            ......
            # 当spider空闲的时候会触发该信号, 调用spider_idle函数
            crawler.signals.connect(self.spider_idle, signal=signals.spider_idle)
          
        def spider_idle(self):
          """空闲的时候触发该函数, 尝试请求下一批url. 有url的时候会直接请求, 最后都会抛出异常, 防止spider被关闭, 然后等待新的url过来"""
          self.schedule_next_requests()
          raise DontCloseSpider
    
        
    

    以上是scrapy-redis的基本分析, 可以发现源码中大量使用了基础的数据结构和算法的知识, 不太熟悉的同学可以参看之前的文章, 我会在接下来的时间继续分享两篇使用技巧和一些实用的特性.

    文章会首发在公众号, 欢迎大家关注及时查看.

  • 相关阅读:
    Counting Sort and Radix Sort
    Naïve Bayes
    windows c/c++ 代码运行时间,毫秒级
    数学基础:四、树的应用1(利用树结构存储字典表)(待优化)
    数学基础:三、动态规划2(求解凑齐钱的最小张数)
    数学基础:三、动态规划1(求解编辑距离)
    数学基础:二、组合算法(递归)
    数学基础:一、排列算法(递归)
    idea注释模版设置
    幂等性的实现
  • 原文地址:https://www.cnblogs.com/zlone/p/11079481.html
Copyright © 2011-2022 走看看