两大中间件:
1. 爬虫中间件: 位于爬虫与引擎之间, 只要工作是处理爬虫的输入requests和输出.(使用少)
2. 下载中间件: 位于引擎与下载器之间, 加代理头, 加头, 集成selenium.(使用多)
两个中间件都在scrapy项目的middlewares.py文件中, 使用前需要在settings.py中配置.
使用爬虫中间件需要先配置, 在使用.
# 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)
使用下载中间件需要先配置, 在使用.
# 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. 创建项目
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. 从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
# 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
# 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. 在爬虫脚本中集成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
在中间件中不允许直接修改request的url属性值.
如果修改了, 会报错
AttributeError(属性错误):Request.url 不可修改,
请改用 Request.replace() instead
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加密)
将值拿去集合中做比较.
"""
指纹的值太长, 当爬取的数据以亿为单位的时候占用的资源就很多.
bloomfilter 是一个通过多哈希函数映射在一张表的数据结构, 能快速判断一个元素是否在一个集合中,
具有姮好的空间和时效. (爬虫中常用于url去重.)
原理: bloomfilter开辟一个m位的bitArray(位数组), 开始虽有数据全部置0, 当一个元素过来时,
能过多个哈希函数(h1, h2, h3..)计算不同的哈希值,
并通过哈希值找到对应的bitArray下标, 将里面的值0, 置为1.
关于哈希函数, 他们计算出来的值必须在[0, m] 之中.
布隆过滤器它占用空间更少并且效率更高, 但是缺点是其返回的结果是概率性的, 而不是非常准确的.
理论情况下添加到集合中的元素越多, 误报的可能性就越大.
并且, 存放在布隆过滤器的数据不容易删除.
* 1. 安装依赖的包
pip install bitarray
* 2. 安装布隆过滤器
pip install pybloom_live
# 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
# 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
* 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'
把一个爬虫任务放在多太机器中取执行, 提高爬取效率.
关键: 共享队列.
原来scrapy的Scheduler维护的是本机的任务队列
(存放Request对象及其回调函数等信息),
+ 本机的去重队列(存放访问过的url地址)
所以实现分布式爬取的关键就是, 找一台专门的主机运行一个共享的队列(使用Redis)然后重写Scrapy的Scheduler到队列取Request, 并且去除重复的request请求.
总结:
1. 共享队列
2. 重写Scheduler, 让其无论去重,还是获取任务都是去访问共享队列
3. 为Scheduler定制去重规则(利用redis的集合类型)
* 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函数展示到终端, 直接查看数据即可.)
* 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/
————————————————
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
文章的段落全是代码块包裹的, 留言是为了避免文章提示质量低.
————————————————