温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Python自动化开发学习-分布式爬虫(scrapy-redis)

发布时间:2020-07-10 17:43:16 来源:网络 阅读:1400 作者:骑士救兵 栏目:编程语言

scrapy-redis

讲师的博客:https://www.cnblogs.com/wupeiqi/p/6912807.html
scrapy-redis是一个基于redis的scrapy组件,通过它可以快速实现简单分布式爬虫程序,该组件本质上提供了三大功能:

  • scheduler : 调度器
  • dupefilter : URL去重规则(被调度器使用)
  • pipeline : 数据持久化

准备工作

安装模块

pip install scrapy-redis

创建爬虫应用
项目就不重新创建了,直接在之前Scrapy课程的项目里,再创建一个新的应用:

> cd PeppaScrapy
> scrapy genspider [项目名称] [起始url]

通过环境变量指定配置文件
之前的课程上,已经对配置文件做了一些设置了。这里既不想把之前的内容覆盖掉,也不想受到之前配置的影响。
可以通过命令行的-s参数或者是在类里写一个名称为custom_settings的字典,一个一个参数的进行设置。这两个的优先级都很高。但是都不是配置文件的形式,这里可以单独再写一个配置文件,然后通过设置系统环境变量的方式来指定爬虫应用加载的配置文件。
在原来的settings.py的同级目录里创建一个mysettings.py的配置文件,文件可以在任意位置,只要能配python导入。然后设置一个系统环境变量即可:

import os
os.environ.setdefault('SCRAPY_SETTINGS_MODULE', 'PeppaScrapy.mysettings')

连接redis设置

把设置都写在配置文件中即可:

# redis 配置文件
REDIS_HOST = 'localhost'  # 主机名
REDIS_PORT = 6379  # 端口
# REDIS_URL = 'redis://user:pass@hostname:9001'  # 连接URL,和上面2条一样,也是连接redis的设置,优先使用这个
# REDIS_PARAMS  = {}  # Redis连接参数,这里有一些默认值,可以去源码里看
# REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'  # 指定连接Redis的Python模块,默认:redis.StrictRedis
# REDIS_ENCODING = "utf-8"  # redis编码类型,默认:'utf-8'

连接参数
这里提供两种方式连接,一种是前2个设置,指定HOST和PORT。如果还需要用户名和密码就没办法了。
另一种就是用一个REDIS_URL参数,按照上面的格式把所有的信息填好。另外这里REDIS_URL参数的优先级高,就是说如果设置了REDIS_URL参数,那么上面的2个参数就没有效果了。这个逻辑可以在scrapy_redis.connection.py里的get_redis函数里找到。

其他连接参数
REDIS_PARAMS,是连接redis时使用的其他参数。所以其实用户名,密码也是可以写在这里面的。即使什么都不写,scrapy-redis模块本身也设置了一些默认值,可以在scrapy_redis.defaults.py里找到:

REDIS_PARAMS = {
    'socket_timeout': 30,
    'socket_connect_timeout': 30,
    'retry_on_timeout': True,
    'encoding': REDIS_ENCODING,  // 这个值默认是'utf-8',也在这个文件里
}

具体还可以设置哪些参数,就是看连接Redis的那个类的构造函数了,源码里就是用这个字典直接**kwargs创建对象了。默认的用来连接Redis的模块是redis.StrictRedis,下面是这个类的构造函数的参数列表:

    def __init__(self, host='localhost', port=6379,
                 db=0, password=None, socket_timeout=None,
                 socket_connect_timeout=None,
                 socket_keepalive=None, socket_keepalive_options=None,
                 connection_pool=None, unix_socket_path=None,
                 encoding='utf-8', encoding_errors='strict',
                 charset=None, errors=None,
                 decode_responses=False, retry_on_timeout=False,
                 ssl=False, ssl_keyfile=None, ssl_certfile=None,
                 ssl_cert_reqs='required', ssl_ca_certs=None,
                 max_connections=None):

REDIS_ENCODING,是指定编码类型,默认utf-8没太多要说的。
用于连接redis的所有参数,除了以下4个是单独写的,其他的都写在REDIS_PARAMS这个字典里。按照上面构造函数里的变量名称写。这4个可以单独写的,其实也就是做了一步映射而已:

# 这个也是 scrapy_redis.connection.py 里的源码
SETTINGS_PARAMS_MAP = {
    'REDIS_URL': 'url',
    'REDIS_HOST': 'host',
    'REDIS_PORT': 'port',
    'REDIS_ENCODING': 'encoding',
}

REDIS_PARAMS['redis_cls'],指定连接Redis的Python模块。按上面说的,处理那4个参数,其他的都只能写在REDIS_PARAMS这个字典里。
源码文件
上面这些主要是翻了3个文件里的源码:

  • scrapy_redis.defaults.py
  • scrapy_redis.connection.py
  • redis.client.py

URL去重

模块提供了一个使用redis做去重的规则,只需要按之前在Scrapy课程里学的,设置自定的去重规则即可。具体就是加上一条配置:

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

url去重算法
这里的去重规则,会先把url转换成唯一标识,然后再存到集合里。
源码里会把url通过hashlib进行哈希运算,用的是SHA1·。把原本的字符串转成数字签名。这么做的好处就是,即使url会很长,但是生成的数字签名的长度是固定的。这里可以注意下这个技巧,把url存储在redis里的时候,占用的长度是固定的,关键是不会太长。
另外这个转换过程还有个特点。如果url里是带get参数的,如果参数的值是一样的,但是出现的顺序不同,转换后生成的唯一标识也是一样的。比如像下面这样:

url1 = 'https://www.baidu.com/s?wd=sha1&ie=utf-8'
url2 = 'https://www.baidu.com/s?ie=utf-8&wd=sha1'

像这种,只是参数的先后顺序不同,两个请求应该还是同一个请求,应该要去重。这里在这种情况下生成的唯一标识是一样的,所以可以做到去重。

这个去重规则实现了去重的算法,是要被调度器使用的。在去重的这个类里,通过request_seen这个方法来判断是否有重复。下面的调度器里就会调用这个方法来判断避免重复爬取。

调度器

在配置文件里,加上下面的配置来指定调度器,这个是scrapy读取的配置:

SCHEDULER = "scrapy_redis.scheduler.Scheduler"

接下来出一些在scrapy-redis模块里要读取的配置。其中部分在scrapy_redis.defaults.py里有默认设置,还有的在代码里也有默认的逻辑,所以配置文件里可以什么都不写,需要改变设置的话也可以自己在配置文件里指定。

使用的队列

SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

指定调度器存取请求使用的队列类型,有3个可选的:

  • PriorityQueue,默认。通过redis的有序集合来实现
  • FifoQueue,队列。通过redis的List实现的,使用lpush和rpop进行存取
  • LifoQueue,栈。也是通过redis的List实现的,使用lpush和lpop进行存取

请求存放在redis中的key

SCHEDULER_QUEUE_KEY = '%(spider)s:requests'

这样,不同的爬虫应用在redis中可以使用不同的key存取请求。

对保存到redis中的数据进行序列化

SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"  

这个设置在defaults里没有,如果没有设置,就用 scrapy_redis.picklecompat 这个。而底层用的还是pickle模块。这样可以把对象序列化之后在redis中保存起来。

清空调度器和去重记录
下面的2个参数都是布尔型的,如果不设置就是False,如果需要开启,就设置为True:

SCHEDULER_PERSIST = True  # 是否在关闭时候保留,调度器和去重记录
SCHEDULER_FLUSH_ON_START = True  # 是否在开始之前清空,调度器和去重记录

上面两个参数的效果都是判断后决定是否要执行调度器的一个flush方法。而这个flush方法里执行的则是清空调度器和去重记录。
注意这两个参数的效果。默认都是False,就是在开始之前不清空调度器和去重记录,在结束的时候也不保留调度器和去重记录。测试的话一般不需要保留。如果是上线使用,一般需要保留调度器和去重记录,那么就按下面来设置:

# 保留调度器和去重记录的设置
SCHEDULER_PERSIST = True
SCHEDULER_FLUSH_ON_START = False

获取请求时等待的时间
调度器获取请求时,如果数据为空,进行等待的时间,默认为0,单位秒:

SCHEDULER_IDLE_BEFORE_CLOSE = 0 

这个实际就是redis的blpop和brpop操作时的timeout参数,默认为0。如果为0,就使用lpop和rpop这2个不阻塞的pop方法。如果大于0,就使用阻塞的pop方法。这里不会用到timeout为0的阻塞pop,所以不会一直阻塞,总会返回的。无论阻塞还是不阻塞,取不到值就返回None。
另外,调度器默认用的是有序集合,redis的有序集合取值没有阻塞也没有timeout,所以这个值是无效的。

去重规则的参数
下面2个是去重规则使用的参数,其中一个在去重的时候讲过了。另一个就是在redis里使用的key:

SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'  # 去重规则在redis中保存时对应的key
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'  # 去重规则对应处理的类

数据持久化

模块还提供了数据持久化,在scrapy的配置里指定好数据持久化使用的类:

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

# 下面是两个持久化时可以定制的配置
# PIPELINE_KEY = '%(spider)s:items'
# REDIS_ITEMS_SERIALIZER = "scrapy.utils.serialize.ScrapyJSONEncoder"

先去看看这个类里具体做了些什么:

from twisted.internet.threads import deferToThread

class RedisPipeline(object):

    def process_item(self, item, spider):
        return deferToThread(self._process_item, item, spider)

    def _process_item(self, item, spider):
        key = self.item_key(item, spider)
        data = self.serialize(item)
        self.server.rpush(key, data)
        return item

先看process_item方法,返回一个deferToThread,在twisted里的相当于拿一个线程来做一个事。具体twisted干了啥就忽略把,主要是处理_process_item这个方法。所以真正做的处理是在_process_item方法里。
这里先把item的数据通过self.serialize函数做序列化,然后就是下面的rpush存到redis里去了。这里有2个可以定制的参数,一个是序列化的方法,一个是存储的redis里使用的Key。
默认使用下面的Key,当然可以在配置文件里指定:

PIPELINE_KEY = '%(spider)s:items'

默认使用的序列化的方法是一个做了一些定制的json方法,在下面这里:

from scrapy.utils.serialize import ScrapyJSONEncoder

也是可以通过参数来指定自己的序列化方法的:

REDIS_ITEMS_SERIALIZER = "scrapy.utils.serialize.ScrapyJSONEncoder"

爬虫和起始url

之前在spiders文件夹下写爬虫的时候,都是继承scrapy.Spider然后写自己的类。
模块也提供了一个它自己的类,可以直接继承模块提供的这个类来写爬虫:

from scrapy_redis.spiders import RedisSpider

class TestSpider(RedisSpider):
    name = 'jandan'
    allowed_domains = ['jandan.net']

用了模块的爬虫后,起始url也可以直接从redis里获取了,相关的配置有下面2个:

START_URLS_KEY = '%(name)s:start_urls'
START_URLS_AS_SET = False

第一个设置,是在redis里用来存储起始url的key,只有通过这个key才能从redis里获取到起始的url。
第二个设置,是指定在redis里使用是否使用集合来存储起始url,默认不用集合,用的就是列表。
使用模块提供的这个爬虫,会一直运行,永远不会停止。没有任务的话应该就是一直等着直到获取到新的任务。可以往redis对应的起始url的key里添加数据,就会自动的开始爬虫。适合在线上使用。
而scrapy模块提供的scrapy.Spider这个爬虫适合平时自己用,爬完就结束了。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI