Scrapy Spider中间件,你学会了吗?本篇博客有一案例
本篇博客补充一下 scrapy 中的 SpiderMiddlerware 相关用法。
scrapy 架构知识补充
在 scrapy 中所有的中间件都被当做类处理(管理这些中间件的类是 MiddlerwareManager),其大概可以分为四种。
DownloaderMiddlerware
:下载中间件,其中包括方法process_request()
,process_response()
,process_exception()
;SpiderMiddlerware
:爬虫中间件,其中包括方法process_spider_input()
,process_spider_output()
,process_spider_exception()
,process_start_requests()
;ItemPipeline
:数据管道,包括process_item()
,open_spider()
,close_spider()
;Extension
:其它扩展。
其中各部分被使用的场景依次如下:
DownloaderMiddlerware
:执行请求响应相关问题,例如修改用户代理;SpiderMiddlerware
:修改或删除特定请求;ItemPipeline
:修改或保存数据Extension
:其它扩展。
SpiderMiddlerware
SpiderMiddlerware
是一个钩子框架,其存在如下三个作用。
Downloader
产生Response
之后,可以使用;Spider
产生Request
之后,可以使用;Spider
产生Item
之后,可以使用。
这些中间件都存在 4 个核心方法,各个方法被调用的时间点如下所示:
process_spider_input(response, spider)
:当 Response 对象被 SpideMiddleware 处理时,process_spider_input()
方法被调用,控制响应内容;process_spider_output(response, result, spider)
:当 Spider 处理 Response 对象返回结果时,process_spider_output()
被调用,控制响应输出;process_spider_exception(response, exception, spider)
:当 Spider 或 SpideMiddleware 的process_spider_input()
方法抛出异常时,process_spider_exception()
方法被调用,异常捕捉;process_start_requests(start_requests, spider)
:当 Spider 产生 Request 请求时被调用,执行类似process_spider_output()
。
现在,你已经了解了核心方法,并大概知道了基本的使用位置,通过这些内容可以辅助我们理解 SpideMiddleware,下面通过代码进行测试。
在 middlewares.py
文件中的 CncnSpiderMiddleware
类内部,都增加输出函数,然后运行 scrapy 爬虫,获得如下输出。
可以看到 spider_opened()
方法被优先调用,然后依次是 _requests
,_spider_input
,_spider_ouput
process_start_requests(start_requests, spider)
当 spider 运行到 start_requests()
方法时,SpiderMiddlerware 调用 process_start_requests()
方法,在 start_requests
参数中并且必须返回另一个可迭代的 Request 对象,默认代码如下所示:
for r in start_requests:
yield r
process_spider_input(response, spider)
每个进入 SpiderMiddlerware 的 Response 响应对象,都会调用此方法,process_spider_input()
返回 None 或抛出异常,例如修改响应对象的状态码。
def process_spider_input(self, response, spider):
print("----process_spider_input--- 被调用")
response.status = 201
return None
process_spider_output(response, result, spider)
在处理完响应对象之后,使用 Spider 返回的结果调用此方法,process_spider_output()
可返回可迭代的 Request 或者 Item 对象。
当你了解 SpiderMiddlerware 之后,会发现其功能与 DownloaderMiddlerware 非常相似,具体场景可能还需要打磨。
系统内置了一些 SpiderMiddlerware ,在 default_settings.py
文件中可进行查阅。
SPIDER_MIDDLEWARES_BASE = {
# Engine side
'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware': 50,
'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': 500,
'scrapy.spidermiddlewares.referer.RefererMiddleware': 700,
'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800,
'scrapy.spidermiddlewares.depth.DepthMiddleware': 900,
# Spider side
}
DepthMiddleware
限制爬取深度。
HttpErrorMiddleware
过滤出所有失败/错误的 Response
。
OffsiteMiddleware
过滤出所有域名不在 spider
属性 allowed_domains
的 Request
。
RefererMiddleware
用于填充 Request
请求头的 Referer
属性。
UrlLengthMiddleware
过滤出 URL
长度比 URLLENGTH_LIMIT
长的 Request
。
scrapy 爬虫时间
又到了编写爬虫代码的时刻了,本次目标站点:中国社区网,该类网站数据不能在本地存储哦,爬取完毕就删除。
由于网站设计 3 层数据,所以代码嵌套逻辑比较多,cncn.py
文件代码如下,其中 parse
方法获取省份,parse_c
获取城市,parse_s
获取街道,这里如果城市中还分区,省略了各个区县,当然你可以自己扩展出来。(其实这个地方最后一个层级我偷懒了,没有写),重点学习数据在各个 Request 之间的传递。
import scrapy
from cncn.items import CncnItem
from urllib import parse
import json
class CnSpider(scrapy.Spider):
name = 'cn'
allowed_domains = ['cncn.org.cn']
start_urls = ['http://www.cncn.org.cn/map/']
def parse(self, response):
lis = response.css('ul.sub_maps li')
for p in lis:
p_name = p.css('a::text').get()
p_url_id = p.css('a::attr(href)').re_first('pid=(\d+)')
yield scrapy.Request(url=f'http://cms.cncn.org.cn/api/map_province_index.php?pid={p_url_id}&callback=',
meta={'p_name': p_name},
callback=self.parse_c)
def parse_c(self, response):
p_name = response.meta['p_name']
map_list = json.loads(response.text)
cs = map_list["map_list"][0]["province_items"]
for c in cs.values():
c_name = c["city_name"]
c_id = c["city_id"]
yield scrapy.Request(url=f'http://cms.cncn.org.cn/api/map_city_index.php?cid={c_id}&callback=',
meta={'p_name': p_name, 'c_name': c_name},
callback=self.parse_s)
def parse_s(self, response):
p_name = response.meta['p_name']
c_name = response.meta['c_name']
map_list = json.loads(response.text)
map_list = map_list["map_list"][0]
city_items = None
if "city_items" in map_list:
city_items = map_list["city_items"]
if city_items is not None:
# 迭代城市
for d in city_items.values():
# 如果区下面有街道,获取街道数据
if "district_items" in d and len(d["district_items"]) > 0:
for sub_d in d["district_items"]:
d_name = sub_d["community_name"]
# 存储数据
item = CncnItem()
item["p_name"] = p_name
item["c_name"] = c_name
item["d_name"] = d_name
print(item)
yield item
else:
return None
数据保存到 CSV 文件中,如下所示。
写在后面
今天是持续写作的第 255 / 365 天。
期待 关注,点赞、评论、收藏。
- 点赞
- 收藏
- 关注作者
评论(0)