爬虫

聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎

2018-07-07  本文已影响69人  江湖十年

分布式爬虫要点

image.png

爬虫 A、B、C 分别放在三台服务器上,还需要一个 “状态管理器” 来对 URL 进行集中管理、去重等操作,它可以单独部署在一个服务器上面,也可以部署在 A、B、C 任何一台服务器上面

image.png

回顾一下 Scrapy 架构图

Scrapy 架构图

在一台机器上运行的爬虫中,SPIDER yield 出来的任何一条 REQUEST 都需要进入 SCHEDULER,然后 ENGINE 从 SCHEDULER 取 REQUEST 发送给 DOWNLOADER;还有关于 Scrapy 的去重,是放在内存中的一个 set 里面的。

如果想将 Scrapy 改造成分布式,就会有两个问题必须要解决
①request 队列集中管理
② 去重集中管理

image.png

如何解决:
①在 Scrapy 中,SCHEDULER 实际上是放在一个 QUEUE(队列)中的,而这个 QUEUE 实际上就是放在内存中的,如果是分布式爬虫,其他服务器是拿不到当前服务器内存中的内容的,所以 Scrapy 就没法支持分布式,要想办法将这地方的 QUEUE 管理做成一种集中式的管理
②去重也是要做集中管理的,Scrapy 当中有一个去重的扩展,去重原理就是通过内存中的 set 来实现的,所以也没法做成分布式

所以关注的重点就是,将 “去重的 set” 和 “REQUEST 队列 SCHEDULER” 这两个放到第三方的组件来做,这个第三方组件就是分布式爬虫的主角 Redis

Redis 是基于内存的数据库,事实上也可以用 关系型数据库来做分布式,但是效率会很低

redis基础知识

Redis 菜鸟教程:http://www.runoob.com/redis/redis-tutorial.html
Redis 中文文档:http://www.redis.cn/commands.html

scrapy-redis编写分布式爬虫代码

GitHub 地址:https://github.com/rmax/scrapy-redis

需要安装库

pip install redis

Scrapy-Redis 的使用

settings.py 中需要配置


# 调度器(必须)
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 去重(必须)
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 用于将 ITEM 序列化并发送到 Redis(可选)
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 300
}

所有的编写的 Spider 不再继承 scrapy.Spider,而要继承 RedisSpider


from scrapy_redis.spiders import RedisSpider

class MySpider(RedisSpider):
    name = 'myspider'

    def parse(self, response):
        # do stuff
        pass

启动 Spider 命令不再是 scrapy crawl myspider,而是改成 scrapy runspider myspider.py ,注意要写 文件名 而不再是 爬虫名

scrapy runspider myspider.py

现在启动爬虫后,所有的 REQUEST 就不再是本地 SCHEDULER 来完成的,而是在 settings.py 中新增的 Scrapy-Redis 的 SCHEDULER SCHEDULER = "scrapy_redis.scheduler.Scheduler"

启动 Spider 以后,必须要向队列里面放入一个初始化 URL,现在所有 URL 都是放在 Redis 当中的,所以初始化的时候,必须要向 Redis 中 push 一个起始 URL,这样才能够进行爬取

redis-cli lpush myspider:start_urls http://google.com

新建 Scrapy 项目 ScrapyRedisTest

 scrapy startproject ScrapyRedisTest
image.png

将 Scrapy-Redis 源码 clone 下来

git clone https://github.com/rmax/scrapy-redis.git
image.png

将 clone 下来的 Scrapy-Redis 源码 复制到 新建的 ScrapyRedisTest 项目中

image.png

将 jobbole spider 改造成 基于 Scrapy-Redis 的分布式爬虫

image.png image.png image.png image.png image.png image.png

关于 RedisMixin 类中获取下一个 URL 的方法 next_requests

Scrapy 中获取 next_requests 的时候是通过 Scrapy 的 SCHEDULER 来完成的,SCHEDULER 是维持一个 QUEUE,是放在内存当中的;现在 Scrapy-Redis 将这个 QUEUE 放到了 Redis 当中,使用 list 或 set 等来完成

可以将原来的 jobbole spider 中 parse 方法逻辑完全拷贝过来

# ScrapyRedisTest/spiders/jobbole.py

from urllib.parse import urljoin

import scrapy
from scrapy_redis.spiders import RedisSpider


class JobboleSpider(RedisSpider):
    name = 'jobbole'
    allowed_domains = ['jobbole.com']
    redis_key = 'jobbole:start_urls'

    def parse(self, response):
        """
            1. 提取文章列表页中所有文章详情页链接,并交给 parse_detail 方法进行解析
            2. 提取下一页链接,并交给 Scrapy 进行下载
        Args:
            response: 响应信息
        Yields:
            1. 文章详情页链接,交给 parse_detail 解析
            2. 下一页链接,交给 Scrapy 下载
        """
        post_nodes = response.xpath('//div[@id="archive"]')
        for post_node in post_nodes:
            post_url = post_node.xpath('.//div[@class="post-meta"]//a[@class="archive-title"]/@href').extract_first('')
            front_img_url = post_node.xpath('.//div[@class="post-thumb"]//img/@src').extract_first('')
            yield scrapy.Request(url=urljoin(response.url, post_url), callback=self.parse_detail,
                                 meta={'front_img_url': front_img_url})
        next_url = response.xpath('//a[@class="next page-numbers"]/@href').extract_first()
        if next_url:
            yield scrapy.Request(url=next_url, callback=self.parse)

    def parse_detail(self, response):
        pass

因为这里只做演示,所以删除了很多逻辑,只有一个简单的 parse 方法

接下来就是 settings.py 中的配置

# ScrapyRedisTest/settings.py

ROBOTSTXT_OBEY = False

# Scrapy-Redis 相关配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 300
}

编写启动爬虫主文件 main.py

image.png

运行爬虫

image.png

向 Redis 里面 push 一个起始 URL

image.png

发现 spider 开始进行爬取

image.png

为了方便查看 Scrapy-Redis 爬虫的流程,可以在 jobbole.py 和 Scrapy-Redis 源码 scheduler.py 中分别打一个断点,进行调试检测

image.png image.png

DeBug 运行 spider,第一次停到断点这里实际上没有起始的 URL,此时向 Redis 中 push 第一个 URL,然后 f9 跳过这个请求,程序又会卡在这个断点

image.png image.png

此时查看 Redis,新增了一个 jobbole:requests 键,值为 ZSET 类型,为什么使用可排序的 ZSET 而不是 SET 或者 LIST 呢?因为默认情况下 jobbole:requests 是有优先级的,我们在程序中是可以设定优先级的,为了满足这个功能,必须使用可排序的 ZSET

实际上,存到 Redis 里面的这个 request 是一个 Python 类,将这个类放到了 Redis 中,但是 Redis 中是不可能放 Python 类的,实际上这里存入 Redis 中是经过 Pickle 序列化的,将类序列化成字符串,存入 Redis,后续拿出来使用的时候可以反序列化成类

image.png

多按几次 f9,再次查看 Redis,多了一个 jobbole:dupefilter 键,这个键的值类型为 SET,因为是用于去重的,所以就没必要使用 ZSET

image.png image.png

scrapy-redis源码剖析-dupefilter.py、picklecompat.py

image.png

dupefilter.py 是用来去重的文件,Scrapy-Redis 的 dupefilter.py 源码几乎同 Scrapy 的 dupefilter.py 源码一模一样

image.png image.png image.png

Scrapy-Redis 在初始化的时候就生成了一个 server,这个 server 就连接到了 Redis,调用了 get_redis_from_settings 方法,这个方法是在 connection.py 中的

image.png image.png image.png image.png

picklecompat.py 实际上就是调用了 Python 的 pickle 库

image.png

scrapy-redis源码剖析- pipelines.py、 queue.py

image.png

pipelines.py 文件中会将 ITEM 放到 Redis 当中

image.png

pipelines.py 文件中 RedisPipeline 类的初始化方法同样会初始化一个 server,这个 server 同样会调用 connection.py 中的 get_redis_from_settings 方法

image.png image.png image.png image.png

事实上,将数据存入 Redis 的过程的类 RedisPipeline 不是必须要配置到 settings.py 中的,实际上也可以在爬虫爬取完成之后直接存储到本地数据库中,设置保存到 Redis 当中的好处是,可以完成数据共享,将数据放到 Redis 当中就可以多起几个进程,甚至还可以写一个脚本(不是爬虫),直接从 Redis 中读取数据并将其存储到关系型数据库当中

queue.py 是供给 schedluer 调度来使用的,共有 3 个 Queue

image.png

FifoQueue 意思是:first in first out 的简写,先进先出,也就是有序队列

image.png

LifoQueue 意思是:last in first out 的简写,后进先出,类似于

image.png

PriorityQueue:默认使用的就是这个 Queue

image.png image.png
上一篇 下一篇

猜你喜欢

热点阅读