Scrapy框架-分布式爬虫实现及scrapy_redis使用

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: scrapy是不支持分布式的。分布式爬虫应该是在多台服务器(A B C服务器),他们不会重复交叉爬取(需要用到状态管理器)。有主从之分的分布式结构图重点一、我的机器是Linux系统或者是MacOSX系统,不是Windows二、区别,事实上,分布式爬虫有几个不同的需求,会导致结构不一样,我举个例子:1、我需要多台机器同时爬取目标url并且同时从url中抽取数据,N台机器做一模一样的事,通过redis来调度、中转,也就是说它根本没有主机从机之分。

scrapy是不支持分布式的。分布式爬虫应该是在多台服务器(A B C服务器),他们不会重复交叉爬取(需要用到状态管理器)。


有主从之分的分布式结构图

重点

一、我的机器是Linux系统或者是MacOSX系统,不是Windows

二、区别,事实上,分布式爬虫有几个不同的需求,会导致结构不一样,我举个例子:

1、我需要多台机器同时爬取目标url并且同时从url中抽取数据,N台机器做一模一样的事,通过redis来调度、中转,也就是说它根本没有主机从机之分。

2、我有n台机器负责爬取目标URL,另外有M台机器负责容url中抽取数据,N和M做的事不一样,他们也是通过redis来进行调度和中转,它有主机从机之分。

这里演示的,是第1中,N台机器做一模一样的事。这点大家要搞清楚。

分布式爬虫优点:

① 充分利用多台机器的带宽速度爬取数据

② 充分利用多台机器的IP爬取

通过状态管理器来调度scrapy,就需要改造一下scrapy,要解决两个问题:

① request之前是放在内存的,现在两台服务器就需要对队列进行集中管理。

② 去重也要进行集中管理

redis安装和命令

参考菜鸟教程的安装以及命令介绍(由于安装时候是下载压缩包后进行解压再安装,所以会留下压缩包和文件夹。需要找一个指定的文件夹存放这些东西,我的电脑一般是放在home/ranbos/Programe File目录下,打开终端,执行以下命令)

$ wget http://download.redis.io/releases/redis-4.0.6.tar.gz
$ tar xzf redis-4.0.6.tar.gz
$ cd redis-4.0.6
$ make

这次笔记时候的redis版本是4.0.6。 make完后 redis-4.0.6目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli,两个程序位于安装目录 src 目录下,下面启动redis服务:

$ cd src
$ ./redis-server

就可以看到redis的启动画面了。但是这只是启动服务,如果想输入命令的话,还需要打开另一个终端,同样进入到src目录下,运行./redis-cli命令,才能进入命令交互界面。

设置密码

redis是可以匿名访问的,所以需要设置连接密码,在cli窗口通过命令查看密码设置状态:

CONFIG get requirepass

可以得到一个结果,那就是没有设置密码

"requirepass"

通过命令设置密码:

CONFIG set requirepass "ranbos"

再次查看的时候就会提示:

(error) NOAUTH Authentication required.

需要登录才行,登录的命令是:

AUTH "ranbos"

只要密码对了,就可以连接上去了。


redis基础知识

redis是一个key-value存储系统,它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

基础操作

一些基础操作就根据菜鸟教程的文章进行学习吧


通过scrapy-redis搭建分布式爬虫

在github上搜索scrapy-redis,里面有具体的文档及介绍。

① 安装redis

通过pycharm安装redis

② 配置scrapy-redis

根据文档的说明,到settings.py中更改配置,在空白地方新增代码:

""" scrapy-redis配置 """
# Enables scheduling storing requests queue in redis.
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# Ensure all spiders share same duplicates filter through redis.
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

然后到ITEM_PIPELINES中新增:

    # Store scraped item in redis for post-processing. 分布式redispipeline
    'scrapy_redis.pipelines.RedisPipeline': 300,

即可完成配置。

但是在写代码的时候跟之前的写法不一样,文档这里介绍到:

from scrapy_redis.spiders import RedisSpider

class MySpider(RedisSpider):
    name = 'myspider'

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

在爬虫里面引入scrapy_redis的包,以及在类继承的不能继承scrapy.Spider了,而是继承RedisSpider

还有另外两点

run the spider:

scrapy runspider myspider.py
push urls to redis:

push urls to redis:

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

要预先放置url在redis当中才行,否则爬虫会一直在等待。


开始搭建分布式爬虫

① 新建项目

为了更好的测试scrapy-redis,需要新建一个项目,但是可以选择之前爬虫的虚拟环境,这样就可以不用重复装那么多外部包了

用pycharm新建ScrapyRedis项目,在选择虚拟环境的时候选择之前jobbole-test那个虚拟环境,路径在C盘Admin用户下的Jobbole-test/Script/python.exe。

② 新建scrapy项目

用scrapy startproject ScrapyRedisTest命令来新建项目,建好项目后不着急建工程。

然后到github上下载scrapy-redis(实际上是要用pip安装scrapy-redis外部包)。解压后,复制文件夹下面的src目录下的scrapy_redis放到项目目录下,与项目的Spider目录同级。

接着在spider目录下新建jobbole.py文件,将使用说明里的示例代码粘贴进去,覆盖默认的爬虫类:

from scrapy_redis.spiders import RedisSpider

class MySpider(RedisSpider):
    name = 'myspider'

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

由于路径问题,自己改一下:

from ..scrapy_redis.spiders import RedisSpider

注意

这里复制进来只是为了更好的观察和跟踪代码,实际上是要用Pycharm安装scrapy-redis外部包的,一定要装。

跟踪RedisSpider代码可以发现它是继承了两个类:

class RedisSpider(RedisMixin, Spider)

scrapy的默认Spider以及对redis操作的RedisMixin。

然后跟踪代码RedisMixin。可以看到它用setup_redis给每个爬虫设置了一个redis的key,方法里面包含:

 self.redis_key = settings.get(
                'REDIS_START_URLS_KEY', defaults.START_URLS_KEY,
            )

意思就是不同的爬虫会自己设置一个默认的key,可以进行覆盖,也可以让它自动生成。覆盖的方法就是在scrapy-redis包的源码中,路径是

scrapy-redis\example-project\example\spiders

里面有两个文件:

mycrawler_redis.py

myspider_redis.py

对应两种不同的爬虫。

这里以myspider.py为例,将jobbole.py的代码改成:

from ..scrapy_redis.spiders import RedisSpider


class JobboleSpider(RedisSpider):
    name = 'jobbole'
    allowd_domains = ["blog.jobbole.com"]
    redis_key = 'jobbole:start_urls'
    def parse(self, response):
        # do stuff
        pass

现在先放着,看下一个RedisMixin中还有一个方法next_requests:

 def next_requests(self):
        """Returns a request to be scheduled or none."""
        use_set = self.settings.getbool('REDIS_START_URLS_AS_SET', defaults.START_URLS_AS_SET)
        fetch_one = self.server.spop if use_set else self.server.lpop
        # XXX: Do we need to use a timeout here?
        found = 0
        # TODO: Use redis pipeline execution.
        while found < self.redis_batch_size:
            data = fetch_one(self.redis_key)
            if not data:
                # Queue empty.
                break
            req = self.make_request_from_data(data)
            if req:
                yield req
                found += 1
            else:
                self.logger.debug("Request not made from data: %r", data)

        if found:
            self.logger.debug("Read %s requests from '%s'", found, self.redis_key)

之前的scrapy获取下一个队列next_requests,是从本机Schedule获取的,这里是通过redis获取的。


准备测试

将之前写的jobbole的逻辑代码拿到这个jobbole中:


from scrapy.http import Request
from urllib import parse

from ..scrapy_redis.spiders import RedisSpider


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

    def parse(self, response):

        """
        逻辑分析
            1.通过抓取下一页的链接,交给scrapy实现自动翻页,如果没有下一页则爬取完成
            2.将本页面的所有文章url爬下,并交给scrapy进行深入详情页的爬取
        """
        node_urls = response.css('#archive .floated-thumb .post-thumb a')
        for node_url in node_urls:
            title_url = node_url.css('::attr(href)').extract_first("")
            title_img = node_url.css('img::attr(src)').extract_first("")
            yield Request(url=parse.urljoin(response.url, title_url), meta={"title_img": title_img},
                          callback=self.parse_detail)

        # 实现下一页的翻页爬取
        next_pages = response.css('.next.page-numbers::attr(href)').extract_first("")  # 在当前列表页获取下一页链接
        if next_pages:
            yield Request(url=parse.urljoin(response.url, next_pages), callback=self.parse)  # 如果存在下一页,则将下一页交给parse自身处理

    def parse_detail(self, response):
        """
        将爬虫爬取的数据送到item中进行序列化
        这里通过ItemLoader加载item
        """
        pass

然后根据说明文档,到settings.py中进行配置:

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

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

""" scrapy-redis配置 """
# Enables scheduling storing requests queue in redis.
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# Ensure all spiders share same duplicates filter through redis.
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

也就是将scrapy_redis的item_pipeline、scheduler、dupefilter_class配置进来,并且关闭robots协议设置

为了调试,需要在项目写一个main.py文件,里面的代码跟之前的一样:

import os,sys
from scrapy.cmdline import execute

sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute(["scrapy","crawl","jobbole"])

保存即可运行,这时候如果redis之前有设置登录密码的话,是会报错的。这里可以用命令,到redis里面取消登录密码,到redis/src目录下打开终端:

./redis-cli

进入redis命令终端:

config get requirepass   

查看是否有密码,如果结果显示:

1) "requirepass"
2) "ranbos"

那就说明requirepass对应的密码是ranbos,要取消就输入命令:

config set requirepass ''

即可。这时候保存运行,发现爬虫启动了,但是没有开始爬取,是因为scrapy_redis现在的start_urls是从redis里面取的,所以在redis里面设置key :

redis-cli lpush jobbole:start_urls http://blog.jobbole.com/all-posts

也就是在redis中设置一个Jobbole的初始url,这样爬虫开始爬取的时候就会取这个url开始,如果没有则报错。

然后在jobbole.py的paser方法和paser_detail方法里面打断点,以便调试。

Debug运行,发现可以运行了,也正确的进入了paser方法和paser_detail方法里面。其他操作跟之前的jobbole爬虫一模一样即可。


观察过程

为了更好的观察过程,需要在scrapy-redis源码包

[项目jobbole-test\Lib\site-packages\scrapy_redis\scheduler.py]

中的next_request方法里面:

        request = self.queue.pop(block_pop_timeout)

这句代码打一个断点,然后恢复断点继续(继续才会从redis里面取starturls,它的取值方法是pop,所以取完后redis是不会有这条记录的),等程序运行到parse里面的时候,断点暂停,不要点继续,在暂停的时候到redis中用命令查看:

 redis-cli keys *

就会得到这些数据:

1) "myKey"
2) "jobbole:dupefilter"
3) "jobbole:requests"
4) "runoobkey"
5) "mykey"

之前的那些是我插入的,真正的是:

2) "jobbole:dupefilter"
3) "jobbole:requests"

凡是在spider(这里是jobbole爬虫)中用yield出去的记录,都通过scheduler.py里面的enqueue_request方法发push送到这jobbole:requests里面记录着,然后jobbole:dupefilter是个过滤器里面记录的是指纹。

通过命令:

redis-cli zrangebyscore jobbole:requests 0 100

可以看到redis里面存储的requests数据,这样爬虫发送的所有请求都会在requests中存有记录,分布式爬虫通过里面的记录和dupefilter里面的指纹实现的去重,不会造成交叉重复爬取。


远程redis

既然它是一个分布式爬虫,就会存在多个服务器。但是负责去重和调度的只能是其中1个服务器,其他的都根据它的redis来抽取request。主要的机器一般叫做master,其他的机器称为slave。

数据库密码连接

之前有提到过,redis可以是不用密码的,但是这显然很危险。还是要根据命令设定好密码。但是如果设定好密码后,爬虫不进行配置就会报错。那如何进行密码配置呢(基于单机情况),在settings.py中新增配置:

REDIS_PARAMS ={
    'password': 'ranbos',
}

就是这么简单,保存后运行即可。

远程服务器redis

如果是远程服务器上面的redis是如何连接的呢?

  • 还好有台阿里云服务器,在上面根据之前的redis安装方法将它安装上,然后设置好密码。
  • 在阿里云服务器安全配置规则里面把6379端口打开
  • (有可能需要将bind地址从127.0.0.1改成0.0.0.0)这个我忘了
  • 在本地settings配置中新增配置即可
    新增的配置代码为:
# 指定redis数据库的连接参数
REDIS_HOST = "59.110.xxx.xxx"
REDIS_PORT = "6379"
REDIS_PARAMS ={
    'password': 'ranbospider',  # 服务器的redis对应密码
}

然后开启爬虫,再用命令在服务器的redis上把start_urls添加进去:

lpush jobbole:start_urls http://blog.jobbole.com/all-posts

就完成了scrapy及远程服务器的连接设置。(多个sleva连接master都可以这么设置)

动态配置

在动态配置知识中,可以通过在具体的spider里面重载custom_settings来实现动态配置。这里的redis同样适合动态配置,现将setting里面之前写的配置注释掉,到具体的spider代码中(这里用jobbole演示):

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

    custom_settings = {
        # 指定redis数据库的连接参数
        'REDIS_HOST':"59.110.xxx.xxx",
        'REDIS_PORT':"6379",
        'REDIS_PARAMS': {
            'password': 'ranbospider',
        },
    }
    def parse(self, response):
        pass

同样可以实现远程redis,而且还可以根据不同的爬虫设定不同的服务器地址、配置。

小知识

在我们测试的时候,手动停止爬虫(爬虫自动爬取完毕是finish),手动停止是killed。待下次开启爬虫测试的时候,它总是会再爬取几条信息。

原因是上一次手动关闭爬虫,但request队列里面还有记录,所以打开它就会爬完上次的数据。然后就进入等待阶段,等待我用命令将start_urls添加到redis里面。


去重源码讲解

在源码包里面有:

dupefilter.py

文件,它的功能主要是去重。它的源码里面用到的方法与scrapy的源码和功能基本上是一致的:

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

在初始化的时候就自动连接了server,而这个server 在from_settings方法里面生成,跟踪代码可以发现它是自动连接到redis的,而且把key也传到了dupefilter里面。

然后看到request_seen方法:

def request_seen(self, request):

    fp = self.request_fingerprint(request)
        # This returns the number of values added, zero if already exists.
        added = self.server.sadd(self.key, fp)
        return added == 0

意思是会根据request生成一个指纹,然后把指纹添加到redis中,如果成功则返回1,如果失败则返回0。返回0代表里面已经有一个相同的指纹了。

这样就完成了去重的任务


打开源码包里面的pipelines.py,里面有一个RedisPipeline类。首先,它的入口是from_settings方法,里面也有一句代码:

            'server': connection.from_settings(settings),

这个server指向的也是上面介绍的那个server,也就是我们的redis。

接着看process_item方法,这里面这是pipelines里面的重要方法,数据传到pipeline都会经过process_item的处理


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

它调用了deferToThread方法(一个异步化的方法),放到另外一个线程中去做。然后它还调用了_process_item方法:

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

它首先调用spider的name去redis中找到对应的变量,然后通过rpush放置到队列的队尾。


源码包里面还有个queue.py文件

里面有几个类要讲解一下

FifoQueue 就是先进先出的有序队列

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
1月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
2天前
|
存储 监控 数据可视化
常见的分布式定时任务调度框架
分布式定时任务调度框架用于在分布式系统中管理和调度定时任务,确保任务按预定时间和频率执行。其核心概念包括Job(任务)、Trigger(触发器)、Executor(执行器)和Scheduler(调度器)。这类框架应具备任务管理、任务监控、良好的可扩展性和高可用性等功能。常用的Java生态中的分布式任务调度框架有Quartz Scheduler、ElasticJob和XXL-JOB。
121 66
|
9天前
|
分布式计算 大数据 数据处理
技术评测:MaxCompute MaxFrame——阿里云自研分布式计算框架的Python编程接口
随着大数据和人工智能技术的发展,数据处理的需求日益增长。阿里云推出的MaxCompute MaxFrame(简称“MaxFrame”)是一个专为Python开发者设计的分布式计算框架,它不仅支持Python编程接口,还能直接利用MaxCompute的云原生大数据计算资源和服务。本文将通过一系列最佳实践测评,探讨MaxFrame在分布式Pandas处理以及大语言模型数据处理场景中的表现,并分析其在实际工作中的应用潜力。
41 2
|
25天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
73 5
|
28天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
61 8
|
1月前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
57 6
|
1月前
|
数据库
如何在Seata框架中配置分布式事务的隔离级别?
总的来说,配置分布式事务的隔离级别是实现分布式事务管理的重要环节之一,需要认真对待和仔细调整,以满足业务的需求和性能要求。你还可以进一步深入研究和实践 Seata 框架的配置和使用,以更好地应对各种分布式事务场景的挑战。
32 6
|
1月前
|
消息中间件 运维 数据库
Seata框架和其他分布式事务框架有什么区别
Seata框架和其他分布式事务框架有什么区别
29 1
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
44 5