⑴ 中间件
中间件基本介绍
在Scrapy中,中间件是一种插件机制
它允许你在发送请求和处理响应的过程中对Scrapy引擎的行为进行干预和定制。
Scrapy中间件的用途:
修改请求、处理响应、处理异常、设置代理、添加自定义的HTTP头部等等。
=====================================================================
Scrapy中间件主要分为以下几种类型:
下载中间件(Download Middleware):下载中间件是用于处理Scrapy发送请求和接收响应的过程。它可以用于修改请求的头部、处理代理、处理重定向、处理异常、修改请求URL等。你可以通过编写下载中间件来实现自定义的下载逻辑。
爬虫中间件(Spider Middleware):爬虫中间件用于处理爬虫发送的请求和接收的响应。它可以用于修改请求、处理响应、处理异常、修改爬取策略等。通过编写爬虫中间件,你可以实现一些高级的爬取逻辑,比如动态生成请求、处理请求的优先级等。
服务中间件(Service Middleware):服务中间件是用于处理Scrapy引擎和其他服务(比如缓存服务、代理服务等)之间的交互。通过编写服务中间件,你可以实现与外部服务的集成、数据的缓存、代理的管理等功能。
可以通过编写自定义的中间件来扩展和定制Scrapy的功能,实现一些高级的爬取和处理逻辑。Scrapy中间件的灵活性和可扩展性使得它成为一个非常强大的爬虫框架。
=====================================================================
从开发角度举个例子:
我们都知道,一个Django开发的web网站,有一个路由分发,有一堆辑函数。
当浏览器带着url访问服务器时,路由来判断需要哪个逻辑函数来处理,再由函数完成对数据库的增删改查,返回给浏览器响应。
比如一些大的网站会做一些ip黑名单反爬措施,当某一个ip来访问时,路由分发后交给函数进行验证是否为黑名单,但是如果有成百上千个逻辑函数,每一个函数都去判断一遍那就太扯了。
所以可以在路由分发之前加一个中间件,当ip进行访问时先经过中间件定义的验证类1/2/3依次进行验证,如果是黑名单,就不用往下分发了。
反之假设不是黑名单:
请求路线就是:url请求 - > 中间件1/2/3验证通过 -> 路由分发 -> 执行逻辑函数;
响应路线就是:逻辑函数返回响应信息 -> 路由分发 -> 中间件3/2/1层层执行返回给浏览器。
---------------->>>>
如开头的图片所示,对于爬虫而言,思路是一样的。
简单来说:中间件可以理解为python中的一个类
在一份代码中可以有多个中间件,每一个中间件都一个类
一个中间件的类里面封装两个核心方法:
process request(处理请求)
process response(处理响应)
中间件(Downloader)的配置及使用
对于做爬虫而言,我们常用的中间件是下载中间件(Download Middleware)
如果是用命令创建的项目,一般在项目文件下自动生成一个'middlewares.py'文件
middlewares.py文件中,有一个'NewsproDownloaderMiddleware'的类,它下面有4个方法:
from_crawler:创建中间件,可以获取Crawler对象的信息,比如配置、信号等process_request:处理请求,可以在方法中对请求进行预处理,比如修改请求头、添加代理等操作process_exception:处理请求过程中的异常,比如重新发送请求、记录日志等操作。process_response:处理响应,比如修改响应内容、记录日志、判断是否需要重新发送请求等操作spider_opened:开启爬虫后,可以在方法中对响应进行处理,比如修改响应内容、记录日志、判断是否需要重新发送请求等操作;-------------------------------------------------------------------------------------------------------------------------
5个方法中,使用最多的是:
process_request(使用占比约60%)、process_response(使用占比约30%)、 process_exception(使用占比约10%)其中process_request、process_response是核心
class MyDownMiddleware(object):
def process_request(self, request, spider):
"""
请求需要被下载时,经过所有下载器中间件的process_request调用
:param request:
:param spider:
:return:
None,继续后续中间件去下载;
Response对象,停止process_request的执行,开始执行process_response
Request对象,停止中间件的执行,将Request重新调度器
raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
"""
pass
def process_response(self, request, response, spider):
"""
spider处理完成,返回时调用
:param response:
:param result:
:param spider:
:return:
Response 对象:转交给其他中间件process_response
Request 对象:停止中间件,request会被重新调度下载
raise IgnoreRequest 异常:调用Request.errback
"""
print('response1')
return response
def process_exception(self, request, exception, spider):
"""
当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
:param response:
:param exception:
:param spider:
:return:
None:继续交给后续中间件处理异常;
Response对象:停止后续process_exception方法
Request对象:停止中间件,request将会被重新调用下载
"""
return None
中间件的配置
是否使用中间件,取决于'settings.py'文件中有没有配置Download方法
'settings.py'文件中Download默认是注释掉的,需要用的时候可以放开(54~56行)
里面的"Newspro.middlewares.NewsproDownloaderMiddleware"可以写很多个
方法命名自带完整的文件路径:Newspro是项目文件名,middlewares是py文件名,后面是方法
后面的数字表示权重,数字越小优先级越高
Downloader中间件简单案例
使用中间件请求百度,以及获取响应状态码:
❶ 新建DownloadDemo项目文件目录 -> 新建'baidu.py'文件
并添加输出响应状态码:response.status
❷ 自动生成'middlewares.py'文件添加及修改代码:
避免混淆,将类名改为TestMiddleware,'settings.py'文件的'DOWNLOADER_MIDDLEWARES'记得同步修改类名;
-> class TestMiddleware方法中,删除用不上的方法和注释内容,并添部分加代码:
from scrapy import signals
class TestMiddleware:
def process_request(self, request, spider):
print("request的方法属性:",dir(request))
print("request.url:",request.url)
print("process_request正在执行!!~")
return None
def process_response(self, request, response, spider):
print("process_response正在执行!!~")
return response
def process_exception(self, request, exception, spider):
pass
❸ 开启'settings.py'文件中的'DOWNLOADER_MIDDLEWARES'代码,默认是注释的
❹ 修改'settings.py'文件中的机器人协议ROBOTSTXT_OBEY为False,默认是True
大多数网站都是不让爬的,如果是True表示同意不让爬;所以需要改成False
❺ 然后启动爬虫文件baidu.py:
控制台成功输出了请求url、及响应状态码
tips: request的方法属性:当我们不知道这个方法有哪些方法属性时,就可以通过printdir(request)来获取他所有的方法属性,带两对_的,都是python自带的方法,不带_的是当前代码下自动生成的方法,也可以直接调用
⑵ settings配置文件详解
在之前的案例中,部分settings配置已经有提及到;settings.py汇总如下:
① 为什么项目中需要配置文件?
在配置文件中存放一些公共变量,在后续的项目中方便修改;
如:本地测试数据库和部署服务器的数据库不一致
-------------------->>>>② 配置文件中的变量使用方法:
● 变量名一般全部大写
● 导入即可使用-------------------->>>>
❸ settings.py中的重点字段和含义:
● USER_AGENT:设置ua
● ROBOTSTXT_OBEY:是否遵守robots协议,默认是遵守
● CONCURRENT_REQUESTS:设置并发请求的数量,默认是16个
● DOWNLOAD_DELAY :下载延迟,默认无延迟 (下载器在从同一网站下载连续页面之前应等待的时间(以秒为单位)。这可以用来限制爬行速度,以避免对服务器造成太大影响;主要用于翻页爬取)
● COOKIES_ENABLED:是否开启cookie,即每次请求带上前一次的cookie,默认是开启的
● DEFAULT_REQUEST_HEADERS :设置默认请求头,这里加入了user_agent将不起作用
● SPIDER_MIDDLEWARES:爬虫中间件,设置过程和管道相同
● DOWNLOADER_MIDDLEWARES:下载中间件
● LOG_LEVEL:控制终端输出信息的log级别,终端默认显示的是debug级别的log信息
● LOG_LEVEL = "WARNING"
- CRITICAL 严重
- ERROR 错误
- WARNING 警告
- INFO 消息
- DEBUG 调试● LOG_FILE:设置log日志文件的保存路径,如果设置该参数,终端将不再显示信息
LOG_FILE = "./test.log"
● FEED_EXPORT_ENCODING='UTF-8' :保存的是unicode编码字符转为utf8其他设置参考:https://www.jianshu.com/p/df9c0d1e9087
⑶ start_request方法重写
在 Scrapy中,可以通过重写 Spider 类的 start_requests 方法来自定义 Spider 的起始请求。默认情况下,Scrapy 会从 start_urls 中生成起始请求,但有时候我们需要更复杂的逻辑来生成起始请求,这时就可以重写 start_requests 方法。
----------->>>>
比如说:
要爬取的一个网站需要先登陆,再进入到其他页面才能爬取到我们想要的内容,Spider自动生成的是登陆的url,默认封装的是get请求;但是登陆一般是post请求,需要传用户名和密码,而且登陆之后,还需要请求其他要爬取页面的url,这时候显然start_requests已经不符合我们的需求了。
start_requests重写案例
之前在requests篇章的时候,有爬取过17k免费小说,现在使用scrapy框架来进行start_requests方法重写:
novel17k.py完整代码如下:
import scrapy
class Novel17kSpider(scrapy.Spider):
name = "novel17k"
#我的书架url(获取书架的每本书籍)
start_url = ["https://user.17k.com/ck/author2/shelf?page=1&appKey=2406394919"]
#重写start_requests,并前置补充登陆信息
def start_requests(self) :
#直接写死登陆url
login_url = "https://passport.17k.com/ck/user/login"
#使用Request子类FormRequest进行请求,自动为post请求
yield scrapy.FormRequest(
url = login_url,
formdata = {
"loginName": "13585687903","password": "135903yc" },
callback = self.do_start)
'''
上一个函数里的callback函数
do_start相当于原始的父类start_requests
当上面的登陆执行完之后,就callback执行start_url里的get请求
'''
def do_start(self,response):
for url in self.start_urls:
yield scrapy.Request(url,dont_filter=True)
#接收的是start_url的内容
def parse(self, response):
print("response::", response.text)
settings文件中:
记得将20行的机器人协议改成False、
34行的cookie打开并改成True(默认是关闭而且是false),不然执行到登陆代码会报错
这里的cookie打开修改后,就不用在爬虫程序中传cookie或者像使用自带的requests一样写requests.session了,打开为True就是让cookie工作的意思,这样scrapy就自动帮我们管理了cookie,不得不说真的很方便~
Teminal在终端输入''scrapy crawl novel17k"启动爬虫:response.text信息获取成功
⑷ 重复过滤
重复请求的去重操作
重复请求的去重是指在爬虫爬取过程中,避免发送重复的请求,以提高爬取效率和避免重复处理相同的页面。Scrapy 提供了内置的请求去重功能,可以帮助我们自动处理重复请求。
Scrapy 默认使用 Request 对象的 URL 来进行去重。
当 Spider 发送一个 Request 请求时,Scrapy 会检查该 URL 是否已经被处理过,如果是,则不会再次发送请求。这样可以避免重复请求相同的页面。
Scrapy 默认使用基于内存的去重器,它会将已经处理过的请求的 URL 存储在内存中。
如果需要持久化的请求去重,可以使用基于数据库的去重器,比如基于 Redis 的去重器。
基于settings文件去重
要启用请求去重功能,只需在项目的 settings.py 文件中设置 DUPEFILTER_CLASS 参数即可:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
基于Redis的去重
如果要使用基于 Redis 的去重器,可以这样设置:
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
过滤重复的items
itmes去重案例
大图网:http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html
比如,这个图片网站中,每页展示40张图片,一共有100页;每下翻一页,url中最后的数字就对应页码数字而变化,第二页就是‘美女图片-0-0-0-0-0_2.html’,以此类推。
---------------->>>>
上面有学习到,start_requests方法重写,那么如果是要爬取这个网站的100页所有图片,是否可以封装yield requests对象+for循环来实现呢?
我们来试试:
import scrapy
from scrapy import Request
class BigimgSpider(scrapy.Spider):
name = "bigimg"
allowed_domains = ["xxx.com"]
#大图网url,第一页
start_urls = ["http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html"]
def start_requests(self):
#根据源码改编:
for i in range(3):
url = self.start_urls[0]
yield Request(url,dont_filter=True)
def parse(self, response):
#获取首页图片的图片名称和src图片源地址
li_list = response.xpath('/html/body/div[5]/ul/li')
for li in li_list:
title = li.xpath('./a/img/@title').extract_first()
src = li.xpath('./a/img/@src').extract_first()
print(title,src)
启动爬虫等待执行完毕
结果是:用第一个图片的src后半段在控制台搜索,会有3个重复项
原因是:针对一个页面重复爬了3次,所以每个图片的item都有3个重复项(这里只是为了展示item去重效果,故意这样写的)
但是实际情况中,也可能出现第一个页面和后面的某一个页面的图片信息重复,所以才有必要针对itme进行去重整理。
既然tiem有重复的,当然应该过滤掉,那么如何过滤重复呢?
❶ 进入项目下的'pipelines.py',整理item去重集合:
from scrapy.exceptions import DropItem
class DuperFilerPipeline:
def __init__(self):
self.imgs_seen = set()
def process_item(self, item, spider):
if item['src'] in self.imgs_seen:
raise DropItem("Duplicate item found: %s % item")
else:
self.imgs_seen.add(item['src'])
print("item::",item)
return item
❷ 进入爬虫程序'bigimg.py'文件中进行item实例化封装
注意item实例化的缩进:如果写于for循环对齐写,只会输出一张图片信息,要写在for循环里面
import scrapy
from scrapy import Request
from daimg.items import DaimgItem
class BigimgSpider(scrapy.Spider):
name = "bigimg"
allowed_domains = ["xxx.com"]
#大图网url,第一页
start_urls = ["http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html"]
def start_requests(self):
#根据源码改编:
for i in range(3):
url = self.start_urls[0]
yield Request(url,dont_filter=True)
def parse(self, response):
#获取首页图片的图片名称和src图片源地址
li_list = response.xpath('/html/body/div[5]/ul/li')
for li in li_list:
title = li.xpath('./a/img/@title').extract_first()
src = li.xpath('./a/img/@src').extract_first()
# print(title,src)
#进行item实例化:
item = DaimgItem()
item["name"] = title
item["src"] = src
yield item
❸ 进入项目文件夹下的items.py进入修改配置:
❹ 进入settings.py打开配置:
19行(机器人协议)、27行(延迟)
30行(控制请求最大数量),我这里是python3.12版本,之前没打开它还是有重复项,其他python版本好像不用打开,具体可以自己试试。
同时,放开65~67行的itme_pipelines:并且保存类名一致
重新启动一下爬虫程序:控制台重新搜索就没有重复了
⑸ 全站爬虫(CrawlSpider)
在上面items去重的案例中,由于for循环编写姿势不对,没有实现全站100页爬取的需求
如果只是爬取前几页的图片,可以正确编写for循环的逻辑:
#手动全站查询:
import scrapy
from scrapy import Request
from daimg.items import DaimgItem
import re
class BigimgSpider(scrapy.Spider):
name = "bigimg"
allowed_domains = ["xxx.com"]
#大图网url,第一页
start_urls = ["http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html"]
def start_requests(self):
#拿到前5页的url:
for i in range(1,6):
url = self.start_urls[0]
new_url = re.sub("0-0-0-0-0_\d","0-0-0-0-0_"+str(i),url)
yield Request(new_url,dont_filter=True)
def parse(self, response):
#获取首页图片的图片名称和src图片源地址
li_list = response.xpath('/html/body/div[5]/ul/li')
for li in li_list:
title = li.xpath('./a/img/@title').extract_first()
src = li.xpath('./a/img/@src').extract_first()
# print(title,src)
#进行item实例化:
item = DaimgItem()
item["name"] = title
item["src"] = src
yield item
执行后:就得到了前5页的url
在之前Scrapy 的基本使用当中,spider 如果要重新发送请求的话,就需要自己解析页面,然后发送请求。
而 CrawlSpider 则可以通过设置url 条件自动发送请求。
常见的网站中,展示内容都是分页的,比如每页展示10条,总共有100页。
全站爬虫顾名思义就是,爬取对应网站的全部内容,如果有100页,就是一次爬完100页。
-------------->>>>>>
上面重写了for循环的构建,拿到不同页码的url。但是,如果想要爬取100页的全部内容,这样处理起来还是比较麻烦,相当于半自动化,只能手动构建循环逻辑。
所以全站爬虫的根本,解决的就是更加轻松的循环递归。
-------------->>>>>>
CrawlSpider是 Spider 的一个派生类,它需要配合下面两个组件进行使用:
LinkExtractors、Rule
LinkExtractors
CrawlSpider 与 spider 不同的是就在于下一次请的url不需要自己手动解析,而这一点则是通过 LinkExtractors 实现的。
简答来说LinkExtractors就是一个链接提取器
其中的参数如下:
● allow:允许的 url。所有满足这个正则表达式的 ur1 都会被提取
● deny:禁止的 url。所有满足这个正则表达式的 url 都不会被提取
● allow domains: 允许的域名。只有在这个里面指定的域名的 url 才会被提取
● deny domains:禁止的域名。所有在这个里面指定的域名的 url 都不会被提取
● restrict xpaths:严格的 xpath。和 allow 共同过滤链接
● tags: 接收一个标签或标签列表,提取标签内的列表,默认为[ a”,area"]● attrs: 接收一个属性或属性列表,提取指定属性内的链接,默认为[href']
Rule
LinkExtractors 需要传递到 Rule 类对象中才能发挥作用。
Rule是用来LinkExtractors提取器的具体对象
Rule 类常见的参数如下:
● link extractors:是一个LinkExtractor对象,用于定义需要提取的链接------------->>>>
● callback:从link extractor中没获取链接时参数所制定的值作为回调函数,该回调函数接受一个response作为起第-个参数注意:当编写爬虫规则是、避免使用parse作为回调函数。由于CrawlSpider使用parse方法来实现其逻辑,如果覆盖了parse方法CrawlSpider将会运行失败------------->>>>
● follow:是一个布尔值(boolean),制定了根据该规则从response提取的链接是偶需要跟进。如果cal1back为None,follow默认设置为True,否则默认为Flase。------------->>>>
● process links:指定该spider中那个的函数将会被调用,从lnk extractor中获取到链接列表是将会调用该函数。该方法主要用来过滤。------------->>>>
● process request:指定该spder中那个的函数将会被调用,该规则提取到每个request是都会调用该函数(用来过滤request)。
除了上述的这些差别,Crawlspider 和 spider 基本没有什么差别了。
获取全站url案例
还是沿用上面的大图网案例,使用Crawlspider来改写'bigimg.py'文件,实现全站爬虫:
先使用Crawlspider配合LinkExtractors、Rule两个组件,实现获取所有页码的url:
相当于对url进行深度递归,直到找到最后一个页码的url为止。
from scrapy.spiders import CrawlSpider,Rule
from scrapy.linkextractors import LinkExtractor
#修改类继承:CrawlSpider类是scrapy.Spider的子类
class BigimgSpider(CrawlSpider):
name = "bigimg"
#大图网url,第一页
start_urls = ["http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html"]
# LinkExtractor链接提取器:构建url链接页码规则,类似正则
link = LinkExtractor(allow=r"/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_\d+.html")
#封装提取器:基于Link构建一个Rule对象
rules = (Rule(link,callback="parse_item",follow=True),)
def parse_item(self, response):
print("response:",response)
启动程序:总共输出100个url
相比较上面的手动构建fou循环,简答很多,非常nice
注意细节:
rules = (Rule(link,callback="parse_item",follow=True),)这行代码中记得加个逗号,
不然会报错:builtins.TypeError: 'Rule' object is not iterable
因为rules
需要是一个可迭代的对象,但是在代码中rules
被定义为一个单独的Rule
对象,而不是一个元组或列表。
CrawlSpider如何工作的?
因为CrawlSpider继承了Spider,所以具有Spider的所有函数。
首先由 start_requests 对start_urls 中的每-个ur发起请求(make_requests_from_url),这个请求会被parse接收。
在Spider里面的parse:需要我们去定义(即数据解析逻辑代码)。
CrawSpider里面的parse:CrawSpider定义parse 去解析响应(self,parse_response(response,self.parse start url, cb kwargs={}, follow=True))parse_response根据有无 callback,follow和self.follow_links 执行不同的操作。
CrawlSpider注意事项
❶ crawlspider爬虫默认继承了CrawlSpider类。
❷ parse_item表示crawlspider爬虫的数据提取函数,在crawlspider爬虫中不能编写parse函数。parse函数是普通类独有的。
❸ rules是元组类型,小括号后面必须加逗号","
❹ scrapy genspider -t crawl 爬虫名`可以直接生成CrawlSpider结构的模板代码。
CrawlSpider实现全站图片爬取
基于上面获取全站ulr的基础之上,要再构建一个link2和对应的Rule,就能轻松实现全部图片下载
15行这里记得加上'deny_extensions=[''],',不然不会下载图片
import os
import requests
from scrapy.spiders import CrawlSpider,Rule
from scrapy.linkextractors import LinkExtractor
#修改类继承:CrawlSpider类是scrapy.Spider的子类
class BigimgSpider(CrawlSpider):
name = "bigimg"
#大图网url,第一页
start_urls = ["http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html"]
# LinkExtractor链接提取器:构建url链接页码规则,类似正则
link = LinkExtractor(allow=r"/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_\d+.html")
#定义提取img和src的提取规则:
link2 = LinkExtractor(tags=['img'],attrs=['src'],deny_extensions=[''],)
#封装提取器:基于Link构建一个Rule对象
rules = (
Rule(link,follow=True),#删除link1的解析,因为解析动作挪到了link2
Rule(link2,callback="parse_item",follow=False),
)
def parse_item(self, response):
title = os.path.basename(response.url)
img_path = "imgs/" + title + ".jpg"
with open(img_path, "wb") as f:
f.write(response.body)
启动程序后:下载的图片就进入img文件夹里了
⑹ 分布式爬虫(scrapy_redis)
scrapy_redis的一些概念详解
分布式爬虫是指将一个大型的爬虫任务分解成多个子任务,由多个爬虫进程或者多台机器同时执行的一种爬虫方式。
在分布式爬虫中,每个爬虫进程或者机器都具有独立的爬取能力,可以独立地爬取指定的网页或者网站,然后将爬取到的数据进行汇总和处理。
scrapy_redis架构解析
百度找的图片:
Scheduler
Scrapy改造了python本来的collection.deque(双向队列)形成了自己的Scrapy queue,但是Scrapy多个spider不能共享待爬取队列Scrapy queue, 即Scrapy本身不支持爬虫分布式。scrapy-redis 的解决是把这个Scrapy queue换成**redis数据库**(也是指redis队列),从同一个redis-server存放要爬取的request,便能让多个spider去同一个数据库里读取。
Scrapy中跟“待爬队列”直接相关的就是调度器Scheduler,它负责对新的request进行入列操作(加入Scrapy queue),取出下一个要爬取的request(从Scrapy queue中取出)等操作。它把待爬队列按照优先级建立了一个字典结构,比如:
优先级0 : 队列0
优先级1 : 队列1
优先级2 : 队列2---------------->>>>
然后根据request中的优先级,来决定该入哪个队列,出列时则按优先级较小的优先出列。
为了管理这个比较高级的队列字典,Scheduler需要提供一系列的方法。
但是原来的Scheduler已经无法使用,所以使用Scrapy-redis的scheduler组件。
Duplication Filter
Scrapy中用集合实现这个request去重功能,Scrapy中把已经发送的request指纹放入到一个集合中,把下一个request的指纹拿到集合中比对,如果该指纹存在于集合中,说明这个request发送过了,如果没有则继续操作。
在scrapy-redis中去重是由Duplication Filter组件来实现的,它通过redis的set 不重复的特性,巧妙的实现了Duplication Filter去重。scrapy-redis调度器从引擎接受request,将request的指纹存⼊redis的set检查是否重复,并将不重复的request push写⼊redis的 request queue。
引擎请求request(Spider发出的)时,调度器从redis的request queue队列⾥里根据优先级pop 出⼀个request 返回给引擎,引擎将此request发给spider处理。
Item Pipeline
引擎将(Spider返回的)爬取到的Item给Item Pipeline,scrapy-redis 的Item Pipeline将爬取到的 Item 存⼊redis的 items queue。
修改过Item Pipeline可以很方便的根据 key 从 items queue 提取item,从⽽实现items processes集群。
Base Spider
不再使用scrapy原有的Spider类,重写的RedisSpider继承了Spider和RedisMixin这两个类,RedisMixin是用来从redis读取url的类。
当我们生成一个Spider继承RedisSpider时,调用setup_redis函数,这个函数会去连接redis数据库,然后会设置signals(信号):一个是当spider空闲时候的signal,会调用spider_idle函数,这个函数调用schedule_next_request函数,保证spider是一直活着的状态,并且抛出DontCloseSpider异常。
一个是当抓到一个item时的signal,会调用item_scraped函数,这个函数会调用schedule_next_request函数,获取下一个request。
可以简单理解为:rides充当一个Master的共享服务器,里面有两个核心功能:
rides队列进行过滤重复值
将过滤后的数据以key:vlaue的形式传输到对应的存储位置
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。
------------>>>>
首先Slaver端从Master端拿任务(Request、url)进行数据抓取,Slaver抓取数据的同时,产生新任务的Request便提交给 Master 处理;Master端只有一个Redis数据库,负责将未处理的Request去重和任务分配,将处理后的Request加入待爬队列,并且存储爬取的数据。
------------>>>>
Scrapy-Redis默认使用的就是这种策略,我们实现起来很简单,因为任务调度等工作Scrapy-Redis都已经帮我们做好了,我们只需要继承RedisSpider、指定redis_key就行了。
------------>>>>
缺点:
Scrapy-Redis调度的任务是Request对象,里面信息量比较大(不仅包含url,还有callback函数、headers等信息),可能导致的结果就是会降低爬虫速度、而且会占用Redis大量的存储空间,所以如果要保证效率,那么就需要一定硬件水平。
分布式爬虫 VS单机爬虫的优势
❶ 高效性:分布式爬虫可以同时爬取多个网页或者网站,从而大大提高爬取速度和效率。
❷ 可扩展性:分布式爬虫可以根据需要动态地添加或者删除爬虫进程或者机器,从而适应不同的爬虫任务需求。
❸ 高稳定性:分布式爬虫可以通过任务分解和数据备份等机制保证任务的连续性和稳定性。
❹ 高可靠性:分布式爬虫可以通过多个 IP 地址和 User-Agent 等方式来防止 IP 被封锁、反爬等问题。
分布式爬虫的settings文件
● 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化:
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"------------>>>>
● 使用scrapy-redis组件自己的调度器:
SCHEDULER = "scrapy_redis.scheduler.Scheduler"------------>>>>
● 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据:
SCHEDULER_PERSIST = TrueITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400
}
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
# REDIS_ENCODING = ‘utf-8’
# REDIS_PARAMS = {"password":"123456"}
Scrapy-Redis,Redis的作用
❶ 任务调度和分配:Scrapy-Redis 使用 Redis 数据库实现了多台机器之间的任务调度和分配,通过 Redis 中的队列来存储待爬取的 URL、请求和爬取状态等信息,从而实现多个爬虫进程或者机器之间的任务分配和调度。
------------->>>>
❷ URL 管理:Scrapy-Redis 使用 Redis 数据库来存储待爬取的 URL 队列和已经爬取过的 URL 集合,从而避免了爬虫任务的重复执行和冲突问题。------------->>>>
❸ 数据存储:Scrapy-Redis 可以将爬取到的数据存储到 Redis 数据库中,从而实现分布式数据存储。这样可以避免单机存储造成的数据量过大和存储速度慢的问题。------------->>>>
❹ 状态共享:Scrapy-Redis 使用 Redis 数据库作为状态存储,从而实现多台机器之间的状态共享。当一个爬虫进程爬取了一个 URL 后,它可以将爬虫状态保存到 Redis 数据库中,其他进程就可以直接从 Redis 数据库中获取该状态。
scrapy_redis实战案例
这里还是使用大图网的案例,将代码改写为使用scrapy_redis来实现,步骤:
❶ 设置一个redis_key替代之前的start_urls
❷ RedisCrawlSpider替代之前的CrawlSpider,增加了rides的链接等操作;如果不是深度爬取,可以使用RedisSpider
❸ 配置settings文件,可以copy上面的‘分布式爬虫的settings文件’下的内容
另外,python环境中,需要先安装scrapy-redis:pip install scrapy-redis
import redis
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from fbs.items import FbsItem
from scrapy_redis.spiders import RedisCrawlSpider
#继承RedisSpider这个类
class DaimgSpider(RedisCrawlSpider):
name = "daimg"
#scrapy-redis不需要tart_urls,监听redis的某个key的获取起始地址
# start_urls = ["http://www.daimg.com/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87-0-0-0-0-0_1.html"]
#1、必须定义redis_key,即队列监听,redis_key这个变量名不能改
redis_key = "daimgQueue"
link = LinkExtractor(allow=r"/pic/%E7%BE%8E%E5%A5%B3%E5%9B%BE%E7%89%87")
rules = (Rule(link, callback="parse_item",follow=True),)
def parse_item(self, response):
# 获取首页图片的图片名称和src图片源地址
li_list = response.xpath('/html/body/div[5]/ul/li')
for li in li_list:
title = li.xpath('./a/img/@title').extract_first()
src = li.xpath('./a/img/@src').extract_first()
print(title, src)
# 进行item实例化:
item = FbsItem()
item["name"] = title
item["src"] = src
print("item::",item)
由于我本地没有安装reids可视化工具,这里无法展示效果