(笔记)数据采集基础06

20240419

分布式爬虫

1.Scrapy_redis去重

(1)环境准备
pip3 install scrapy-redis
(2)实现
接下来我们只需要简单的几步操作就可以实现分布式爬虫的配置了。
修改 Scheduler
在前面的课时中我们讲解了 Scheduler 的概念,它是用来处理 Request Item 等对象的调度逻辑的,默认情况下,Request 的队列是在内存中的,为了实现分布式,我们需要将队列迁移到 Redis 中,这时候我们就需要修改 Scheduler ,修改非常简单,只需要在 settings.py 里面添加如下代码即可:
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
这里我们将 Scheduler 的类修改为 Scrapy-Redis 提供的 Scheduler 类,这样在我们运行爬虫时,
Request 队列就会出现在 Redis 中了。
修改 Redis 连接信息
另外我们还需要修改下 Redis 的连接信息,这样 Scrapy 才能成功连接到 Redis 数据库,修改格式如 下:
REDIS_URL = 'redis://[user:pass]@hostname:9001'
在这里我们需要根据如上的格式来修改,由于我的 Redis 是在本地运行的,所以在这里就不需要填写用 户名密码了,直接设置为如下内容即可:
REDIS_URL = 'redis://localhost:6379'
修改去重类
既然 Request 队列迁移到了 Redis ,那么相应的去重操作我们也需要迁移到 Redis 里面,前一节课我们讲解了 Dupefilter 的原理,这里我们就修改下去重类来实现基于 Redis 的去重:
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
配置持久化
一般来说开启了 Redis 分布式队列之后,我们不希望爬虫在关闭时将整个队列和去重信息全部删除,因为很有可能在某个情况下我们会手动关闭爬虫或者爬虫遭遇意外终止,为了解决这个问题,我们可以配置 Redis 队列的持久化,修改如下:
SCHEDULER_PERSIST = True
好了,到此为止我们就完成分布式爬虫的配置了。
(3)运行
上面我们完成的实际上并不是真正意义的分布式爬虫,因为 Redis 队列我们使用的是本地的 Redis ,所以多个爬虫需要运行在本地才可以,如果想实现真正意义的分布式爬虫,可以使用远程 Redis ,这样我们就能在多台主机运行爬虫连接此 Redis 从而实现真正意义上的分布式爬虫了。
不过没关系,我们可以在本地启动多个爬虫验证下爬取效果。我们在多个命令行窗口运行如下命令:
scrapy crawl book

2.Scrapy_redis举例

(1)

import scrapy
from xiaoshuo1.items import *
import re
class CentoschinaSpider(scrapy.Spider):
    name = "centoschina"
    # allowed_domains = ["ddsdfs"]
    # start_urls = ["https://www.centoschina.cn/troubleshooting/page/{}".format(page) for page in range(90)]
    #
    # start_urls = ['https://www.baidu.com/' for page in range(1000)]
    # def parse(self, response):
    #     print(response.url)
    def start_requests(self):
        # every_page_url = "https://www.centoschina.cn/troubleshooting/page/2"
        for key in range(1000):
            yield scrapy.Request('https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd={}'.format(key) ,self.question_lists)

    def question_lists(self,response):
        print(response.url)
    #     # title = response.xpath('//h2/a/text()').extract()
    #     hrefs = response.xpath('//h2/a/@href').extract()
    #     current_url = response.url
    #     current_page = int(current_url.split('/')[-1])
    #     next_page = current_page+1
    #     next_url = "https://www.centoschina.cn/troubleshooting/page/{}".format(next_page)
    #     yield scrapy.Request(next_url,self.question_lists)
    #     for href in hrefs:
    #         yield scrapy.Request(href, self.get_content)
    #
    # def get_content(self,response):
    #     item = CentosChinaItem()
    #     # title = response.meta['title']
    #     title = response.xpath('//h1/text()').extract_first().strip()
    #     contents = ''.join(response.xpath('//div[@class="content_post"]//text()').extract()).strip()
    #     print('标题',title,'内容',contents)
    #     item['title'] = title
    #     item['contents'] = contents
    #     return item


(2)

执行如下命令:
git clone https://github.com/rmax/scrapy-redis.git
核心源码在 scrapy-redis/src/scrapy_redis 目录下。
我们从爬取队列入手,来看看它的具体实现。源码文件为 queue.py ,它包含了三个队列的实
现,首先它实现了一个父类 Base ,提供一些基本方法和属性,如下所示:
class Base(object):
"""Per-spider base queue class"""
def __init__(self, server, spider, key, serializer=None):
if serializer is None:
serializer = picklecompat
if not hasattr(serializer, 'loads'):
raise TypeError("serializer does not implement 'loads' function: % r
% serializer)
if not hasattr(serializer, 'dumps'):
raise TypeError("serializer '% s' does not implement 'dumps' functio
% serializer)
self.server = server
self.spider = spider
self.key = key % {'spider': spider.name}
self.serializer = serializer
def _encode_request(self, request):
obj = request_to_dict(request, self.spider)
return self.serializer.dumps(obj)
def _decode_request(self, encoded_request):
obj = self.serializer.loads(encoded_request)
return request_from_dict(obj, self.spider)
def __len__(self):
"""Return the length of the queue"""
raise NotImplementedError
def push(self, request):
"""Push a request"""
raise NotImplementedError
def pop(self, timeout=0):
"""Pop a request"""
raise NotImplementedError
def clear(self):
"""Clear queue/stack"""
self.server.delete(self.key)
首先看一下 _encode_request _decode_request 方法,因为我们需要把一个 Request 对象
存储到数据库中,但数据库无法直接存储对象,所以需要将 Request 序列转化成字符串再存
储。
而这两个方法分别是序列化和反序列化的操作,利用 pickle 库来实现,一般在调用 push
Request 存入数据库时会调用 _encode_request 方法进行序列化,在调用 pop 取出 Request
的时候会调用 _decode_request 进行反序列化。
在父类中 len push pop 方法都是未实现的,会直接抛出 NotImplementedError ,因此是
不能直接使用这个类的,必须实现一个子类来重写这三个方法,而不同的子类就会有不同的实
现,也就有着不同的功能。
接下来我们就需要定义一些子类来继承 Base 类,并重写这几个方法,那在源码中就有三个子
类的实现,它们分别是 FifoQueue PriorityQueue LifoQueue ,我们分别来看下它们的实现
原理。
首先是 FifoQueue
class FifoQueue(Base):
"""Per-spider FIFO queue"""
def __len__(self):
"""Return the length of the queue"""
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.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)
可以看到这个类继承了 Base 类,并重写了 len push pop 这三个方法,在这三个方法中都
是对 server 对象的操作,而 server 对象就是一个 Redis 连接对象,我们可以直接调用其操作
Redis 的方法对数据库进行操作,可以看到这里的操作方法有 llen lpush rpop 等,这就代
表此爬取队列是使用的 Redis 的列表。
序列化后的 Request 会被存入列表中,就是列表的其中一个元素, len 方法是获取列表的长
度, push 方法中调用了 lpush 操作,这代表从列表左侧存入数据, pop 方法中调用了 rpop
作,这代表从列表右侧取出数据。
所以 Request 在列表中的存取顺序是左侧进、右侧出,所以这是有序的进出,即先进先出,
英文叫作 First Input First Output ,也被简称为 FIFO ,而此类的名称就叫作 FifoQueue
另外还有一个与之相反的实现类,叫作 LifoQueue ,实现如下
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)
FifoQueue 不同的就是它的 pop 方法,在这里使用的是 lpop 操作,也就是从左侧出,而
push 方法依然是使用的 lpush 操作,是从左侧入。
那么这样达到的效果就是先进后出、后进先出,英文叫作 Last In First Out ,简称为 LIFO ,而
此类名称就叫作 LifoQueue 。同时这个存取方式类似栈的操作,所以其实也可以称作
StackQueue
另外在源码中还有一个子类实现,叫作 PriorityQueue ,顾名思义,它叫作优先级队列,实现
如下:
class PriorityQueue(Base):
"""Per-spider priority queue abstraction using redis' sorted set"""
def __len__(self):
"""Return the length of the queue"""
return self.server.zcard(self.key)
def push(self, request):
"""Push a request"""
data = self._encode_request(request)
score = -request.priority
self.server.execute_command('ZADD', self.key, score, data)
def pop(self, timeout=0):
"""
Pop a request
timeout not support in this queue class"""
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])
在这里我们可以看到 len push pop 方法中使用了 server 对象的 zcard zadd zrange
作,可以知道这里使用的存储结果是有序集合 Sorted Set ,在这个集合中每个元素都可以设置
一个分数,那么这个分数就代表优先级。
len 方法里调用了 zcard 操作,返回的就是有序集合的大小,也就是爬取队列的长度,在
push 方法中调用了 zadd 操作,就是向集合中添加元素,这里的分数指定成 Request 的优先
级的相反数,因为分数低的会排在集合的前面,所以这里高优先级的 Request 就会存在集合
的最前面。
pop 方法是首先调用了 zrange 操作取出了集合的第一个元素,因为最高优先级的 Request
存在集合最前面,所以第一个元素就是最高优先级的 Request ,然后再调用 zremrangebyrank
操作将这个元素删除,这样就完成了取出并删除的操作。
此队列是默认使用的队列,也就是爬取队列默认是使用有序集合来存储的

3.Scrapy部署

分布式爬虫部署中代码部署比较复杂;

  • 如果采用上传文件的方式部署代码,我们首先需要将代码压缩,然后采用 SFTP 或 FTP 的方式将文件上传到服务器,之后再连接服务器将文件解压,每个服务器都需要这样配置。

  • 如果采用 Git 同步的方式部署代码,我们可以先把代码 Push 到某个 Git 仓库里,然后再远程连接各台主机执行 Pull 操作,同步代码,每个服务器同样需要做一次操作。
    如果代码突然有更新,那我们必须更新每个服务器,而且万一哪台主机的版本没控制好,还可能会影响整体的分布式爬取状况。

可以尝试提供分布式部署的工具 Scrapyd:

安装: pip install scrapyd

它的配置文件为default_scrapyd.conf 可以自行修改,比如改一个端口,它的默认端口为6800
在这里插入图片描述
配置文件的内容可以参见官方文档 https://scrapyd.readthedocs.io/en/stable/config.html#example-configuration-file。这里的配置文件有所修改,其中之一是 max_proc_per_cpu 官方默认为 4,即一台主机每个 CPU 最多运行 4 个 Scrapy 任务,在此提高为 10。另外一个是 bind_address,默认为本地 127.0.0.1,在此修改为 0.0.0.0,以使外网可以访问。

安装并运行了 Scrapyd 之后,我们就可以访问服务器的 6800 端口看到一个 WebUI 页面了,我之前修改过端口,将端口由6800改为6801,安装好了 Scrapyd 并成功运行,那么我就可以在本地的浏览器中打开: http://127.0.0.1:6801,就可以看到 Scrapyd 的首页,这里请自行替换成你的服务器地址查看即可,如图所示:
在这里插入图片描述
如果可以成功访问到此页面,那么证明 Scrapyd 配置就没有问题了

Scrapyd 的功能

daemonstatus.json

这个接口负责查看 Scrapyd 当前服务和任务的状态,我们可以用 curl 命令来请求这个接口,命令如下:

curl http://139.217.26.30:6800/daemonstatus.json 

这样我们就会得到如下结果:

{"status": "ok", "finished": 90, "running": 9, "node_name": "datacrawl-vm", "pending": 0} 

返回结果是 Json 字符串,status 是当前运行状态, finished 代表当前已经完成的 Scrapy 任务,running 代表正在运行的 Scrapy 任务,pending 代表等待被调度的 Scrapyd 任务,node_name 就是主机的名称。

addversion.json

这个接口主要是用来部署 Scrapy 项目,在部署的时候我们需要首先将项目打包成 Egg 文件,然后传入项目名称和部署版本。

我们可以用如下的方式实现项目部署:

curl http://120.27.34.25:6800/addversion.json -F project=wenbo -F version=first -F egg=@weibo.egg 

在这里 -F 即代表添加一个参数,同时我们还需要将项目打包成 Egg 文件放到本地。
这样发出请求之后我们可以得到如下结果:

{"status": "ok", "spiders": 3} 

这个结果表明部署成功,并且其中包含的 Spider 的数量为 3。此方法部署可能比较烦琐,在后面我会介绍更方便的工具来实现项目的部署。

schedule.json

这个接口负责调度已部署好的 Scrapy 项目运行。我们可以通过如下接口实现任务调度:

curl http://120.27.34.25:6800/schedule.json -d project=weibo -d spider=weibocn 

在这里需要传入两个参数,project 即 Scrapy 项目名称,spider 即 Spider 名称。返回结果如下:

{"status": "ok", "jobid": "6487ec79947edab326d6db28a2d86511e8247444"} 

status 代表 Scrapy 项目启动情况,jobid 代表当前正在运行的爬取任务代号。

cancel.json

这个接口可以用来取消某个爬取任务,如果这个任务是 pending 状态,那么它将会被移除,如果这个任务是 running 状态,那么它将会被终止。

我们可以用下面的命令来取消任务的运行:

curl http://120.27.34.25:6800/cancel.json -d project=weibo -d job=6487ec79947edab326d6db28a2d86511e8247444 

在这里需要传入两个参数,project 即项目名称,job 即爬取任务代号。返回结果如下:

{"status": "ok", "prevstate": "running"} 

status 代表请求执行情况,prevstate 代表之前的运行状态。

listprojects.json

这个接口用来列出部署到 Scrapyd 服务上的所有项目描述。我们可以用下面的命令来获取 Scrapyd 服务器上的所有项目描述:

curl http://120.27.34.25:6800/listprojects.json 

这里不需要传入任何参数。返回结果如下:

{"status": "ok", "projects": ["weibo", "zhihu"]} 

status 代表请求执行情况,projects 是项目名称列表。

listversions.json

这个接口用来获取某个项目的所有版本号,版本号是按序排列的,最后一个条目是最新的版本号。

我们可以用如下命令来获取项目的版本号:

curl http://120.27.34.25:6800/listversions.json?project=weibo 

在这里需要一个参数 project,就是项目的名称。返回结果如下:

{"status": "ok", "versions": ["v1", "v2"]} 

status 代表请求执行情况,versions 是版本号列表。

listspiders.json

这个接口用来获取某个项目最新的一个版本的所有 Spider 名称。我们可以用如下命令来获取项目的 Spider 名称:

curl http://120.27.34.25:6800/listspiders.json?project=weibo 

在这里需要一个参数 project,就是项目的名称。返回结果如下:

{"status": "ok", "spiders": ["weibocn"]} 

status 代表请求执行情况,spiders 是 Spider 名称列表。

listjobs.json

这个接口用来获取某个项目当前运行的所有任务详情。我们可以用如下命令来获取所有任务详情:

curl http://120.27.34.25:6800/listjobs.json?project=weibo 

在这里需要一个参数 project,就是项目的名称。返回结果如下:

{"status": "ok", 
 "pending": [{"id": "78391cc0fcaf11e1b0090800272a6d06", "spider": "weibocn"}], 
 "running": [{"id": "422e608f9f28cef127b3d5ef93fe9399", "spider": "weibocn", "start_time": "2017-07-12 10:14:03.594664"}], 
 "finished": [{"id": "2f16646cfcaf11e1b0090800272a6d06", "spider": "weibocn", "start_time": "2017-07-12 10:14:03.594664", "end_time": "2017-07-12 10:24:03.594664"}]} 

status 代表请求执行情况,pendings 代表当前正在等待的任务,running 代表当前正在运行的任务,finished 代表已经完成的任务。

delversion.json

这个接口用来删除项目的某个版本。我们可以用如下命令来删除项目版本:

curl http://120.27.34.25:6800/delversion.json -d project=weibo -d version=v1 

在这里需要一个参数 project,就是项目的名称,还需要一个参数 version,就是项目的版本。返回结果如下:

{"status": "ok"} 

status 代表请求执行情况,这样就代表删除成功了。

delproject.json

这个接口用来删除某个项目。我们可以用如下命令来删除某个项目:

curl http://120.27.34.25:6800/delproject.json -d project=weibo 

在这里需要一个参数 project,就是项目的名称。返回结果如下:

{"status": "ok"} 

status 代表请求执行情况,这样就代表删除成功了。
以上就是 Scrapyd 所有的接口,我们可以直接请求 HTTP 接口即可控制项目的部署、启动、运行等操作。

scrapydAPI 的使用

以上的这些接口可能使用起来还不是很方便,没关系,还有一个 ScrapydAPI 库对这些接口又做了一层封装,其安装方式如下:

pip install python-scrapyd-api 

下面我们来看下 ScrapydAPI 的使用方法,其实核心原理和 HTTP 接口请求方式并无二致,只不过用 Python 封装后使用更加便捷。
我们可以用如下方式建立一个 ScrapydAPI 对象:

from scrapyd_api import ScrapydAPI 
scrapyd = ScrapydAPI('http://120.27.34.25:6800') 

然后就可以通过调用它的方法来实现对应接口的操作了,例如部署的操作可以使用如下方式:

egg = open('weibo.egg', 'rb') 
scrapyd.add_version('weibo', 'v1', egg) 

这样我们就可以将项目打包为 Egg 文件,然后把本地打包的 Egg 项目部署到远程 Scrapyd 了。

另外 ScrapydAPI 还实现了所有 Scrapyd 提供的 API 接口,名称都是相同的,参数也是相同的。

例如我们可以调用 list_projects 方法即可列出 Scrapyd 中所有已部署的项目:

scrapyd.list_projects() 
['weibo', 'zhihu'] 

另外还有其他的方法在此不再一一列举了,名称和参数都是相同的,更加详细的操作可以参考其官方文档: http://python-scrapyd-api.readthedocs.io/。
我们可以通过它来部署项目,并通过 HTTP 接口来控制任务的运行,不过这里有一个不方便的地方就是部署过程,首先它需要打包 Egg 文件然后再上传,还是比较烦琐的,这里再介绍另外一个工具 Scrapyd-Client。

Scrapyd-Client 部署

Scrapyd-Client 为了方便 Scrapy 项目的部署,提供两个功能:

  • 将项目打包成 Egg 文件。

  • 将打包生成的 Egg 文件通过 addversion.json 接口部署到 Scrapyd 上。

也就是说,Scrapyd-Client 帮我们把部署全部实现了,我们不需要再去关心 Egg 文件是怎样生成的,也不需要再去读 Egg 文件并请求接口上传了,这一切的操作只需要执行一个命令即可一键部署。

要部署 Scrapy 项目,我们首先需要修改一下项目的配置文件,例如我们之前写的 Scrapy 项目,在项目的第一层会有一个 scrapy.cfg 文件,它的内容如下:

[deploy] 
url = http://120.27.34.25:6800/ 
project = scrapypyppeteer 

补充一个说明
在这里插入图片描述

这样我们再在 scrapy.cfg 文件所在路径执行如下命令:

scrapyd-deploy 

运行结果如下:

Packing version 1501682277 
Deploying to project "weibo" in http://120.27.34.25:6800/addversion.json 
Server response (200): 
{"status": "ok", "spiders": 1, "node_name": "datacrawl-vm", "project": "scrapypyppeteer", "version": "1501682277"} 

返回这样的结果就代表部署成功了。

我们也可以指定项目版本,如果不指定的话默认为当前时间戳,指定的话通过 version 参数传递即可,例如:

scrapyd-deploy --version 201707131455 

值得注意的是在 Python3 的 Scrapyd 1.2.0 版本中我们不要指定版本号为带字母的字符串,需要为纯数字,否则可能会出现报错。

另外如果我们有多台主机,我们可以配置各台主机的别名,例如可以修改配置文件为:

[deploy:vm1] 
url = http://120.27.34.24:6800/ 
project = scrapypyppeteer 
​ 
[deploy:vm2] 
url = http://139.217.26.30:6800/ 
project = scrapypyppeteer 

有多台主机的话就在此统一配置,一台主机对应一组配置,在 deploy 后面加上主机的别名即可,这样如果我们想将项目部署到 IP 为 139.217.26.30 的 vm2 主机,我们只需要执行如下命令:

scrapyd-deploy vm2 

这样我们就可以将项目部署到名称为 vm2 的主机上了。
如此一来,如果我们有多台主机,我们只需要在 scrapy.cfg 文件中配置好各台主机的 Scrapyd 地址,然后调用 scrapyd-deploy 命令加主机名称即可实现部署,非常方便。

如果 Scrapyd 设置了访问限制的话,我们可以在配置文件中加入用户名和密码的配置,同时修改端口,修改成 Nginx 代理端口,如在模块一我们使用的是 6801,那么这里就需要改成 6801,修改如下:

[deploy:vm1] 
url = http://120.27.34.24:6801/ 
project = scrapypyppeteer 
username = admin 
password = admin 
​ 
[deploy:vm2] 
url = http://139.217.26.30:6801/ 
project = scrapypyppeteer 
username = germey 
password = germey 

这样通过加入 username 和 password 字段,我们就可以在部署时自动进行 Auth 验证,然后成功实现部署。

//

解决scrapyd-deploy不是内部外部命令

通常情况下,在执行scrapyd-deploy时,会提示scrapyd-deploy不是内部或外部命令,嗯…这个是正常操作

解决步骤

1.找到Python解释器下面的Scripts,新建scrapy.bat和scrapyd-deploy.bat两个文件
在这里插入图片描述
改这两个文件,内容如下
在这里插入图片描述
在这里插入图片描述
自己根据需要粘贴修改

@echo off 
"C:\Python39\python.exe" "C:\Python39\Scripts\scrapyd-deploy" %*

@echo off 
C:\Python39\python C:\Python39\Scripts\scrapy %* 

如果报一个转义错误那么,打开scrapyd下的utils.py这个文件
在这里插入图片描述
140行左右改成这样即可
在这里插入图片描述
代码如下自行粘贴

  if isinstance(out, bytes):
        tmp = out.decode('utf-8').splitlines();
    else:
        tmp = out.splitlines();

Gerapy使用流程总结

使用gerapy之前要保证scrapyd 是可用的

直接在cmd里输入scrapyd,挂着别关
之后的步骤如下

1.gerapy init 初始化,会在文件夹下创建一个gerapy文件夹
2.cd gerapy
3.gerapy migrate
4.gerapy runserver 默认是127.0.0.1:8000
5.gerapy createsuperuser 创建账号密码,默认情况下都是没有的
6.游览器输入127.0.0.1:8000 登录账号密码,进入主页
7.各种操作,比如添加主机,打包项目,定时任务等

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值