scrapy与redis实战

Posted qichueng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了scrapy与redis实战相关的知识,希望对你有一定的参考价值。

从零搭建Redis-Scrapy分布式爬虫

Scrapy-Redis分布式策略:

假设有四台电脑:Windows 10、Mac OS X、Ubuntu 16.04、CentOS 7.2,任意一台电脑都可以作为 Master端 或 Slaver端,比如:

  • Master端(核心服务器) :使用 Windows 10,搭建一个Redis数据库,不负责爬取,只负责url指纹判重、Request的分配,以及数据的存储

  • Slaver端(爬虫程序执行端) :使用 Mac OS X 、Ubuntu 16.04、CentOS 7.2,负责执行爬虫程序,运行过程中提交新的Request给Master

  1. 首先Slaver端从Master端拿任务(Request、url)进行数据抓取,Slaver抓取数据的同时,产生新任务的Request便提交给 Master 处理;

  2. Master端只有一个Redis数据库,负责将未处理的Request去重和任务分配,将处理后的Request加入待爬队列,并且存储爬取的数据。

Scrapy-Redis默认使用的就是这种策略,我们实现起来很简单,因为任务调度等工作Scrapy-Redis都已经帮我们做好了,我们只需要继承RedisSpider、指定redis_key就行了。

缺点是,Scrapy-Redis调度的任务是Request对象,里面信息量比较大(不仅包含url,还有callback函数、headers等信息),可能导致的结果就是会降低爬虫速度、而且会占用Redis大量的存储空间,所以如果要保证效率,那么就需要一定硬件水平

一、安装Redis

安装Redis:http://redis.io/download

安装完成后,拷贝一份Redis安装目录下的redis.conf到任意目录,建议保存到:/etc/redis/redis.conf(Windows系统可以无需变动)

二、修改配置文件 redis.conf

打开你的redis.conf配置文件,示例:

  • 非Windows系统: sudo vi /etc/redis/redis.conf

  • Windows系统:C:\\Intel\\Redis\\conf\\redis.conf

  1. Master端redis.conf里注释bind 127.0.0.1,Slave端才能远程连接到Master端的Redis数据库。

  2.daemonize yno表示Redis默认不作为守护进程运行,即在运行redis-server /etc/redis/redis.conf时,将显示Redis启动提示画面;

    • daemonize yes则默认后台运行,不必重新启动新的终端窗口执行其他命令,看个人喜好和实际需要。

三、测试Slave端远程连接Master端

测试中,Master端Windows 10 的IP地址为:192.168.199.108

  1. Master端按指定配置文件启动 redis-server,示例:

    • 非Windows系统:sudo redis-server /etc/redis/redis.conf

    • Windows系统:命令提示符(管理员)模式下执行 redis-server C:\\Intel\\Redis\\conf\\redis.conf读取默认配置即可。

  2. Master端启动本地redis-cli

         3.slave端启动redis-cli -h 192.168.199.108-h 参数表示连接到指定主机的redis数据库

注意:Slave端无需启动redis-server,Master端启动即可。只要 Slave 端读取到了 Master 端的 Redis 数据库,则表示能够连接成功,可以实施分布式。

四、Redis数据库桌面管理工具

这里推荐 Redis Desktop Manager,支持 Windows、Mac OS X、Linux 等平台:

下载地址:https://redisdesktop.com/download 

源码自带项目说明:

使用scrapy-redis的example来修改

先从github上拿到scrapy-redis的示例,然后将里面的example-project目录移到指定的地址:

# clone github scrapy-redis源码文件
git clone https://github.com/rolando/scrapy-redis.git

# 直接拿官方的项目范例,改名为自己的项目用(针对懒癌患者)
mv scrapy-redis/example-project ~/scrapyredis-project

我们clone到的 scrapy-redis 源码中有自带一个example-project项目,这个项目包含3个spider,分别是dmoz, myspider_redis,mycrawler_redis

一、dmoz (class DmozSpider(CrawlSpider))

这个爬虫继承的是CrawlSpider,它是用来说明Redis的持续性,当我们第一次运行dmoz爬虫,然后Ctrl + C停掉之后,再运行dmoz爬虫,之前的爬取记录是保留在Redis里的。

分析起来,其实这就是一个 scrapy-redis 版 CrawlSpider 类,需要设置Rule规则,以及callback不能写parse()方法。

执行方式:scrapy crawl dmoz

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class DmozSpider(CrawlSpider):
    """Follow categories and extract links."""
    name = \'dmoz\'
    allowed_domains = [\'dmoz.org\']
    start_urls = [\'http://www.dmoz.org/\']

    rules = [
        Rule(LinkExtractor(
            restrict_css=(\'.top-cat\', \'.sub-cat\', \'.cat-item\')
        ), callback=\'parse_directory\', follow=True),
    ]

    def parse_directory(self, response):
        for div in response.css(\'.title-and-desc\'):
            yield {
                \'name\': div.css(\'.site-title::text\').extract_first(),
                \'description\': div.css(\'.site-descr::text\').extract_first().strip(),
                \'link\': div.css(\'a::attr(href)\').extract_first(),
            } 

二、myspider_redis (class MySpider(RedisSpider))

这个爬虫继承了RedisSpider, 它能够支持分布式的抓取,采用的是basic spider,需要写parse函数。

其次就是不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。

from scrapy_redis.spiders import RedisSpider

class MySpider(RedisSpider):
    """Spider that reads urls from redis queue (myspider:start_urls)."""
    name = \'myspider_redis\'

    # 注意redis-key的格式:
    redis_key = \'myspider:start_urls\'

    # 可选:等效于allowd_domains(),__init__方法按规定格式写,使用时只需要修改super()里的类名参数即可
    def __init__(self, *args, **kwargs):
        # Dynamically define the allowed domains list.
        domain = kwargs.pop(\'domain\', \'\')
        self.allowed_domains = filter(None, domain.split(\',\'))

        # 修改这里的类名为当前类名
        super(MySpider, self).__init__(*args, **kwargs)

    def parse(self, response):
        return {
            \'name\': response.css(\'title::text\').extract_first(),
            \'url\': response.url,
        }

注意:

RedisSpider类 不需要写allowd_domainsstart_urls

  1. scrapy-redis将从在构造方法__init__()里动态定义爬虫爬取域范围,也可以选择直接写allowd_domains

  2. 必须指定redis_key,即启动爬虫的命令,参考格式:redis_key = \'myspider:start_urls\'

  3. 根据指定的格式,start_urls将在 Master端的 redis-cli 里 lpush 到 Redis数据库里,RedisSpider 将在数据库里获取start_urls。

执行方式:

  1. 通过runspider方法执行爬虫的py文件(也可以分次执行多条),爬虫(们)将处于等待准备状态:

    scrapy runspider myspider_redis.py

  2. 在Master端的redis-cli输入push指令,参考格式:

    $redis > lpush myspider:start_urls http://www.dmoz.org/

  3. Slaver端爬虫获取到请求,开始爬取。

三、mycrawler_redis (class MyCrawler(RedisCrawlSpider))

这个RedisCrawlSpider类爬虫继承了RedisCrawlSpider,能够支持分布式的抓取。因为采用的是crawlSpider,所以需要遵守Rule规则,以及callback不能写parse()方法。

同样也不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。

from scrapy.spiders import Rule
from scrapy.linkextractors import LinkExtractor

from scrapy_redis.spiders import RedisCrawlSpider

class MyCrawler(RedisCrawlSpider):
    """Spider that reads urls from redis queue (myspider:start_urls)."""
    name = \'mycrawler_redis\'
    redis_key = \'mycrawler:start_urls\'

    rules = (
        # follow all links
        Rule(LinkExtractor(), callback=\'parse_page\', follow=True),
    )

    # __init__方法必须按规定写,使用时只需要修改super()里的类名参数即可
    def __init__(self, *args, **kwargs):
        # Dynamically define the allowed domains list.
        domain = kwargs.pop(\'domain\', \'\')
        self.allowed_domains = filter(None, domain.split(\',\'))

        # 修改这里的类名为当前类名
        super(MyCrawler, self).__init__(*args, **kwargs)

    def parse_page(self, response):
        return {
            \'name\': response.css(\'title::text\').extract_first(),
            \'url\': response.url,
        } 

注意:

同样的,RedisCrawlSpider类不需要写allowd_domainsstart_urls

  1. scrapy-redis将从在构造方法__init__()里动态定义爬虫爬取域范围,也可以选择直接写allowd_domains

  2. 必须指定redis_key,即启动爬虫的命令,参考格式:redis_key = \'myspider:start_urls\'

  3. 根据指定的格式,start_urls将在 Master端的 redis-cli 里 lpush 到 Redis数据库里,RedisSpider 将在数据库里获取start_urls。

执行方式:

  1. 通过runspider方法执行爬虫的py文件(也可以分次执行多条),爬虫(们)将处于等待准备状态:

    scrapy runspider mycrawler_redis.py

  2. 在Master端的redis-cli输入push指令,参考格式:

    $redis > lpush mycrawler:start_urls http://www.dmoz.org/

  3. 爬虫获取url,开始执行。

总结:

  1. 如果只是用到Redis的去重和保存功能,就选第一种;

  2. 如果要写分布式,则根据情况,选择第二种、第三种

  3. 通常情况下,会选择用第三种方式编写深度聚焦爬虫

有缘网分布式爬虫案例:

# clone github scrapy-redis源码文件
git clone https://github.com/rolando/scrapy-redis.git

# 直接拿官方的项目范例,改名为自己的项目用(针对懒癌患者)
mv scrapy-redis/example-project ~/scrapy-youyuan 

修改settings.py

下面列举了修改后的配置文件中与scrapy-redis有关的部分,middleware、proxy等内容在此就省略了。

# -*- coding: utf-8 -*-

# 指定使用scrapy-redis的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 指定使用scrapy-redis的去重
DUPEFILTER_CLASS = \'scrapy_redis.dupefilters.RFPDupeFilter\'

# 指定排序爬取地址时使用的队列,
# 默认的 按优先级排序(Scrapy默认),由sorted set实现的一种非FIFO、LIFO方式。
SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderPriorityQueue\'
# 可选的 按先进先出排序(FIFO)
# SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderQueue\'
# 可选的 按后进先出排序(LIFO)
# SCHEDULER_QUEUE_CLASS = \'scrapy_redis.queue.SpiderStack\'

# 在redis中保持scrapy-redis用到的各个队列,从而允许暂停和暂停后恢复,也就是不清理redis queues
SCHEDULER_PERSIST = True

# 只在使用SpiderQueue或者SpiderStack是有效的参数,指定爬虫关闭的最大间隔时间
# SCHEDULER_IDLE_BEFORE_CLOSE = 10

# 通过配置RedisPipeline将item写入key为 spider.name : items 的redis的list中,供后面的分布式处理item
# 这个已经由 scrapy-redis 实现,不需要我们写代码
ITEM_PIPELINES = {
    \'example.pipelines.ExamplePipeline\': 300,
    \'scrapy_redis.pipelines.RedisPipeline\': 400
}

# 指定redis数据库的连接参数
# REDIS_PASS是我自己加上的redis连接密码(默认不做)
REDIS_HOST = \'127.0.0.1\'
REDIS_PORT = 6379
#REDIS_PASS = \'redisP@ssw0rd\'

# LOG等级
LOG_LEVEL = \'DEBUG\'

#默认情况下,RFPDupeFilter只记录第一个重复请求。将DUPEFILTER_DEBUG设置为True会记录所有重复的请求。
DUPEFILTER_DEBUG =True

# 覆盖默认请求头,可以自己编写Downloader Middlewares设置代理和UserAgent
DEFAULT_REQUEST_HEADERS = {
    \'Accept\': \'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\',
    \'Accept-Language\': \'zh-CN,zh;q=0.8\',
    \'Connection\': \'keep-alive\',
    \'Accept-Encoding\': \'gzip, deflate, sdch\'
}

查看pipeline.py

# -*- coding: utf-8 -*-

from datetime import datetime

class ExamplePipeline(object):
    def process_item(self, item, spider):
        #utcnow() 是获取UTC时间
        item["crawled"] = datetime.utcnow()
        # 爬虫名
        item["spider"] = spider.name
        return item

修改items.py

增加我们最后要保存的youyuanItem项,这里只写出来一个非常简单的版本

# -*- coding: utf-8 -*-

from scrapy.item import Item, Field

class youyuanItem(Item):
    # 个人头像链接
    header_url = Field()
    # 用户名
    username = Field()
    # 内心独白
    monologue = Field()
    # 相册图片链接
    pic_urls = Field()
    # 年龄
    age = Field()

    # 网站来源 youyuan
    source = Field()
    # 个人主页源url
    source_url = Field()

    # 获取UTC时间
    crawled = Field()
    # 爬虫名
    spider = Field()

编写 spiders/youyuan.py

在spiders目录下增加youyuan.py文件编写我们的爬虫,之后就可以运行爬虫了。 这里的提供一个简单的版本:

# -*- coding:utf-8 -*-

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
# 使用redis去重
from scrapy.dupefilters import RFPDupeFilter

from example.items import youyuanItem
import re

#
class YouyuanSpider(CrawlSpider):
    name = \'youyuan\'
    allowed_domains = [\'youyuan.com\']
    # 有缘网的列表页
    start_urls = [\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/\']

    # 搜索页面匹配规则,根据response提取链接
    list_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/find/.+\'))

    # 北京、18~25岁、女性 的 搜索页面匹配规则,根据response提取链接
    page_lx = LinkExtractor(allow =(r\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\\d+/\'))

    # 个人主页 匹配规则,根据response提取链接
    profile_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/\\d+-profile/\'))

    rules = (
        # 匹配find页面,跟进链接,跳板
        Rule(list_page_lx, follow=True),

        # 匹配列表页成功,跟进链接,跳板
        Rule(page_lx, follow=True),

        # 匹配个人主页的链接,形成request保存到redis中等待调度,一旦有响应则调用parse_profile_page()回调函数处理,不做继续跟进
        Rule(profile_page_lx, callback=\'parse_profile_page\', follow=False),
    )

    # 处理个人主页信息,得到我们要的数据
    def parse_profile_page(self, response):
        item = youyuanItem()
        item[\'header_url\'] = self.get_header_url(response)
        item[\'username\'] = self.get_username(response)
        item[\'monologue\'] = self.get_monologue(response)
        item[\'pic_urls\'] = self.get_pic_urls(response)
        item[\'age\'] = self.get_age(response)
        item[\'source\'] = \'youyuan\'
        item[\'source_url\'] = response.url

        #print "Processed profile %s" % response.url
        yield item


    # 提取头像地址
    def get_header_url(self, response):
        header = response.xpath(\'//dl[@class=\\\'personal_cen\\\']/dt/img/@src\').extract()
        if len(header) > 0:
            header_url = header[0]
        else:
            header_url = ""
        return header_url.strip()

    # 提取用户名
    def get_username(self, response):
        usernames = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/div/strong/text()").extract()
        if len(usernames) > 0:
            username = usernames[0]
        else:
            username = "NULL"
        return username.strip()

    # 提取内心独白
    def get_monologue(self, response):
        monologues = response.xpath("//ul[@class=\\\'requre\\\']/li/p/text()").extract()
        if len(monologues) > 0:
            monologue = monologues[0]
        else:
            monologue = "NULL"
        return monologue.strip()

    # 提取相册图片地址
    def get_pic_urls(self, response):
        pic_urls = []
        data_url_full = response.xpath(\'//li[@class=\\\'smallPhoto\\\']/@data_url_full\').extract()
        if len(data_url_full) <= 1:
            pic_urls.append("");
        else:
            for pic_url in data_url_full:
                pic_urls.append(pic_url)
        if len(pic_urls) <= 1:
            return "NULL"
        # 每个url用|分隔
        return \'|\'.join(pic_urls)

    # 提取年龄
    def get_age(self, response):
        age_urls = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/p[@class=\\\'local\\\']/text()").extract()
        if len(age_urls) > 0:
            age = age_urls[0]
        else:
            age = "0"
        age_words = re.split(\' \', age)
        if len(age_words) <= 2:
            return "0"
        age = age_words[2][:-1]
        # 从age字符串开始匹配数字,失败返回None
        if re.compile(r\'[0-9]\').match(age):
            return age
        return "0"

运行程序:

  1. Master端打开 Redis: redis-server
  2. Slave端直接运行爬虫: scrapy crawl youyuan
  3. 多个Slave端运行爬虫顺序没有限制。

将项目修改成 RedisCrawlSpider 类的分布式爬虫,并尝试在多个Slave端运行。

有缘网分布式爬虫案例:

修改 spiders/youyuan.py

在spiders目录下增加youyuan.py文件编写我们的爬虫,使其具有分布式:

# -*- coding:utf-8 -*-

from scrapy.linkextractors import LinkExtractor
#from scrapy.spiders import CrawlSpider, Rule

# 1. 导入RedisCrawlSpider类,不使用CrawlSpider
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.spiders import Rule


from scrapy.dupefilters import RFPDupeFilter
from example.items import youyuanItem
import re

# 2. 修改父类 RedisCrawlSpider
# class YouyuanSpider(CrawlSpider):
class YouyuanSpider(RedisCrawlSpider):
    name = \'youyuan\'

# 3. 取消 allowed_domains() 和 start_urls
##### allowed_domains = [\'youyuan.com\']
##### start_urls = [\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/\']

# 4. 增加redis-key
    redis_key = \'youyuan:start_urls\'

    list_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/find/.+\'))
    page_lx = LinkExtractor(allow =(r\'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\\d+/\'))
    profile_page_lx = LinkExtractor(allow=(r\'http://www.youyuan.com/\\d+-profile/\'))

    rules = (
        Rule(list_page_lx, follow=True),
        Rule(page_lx, follow=True),
        Rule(profile_page_lx, callback=\'parse_profile_page\', follow=False),
    )

# 5. 增加__init__()方法,动态获取allowed_domains()
    def __init__(self, *args, **kwargs):
        domain = kwargs.pop(\'domain\', \'\')
        self.allowed_domains = filter(None, domain.split(\',\'))
        super(youyuanSpider, self).__init__(*args, **kwargs)

    # 处理个人主页信息,得到我们要的数据
    def parse_profile_page(self, response):
        item = youyuanItem()
        item[\'header_url\'] = self.get_header_url(response)
        item[\'username\'] = self.get_username(response)
        item[\'monologue\'] = self.get_monologue(response)
        item[\'pic_urls\'] = self.get_pic_urls(response)
        item[\'age\'] = self.get_age(response)
        item[\'source\'] = \'youyuan\'
        item[\'source_url\'] = response.url

        yield item

    # 提取头像地址
    def get_header_url(self, response):
        header = response.xpath(\'//dl[@class=\\\'personal_cen\\\']/dt/img/@src\').extract()
        if len(header) > 0:
            header_url = header[0]
        else:
            header_url = ""
        return header_url.strip()

    # 提取用户名
    def get_username(self, response):
        usernames = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/div/strong/text()").extract()
        if len(usernames) > 0:
            username = usernames[0]
        else:
            username = "NULL"
        return username.strip()

    # 提取内心独白
    def get_monologue(self, response):
        monologues = response.xpath("//ul[@class=\\\'requre\\\']/li/p/text()").extract()
        if len(monologues) > 0:
            monologue = monologues[0]
        else:
            monologue = "NULL"
        return monologue.strip()

    # 提取相册图片地址
    def get_pic_urls(self, response):
        pic_urls = []
        data_url_full = response.xpath(\'//li[@class=\\\'smallPhoto\\\']/@data_url_full\').extract()
        if len(data_url_full) <= 1:
            pic_urls.append("");
        else:
            for pic_url in data_url_full:
                pic_urls.append(pic_url)
        if len(pic_urls) <= 1:
            return "NULL"
        return \'|\'.join(pic_urls)

    # 提取年龄
    def get_age(self, response):
        age_urls = response.xpath("//dl[@class=\\\'personal_cen\\\']/dd/p[@class=\\\'local\\\']/text()").extract()
        if len(age_urls) > 0:
            age = age_urls[0]
        else:
            age = "0"
        age_words = re.split(\' \', age)
        if len(age_words) <= 2:
            return "0"
        age = age_words[2][:-1]
        if re.compile(r\'[0-9]\').match(age):
            return age
        return "0" 

分布式爬虫执行方式:

6. 在Master端启动redis-server:
redis-server
7. 在Slave端分别启动爬虫,不分先后:
scrapy runspider youyuan.py
8. 在Master端的redis-cli里push一个start_urls
redis-cli> lpush youyuan:start_urls http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/
scrapy-redis分布式爬虫实战

[Python3网络爬虫开发实战] 1.8.4-Scrapy-Redis的安装

实战scrapy-redis + webdriver 爬取航空网站

Python网络爬虫Scrapy+MongoDB +Redis实战爬取腾讯视频动态评论教学视频

Python3分布式爬虫(scrap+redis)基础知识和实战详解

scrapy主动退出爬虫的代码片段(python3)