当前位置:网站首页>5.scrapy中间件&分布式爬虫
5.scrapy中间件&分布式爬虫
2022-08-04 01:25:00 【Python_21.】
1. scrapy中间件
两大中间件:
1. 爬虫中间件: 位于爬虫与引擎之间, 只要工作是处理爬虫的输入requests和输出.(使用少)
2. 下载中间件: 位于引擎与下载器之间, 加代理头, 加头, 集成selenium.(使用多)
两个中间件都在scrapy项目的middlewares.py文件中, 使用前需要在settings.py中配置.
1.1 爬虫中间件
使用爬虫中间件需要先配置, 在使用.
# settings.py
SPIDER_MIDDLEWARES = {
# 中间件类 : 数据(优先级)
'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
# middlewares.py
class CnblogsSpiderMiddleware:
""" 并非所有方法都需要定义。如果没有定义方法, scrapy 就好像蜘蛛中间件没有修改传递的对象一样 """
@classmethod
def from_crawler(cls, crawler):
# Scrapy 使用此方法来创建您的蜘蛛。
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
""" 调用通过蜘蛛中间件并进入蜘蛛的每个响应。 应该返回 None 或引发异常。 """
return None
def process_spider_output(self, response, result, spider):
""" 在处理完响应后,使用从 Spider 返回的结果调用。 必须返回一个可迭代的 Request 或 item 对象 """
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
""" 当蜘蛛或 process_spider_input() 方法(来自其他蜘蛛中间件)引发异常时调用。 应该返回 None 或一个可迭代的 Request 或 item 对象 """
pass
def process_start_requests(self, start_requests, spider):
""" 与蜘蛛的启动请求一起调用,与 process_spider_output() 方法类似, 只是它没有关联的响应。必须只返回请求(而不是项目)。 """
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
1.2下载中间件
使用下载中间件需要先配置, 在使用.
# settings.py
DOWNLOADER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}
class CnblogsDownloaderMiddleware:
""" 并非所有方法都需要定义。如果没有定义方法, scrapy 就好像下载器中间件不修改传递的对象一样。 """
@classmethod
def from_crawler(cls, crawler):
# Scrapy 使用他的方法来创建你的蜘蛛
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
""" 为通过下载器中间件的每个请求调用。downloader 中间件必须: - 返回 None:继续处理此请求 - 或返回 Response 对象 - 或返回 Request 对象 - 或引发 IgnoreRequest:将调用已安装的下载器中间件的 process_exception() 方法 """
return None
def process_response(self, request, response, spider):
""" 使用从下载器返回的响应调用。 必须要么; - 返回一个 Response 对象 - 返回一个 Request 对象 - 或引发 IgnoreRequest """
return response
def process_exception(self, request, exception, spider):
""" 当下载处理程序或 process_request()(来自其他下载器中间件) 引发异常时调用。 必须: - 返回无:继续处理此异常 - 返回响应对象:停止 process_exception() 链 - 返回请求对象:停止 process_exception() 链 """
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
1.3 创建测试环境
* 1. 创建项目
C:\Users\13600\Desktop\synchro\Project\test1
New Scrapy project 'test1', using template directory 'c:\program\python38\lib\site-packages\scrapy\templates\project', created in:
C:\Users\13600\Desktop\synchro\Project\test1
You can start your first spider with:
cd C:\Users\13600\Desktop\synchro\Project\test1
scrapy genspider example example.com
* 2. 使用pycharm打开项目
* 3. 创建爬虫脚本
PS C:\Users\13600\Desktop\synchro\Project\test1> scrapy genspider cnblog www.cnblogs.com
Created spider 'cnblog' using template 'basic' in module:
test1.spiders.cnblog
* 4. 在项目目录下新建启动脚本文件main.py
# main.py
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'cnblog'])
* 5. 在配置文件中配置日志级别
# settings.py
LOG_LEVEL = 'ERROR'
* 6 . 在settings.py中配置中间件参数.
# settings.py
DOWNLOADER_MIDDLEWARES = {
'test1.middlewares.Test1DownloaderMiddleware': 543,
}
* 7. 在下载中间值中间添加测试代码
# middlewares.py
class Test1DownloaderMiddleware:
...
# 请求处理
def process_request(self, request, spider):
print(request.url)
return None
# settings.py中没有关闭爬虫协议, 爬取四次:
""" http://www.cnblogs.com/robots.txt https://www.cnblogs.com/robots.txt http://www.cnblogs.com/ https://www.cnblogs.com/ 爬虫先会爬取爬虫协议, 如果http协议的请求获取不到数据会加上s再次发生请求. """
* 8. 关闭遵循爬虫协议
# settings.py
ROBOTSTXT_OBEY = False
* 9. 修改爬虫脚本的类中start_urls属性, 改为https协议.
# cnblog.py
start_urls = ['https://www.cnblogs.com/']
1.4 更换随机请求头
* 1. 从request对象中headers属性中获取请求头
获取的属性值是一个字段套列表
# middlewares.py
class Test1DownloaderMiddleware:
...
# 请求处理
def process_request(self, request, spider):
print(request.headers)
print(request.headers['User-Agent'])
return None
""" { b'Accept': [b'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8'], b'Accept-Language': [b'en'], b'User-Agent': [b'Scrapy/2.6.2 (+https://scrapy.org)'] } b'Scrapy/2.6.2 (+https://scrapy.org)' User-Agent 为 Scrapy/2.6.2 ... 直接暴露了马脚 """
* 2. 使用fake_useragent模块随机生成User-Agent字符串.
pip install fake_useragent
# middlewares.py
class Test1DownloaderMiddleware:
...
# 请求处理
def process_request(self, request, spider):
from fake_useragent import UserAgent
request.headers['User-Agent'] = UserAgent().random
return None
1.5 添加随机cookie值
# middlewares.py
class Test1DownloaderMiddleware:
...
# 请求处理
def process_request(self, request, spider):
from random import randint
# cookie池
cookie_list = [{
'username': 'xx'}, {
'username': 'oo'}, ...]
request.cookie = cookie_list[randint(0, y)]
return None
1.6 添加代理IP
# middlewares.py
class Test1DownloaderMiddleware:
# 请求处理
def process_request(self, request, spider):
print(request.meta)
# {'download_timeout': 180.0} 默认只有超时时间
# 代理ip在meta属性中添加一个key为proxy的字典.
# (代理有问题会重试发送请求)
request.meta['proxy'] = 'https://ip:端口'
return None
1.7 集成selenium
流程(当次爬虫运行, 都使用同一个流浪器对象, 只是在中间件打开不同的地址):
1. 在爬虫脚本中集成selenium, 先生成一个浏览器对象,
2. 在下载中间件中请求方法使用
3. 在爬虫脚本中关闭浏览器对象
* 1. 将chromedriver.exe谷歌浏览器控制插件复制到scrapy框架的项目目录下.
* 2. 在爬虫脚本总生成浏览器对象
import scrapy
class CnblogSpider(scrapy.Spider):
name = 'cnblog'
allowed_domains = ['www.cnblogs.com']
start_urls = ['https://www.cnblogs.com/']
# 集成selenium
from selenium import webdriver
bro = webdriver.Chrome(executable_path='chromedriver.exe')
# 解析数据
def parse(self, response):
print(response.text)
# close方法在爬虫脚本结束时执行
def close(self, reason):
# 关闭浏览器对象
self.bro.close()
* 3. 在下载中间中使用浏览器对象
class Test1DownloaderMiddleware:
...
# 请求处理
def process_request(self, request, spider):
spider.bro.get(request.url)
spider.bro.implicitly_wait(10)
# print(spider.bro.page_source)
# 在这里获取数据需要返回response对象而不是None
# 内置封装了一个HtmlResponse对象用于返回
from scrapy.http import HtmlResponse
# 这个HtmlResponse则被爬虫脚本的response接口, HtmlResponse需要的参数(url, 数据, 请求对象)
# body的数据需要时解码在后面添加.encode('utf-8')
response = HtmlResponse(request.url, body=spider.bro.page_source.encode('utf-8'), request=request)
return response
1.8 注意事项
在中间件中不允许直接修改request的url属性值.
如果修改了, 会报错
AttributeError(属性错误):Request.url 不可修改,
请改用 Request.replace() instead
2. 去重源码
scrapy内置去重功能, 已经取过的url不会再次爬取.
在配置文件settings.py中配置去重使用的类.
# from scrapy.dupefilters import RFPDupeFilter
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
# 去重类 继承BaseDupeFilter
class RFPDupeFilter(BaseDupeFilter):
...
def request_seen(self, request: Request) -> bool:
# yield request, 经过一些算法, 得到fp(指纹)
fp = self.request_fingerprint(request)
# 如果fp在集合中则不继续爬取
if fp in self.fingerprints:
return True
# 将fp添加到集合中
self.fingerprints.add(fp)
# 写入文件
if self.file:
self.file.write(fp + '\n')
return False
request_fingerprint函数的使用
在项目目录下新建一个py文件用于测试.
from scrapy.utils.request import request_fingerprint
from scrapy import Request
# 先生成两个request对象
url_1 = Request('https://www.baidu.con/xx?name=kid&age=18')
url_2 = Request('https://www.baidu.con/xx?age=18&name=kid')
fingerprint_1 = request_fingerprint(url_1)
fingerprint_2 = request_fingerprint(url_2)
print(fingerprint_1) # d3625990212837cb7ef7a02c4ccd8859daa24b82
print(fingerprint_2) # d3625990212837cb7ef7a02c4ccd8859daa24b82
""" 参数顺序问题 ?name=kid&age=18 ?age=18&name=kid 分隔之后得到的数据会按字母排序, 计算出一个指纹(类型MD5加密) 将值拿去集合中做比较. """
指纹的值太长, 当爬取的数据以亿为单位的时候占用的资源就很多.
3. 布隆过滤器
3.1 介绍
bloomfilter 是一个通过多哈希函数映射在一张表的数据结构, 能快速判断一个元素是否在一个集合中,
具有姮好的空间和时效. (爬虫中常用于url去重.)
原理: bloomfilter开辟一个m位的bitArray(位数组), 开始虽有数据全部置0, 当一个元素过来时,
能过多个哈希函数(h1, h2, h3..)计算不同的哈希值,
并通过哈希值找到对应的bitArray下标, 将里面的值0, 置为1.
关于哈希函数, 他们计算出来的值必须在[0, m] 之中.
布隆过滤器它占用空间更少并且效率更高, 但是缺点是其返回的结果是概率性的, 而不是非常准确的.
理论情况下添加到集合中的元素越多, 误报的可能性就越大.
并且, 存放在布隆过滤器的数据不容易删除.
3.2安装模块
* 1. 安装依赖的包
pip install bitarray
* 2. 安装布隆过滤器
pip install pybloom_live
3.3 固定长度
# BloomFilter 固定长度
from pybloom_live import BloomFilter
# 容量
bf = BloomFilter(capacity=1000)
# 测试url
url_1 = 'https://www.baidu.com'
url_2 = 'https://cnblogs.com'
# 将url添加到过滤器中
bf.add(url_1)
print(url_1 in bf) # True
print(url_2 in bf) # False
3.4 自动扩量
# ScalableBloomFilter 自动扩量
from pybloom_live import ScalableBloomFilter
""" initial_capacity 初始容量 error_rate 错误率 mode 模式, ScalableBloomFilter.LARGE_SET_GROWTH 大规模增长 """
bloom = ScalableBloomFilter(
initial_capacity=100,
error_rate=0.001,
mode=ScalableBloomFilter.LARGE_SET_GROWTH
)
# 测试url
url_1 = 'https://www.baidu.com'
url_2 = 'https://cnblogs.com'
# 将url添加到bloom过滤中
bloom.add(url_1)
print(url_1 in bloom) # True
print(url_2 in bloom) # False
4. 自定义去重规则
* 1. 在项目下目录下新建py文件bloom
from scrapy.dupefilters import BaseDupeFilter
from pybloom_live import ScalableBloomFilter
# 自定义去重继承BaseDupeFilter,
# 模仿自定义的写法,
# 在__init__ 中生成一个布隆过滤器
# 重写request_seen方法
class CustomDeduplication(BaseDupeFilter):
def __init__(self):
self.bloom = ScalableBloomFilter(
initial_capacity=100,
error_rate=0.001,
mode=ScalableBloomFilter.LARGE_SET_GROWTH
)
def request_seen(self, request):
# 从request中获取出url
url = request.url
if url in self.bloom:
return True
self.bloom.add(url)
* 2. 配置文件中配置DUPEFILTER_CLASS属性, 使用自定义的去重类.
DUPEFILTER_CLASS = 'test1.bloom_deduplication.CustomDeduplication'
5. 分布式爬虫
5.1 介绍
把一个爬虫任务放在多太机器中取执行, 提高爬取效率.
关键: 共享队列.
原来scrapy的Scheduler维护的是本机的任务队列
(存放Request对象及其回调函数等信息),
+ 本机的去重队列(存放访问过的url地址)
所以实现分布式爬取的关键就是, 找一台专门的主机运行一个共享的队列(使用Redis)然后重写Scrapy的Scheduler到队列取Request, 并且去除重复的request请求.
总结:
1. 共享队列
2. 重写Scheduler, 让其无论去重,还是获取任务都是去访问共享队列
3. 为Scheduler定制去重规则(利用redis的集合类型)
5.2 分布式爬取案例
* 1. 创建scrapy项目
命令: scrapy startproject cnblogs_distributed C:\Users\13600\Desktop\synchro\Project\cnblogs_distributed
New Scrapy project 'cnblogs_distributed', using template directory 'c:\program\python38\lib\site-packages\scrapy\templates\project', created in:
C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed
You can start your first spider with:
cd C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed
scrapy genspider example example.com
C:\Users\13600\Desktop>
* 2. 使用PyCharm打开scrapy项目并创建爬虫脚本(爬虫脚本名称与项目名不能重复)
命令: scrapy genspider cnblogs www.cnblogs.com
PS C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed\cnblogs_distributed> scrapy genspider cnblogs www.cnblogs.com
Created spider 'cnblogs' using template 'basic' in module:
cnblogs_distributed.spiders.cnblogs
PS C:\Users\13600\Desktop\synchro\Project\cnlogs_distributed\cnblogs_distributed>
* 3. 安装scrapy_redis模块
命令: pip install scrapy_redis
* 4. 在项目目录下创建运行爬虫脚本主程序main.py
# main.py
from scrapy.cmdline import execute
execute(['scrapy', 'crawl', 'cnblogs'])
* 5. 修改爬虫配置文件
# 不遵循爬虫协议
ROBOTSTXT_OBEY = False
# 展示错误日志
LOG_LEVEL = 'ERROR'
# 全局USER_AGENT
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36 Edg/103.0.1264.71'
# 分布式爬虫的配置
# redis的连接(不写默认也是使用这个)
# REDIS_HOST = 'localhost' # 主机名
# REDIS_PORT = 6379 # 端口
# 使用scrapy-redis的去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis的Scheduler
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化的可以配置,也可以不配置
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 299
}
* 6. 在item.py中创建item对象.
# item.py
class CnblogsItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
article_url = scrapy.Field()
summary = scrapy.Field()
content = scrapy.Field()
* 7. 爬虫脚本程序
import scrapy
from scrapy import Request
# 使用RedisSpider
from scrapy_redis.spiders import RedisSpider
# 继承 RedisSpider
class CnblogsSpider(RedisSpider):
name = 'cnblogs'
allowed_domains = ['www.cnblogs.com']
# 指定Redis中集合的key名, key=存放不重复request字符串的集合
redis_key = 'myspider:start_urls'
def parse(self, response):
# 获取item对象
from items import CnblogsItem
item = CnblogsItem()
# 获取所有的article标签
article_list = response.css('article.post-item')
# 遍历article标签
for article in article_list:
# 获取标签
title = article.css('a.post-item-title::text').extract_first()
item['title'] = title
# 获取文章链接
article_url = article.css('a.post-item-title::attr(href)').extract_first()
item['article_url'] = article_url
# 获取文章摘要
summary = article.css('p.post-item-summary::text')[-1].extract().strip()
item['summary'] = summary
yield Request(article_url, callback=self.parse_detail, meta={
'item': item})
def parse_detail(self, response, **kwargs):
# 从response中获取出item对象
item = response.meta.get('item')
# 获取到html标签的文档, 不然下载再来就是没有排版的文字.
content = response.css('#cnblogs_post_body').extract_first()
item['content'] = content
# 将数据返回
yield item
redis_key = 'myspider:start_urls' 多个机器使用一个起始地址,
往redis中写入起始地址后放入, 三台机器谁先抢到地址, 谁就先执行任务,爬取这个地址
之后返回一堆地址放入起始地址中, 三台机器再抢, 抢到一个执行一个...
* 8. 在scrapy的__init__.py下将项目路径添加到环境变量中
# __init__.py
import os
import sys
# 将项目路径添加到环境变量中
BASE_PATH = os.path.dirname(__file__)
sys.path.append(BASE_PATH)
* 9. 启动程序
默认使用本地的redis, 无须配置
模拟三台机器运行分布式爬虫, 开三个终端, 启动三个爬虫程序
一个进程算一台机器.
命令: scrapy crawl cnblogs
* 10. 往redis中写入起始地址
127.0.0.1:6379> lpush myspider:start_urls https://www.cnblogs.com/
启动之后开始爬取数据(信息没有展示到print函数展示到终端, 直接查看数据即可.)
5.3 总结
* 1. pip3 install scrapy-redis
* 2. 原来继承Spider,现在继承RedisSpider
* 3. 不能写start_urls = ['https:/www.cnblogs.com/']
需要写redis_key = 'myspider:start_urls'
* 4. setting中配置↓
# redis的连接
# 主机名
REDIS_HOST = 'localhost'
# 端口
REDIS_PORT = 6379
# 使用scrapy-redis的去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis的Scheduler
# 分布式爬虫的配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化的可以配置,也可以不配置
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 299
}
* 5. 使用cmd命令启动scrapy项目, 将项目地址添加到环境变量中, 否则, scrapy中的模块也能提示找不到.
# scrapy项目的__init__.py
import os
import sys
# 将项目路径添加到环境变量中
BASE_PATH = os.path.dirname(__file__)
sys.path.append(BASE_PATH)
* 6. redis中为myspider:start_urls插入一个起始地址
lpush myspider:start_urls https://www.cnblogs.com/
边栏推荐
- VR panorama shooting online exhibition hall, 3D panorama brings you an immersive experience
- GNSS文章汇总
- JS 保姆级贴心,从零教你手写实现一个防抖debounce方法
- C 学生管理系统_分析
- typescript56 - generic interface
- typescript58 - generic classes
- Is there any jdbc link to Youxuan database documentation and examples?
- js函数防抖和函数节流及其使用场景
- Electronics manufacturing enterprise deployment WMS what are the benefits of warehouse management system
- OpenCV如何实现Sobel边缘检测
猜你喜欢
随机推荐
GNSS[0]- Topic
【虚拟化生态平台】虚拟化平台esxi挂载USB硬盘
KunlunBase 1.0 发布了!
C 学生管理系统_分析
typescript52-简化泛型函数调用
无代码7月热讯 | 微软首推数字联络中心平台;全球黑客马拉松...
XSS-绕过for循环过滤
简单的线性表的顺序表示实现,以及线性表的链式表示和实现、带头节点的单向链表,C语言简单实现一些基本功能
Android interview questions and answer analysis of major factories in the first half of 2022 (continuously updated...)
typescript53-泛型约束
【日志框架】
js中常用的几种遍历处理数据的方法梳理
nodejs切换版本使用(不需要卸载重装)
【无标题】
Vant3—— 点击对应的name名称跳转到下一页对应的tab栏的name的位置
Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
outputBufferIndex = mDecode.dequeueOutputBuffer(bufferInfo, 0) 一直返回为-1
nodejs 安装多版本 版本切换
114. 如何通过单步调试的方式找到引起 Fiori Launchpad 路由错误的原因
LDO investigation