Spider Middleware 的使用
Spider Middleware,中文可以翻译为爬虫中间件,但我个人认为英文的叫法更为合适。它是处于 Spider 和 Engine 之间的处理模块。当 Downloader 生成 Response 之后,Response 会被发送给 Spider,在发送给 Spider 之前,Response 会首先经过 Spider Middleware 的处理,当 Spider 处理生成 Item 和 Request 之后,Item 和 Request 还会经过 Spider Middleware 的处理。
Spider Middleware 有如下 3 个作用。
-
Downloader 生成 Response 之后,Engine 会将其发送给 Spider 进行解析,在 Response 发送给 Spider 之前,可以借助 Spider Middleware 对 Response 进行处理。
-
Spider 生成 Request 之后会被发送至 Engine,然后 Request 会被转发到 Scheduler,在 Request 被发送给 Engine 之前,可以借助 Spider Middleware 对 Request 进行处理。
-
Spider 生成 Item 之后会被发送至 Engine,然后 Item 会被转发到 Item Pipeline,在 Item 被发送给 Engine 之前,可以借助 Spider Middleware 对 Item 进行处理。
总的来说,Spider Middleware 可以用来处理输入给 Spider 的 Response 和 Spider 输出的 Item 以及 Request。
使用说明
同样需要说明的是,Scrapy 其实已经提供了许多 Spider Middleware,与 Downloader Middleware 类似,它们被 SPIDER_MIDDLEWARES_BASE 变量所定义。
SPIDER_MIDDLEWARES_BASE 变量的内容如下:
{
"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_MIDDLEWARES_BASE 里定义的 Spider Middleware 是默认生效的,如果我们要自定义 Spider Middleware,可以和 Downloader Middleware 一样,创建 Spider Middleware 并将其加入 SPIDER_MIDDLEWARES。直接修改这个变量就可以添加自已定义的 Spider MiddleWare,以及禁用 SPIDER_MIDDLEWARES_BASE 里面定义的 Spider Middleware。
这些 Spider Middleware 的调用优先级和 Downloader Middleware 也是类似的,数字越小的 Spider Middleware 是越靠近 Engine 的,数字越大的 Spider Middleware 是越靠近 Spider 的。
核心方法
Scrapy 内置的 Spider Middleware 为 Scrapy 提供了基础的功能。如果我们想要扩展其功能,只需要实现集几个方法。
每个 Spider Middleware 都定义了以下一个或多个方法的类,核心方法有如下 4 个。
-
process_spider_input(response, spider)
-
process_spider_output(response, result, spider)
-
process_spider_exception(response, exception, spider)
-
process_start_requests(start_requests, spider)
只需要实现其中一个方法就可以定义一个 Spider Middleware,下面我们来看看这 4 个方法的详细用法。
process_spider_input(response, spider)
当 Response 通过 Spider Middleware 时,process_spider_input 方法被调用,处理该 Response。它有两个参数。
-
response:Response 对象,即被处理的 Response。
-
spider:Spider 对象,即该 Response 对应的 Spider 对象。
process_spider_input 应该返回 None 或者抛出一个异常。
-
如果它返回 None,Scrapy 会继续处理该 Response,调用所有其他的 Spider Middleware 直到 Spider 处理该 Response。
-
如果它抛出一个异常,Scrapy 不会调用任何其他 Spider Middleware 的 process_spider_input 方法,并调用 Request 的 errback 方法。errback 的输出将会以另一个方向被重新输人中间件,使用 process_spider_output 方法来处理,当其抛出异常时则调用 process_spider_exception 来处理。
process_spider_output(response, result, spider)
当 Spider 处理 Response 返回结果时,process_spider_output 方法被调用。它有 3 个参数。
-
response:Response 对象,即生成该输出的 Response。
-
result:包含 Request 或 Item 对象的可送代对象,即 Spider 返回的结果。
-
spider:Spider 对象,即结果对应的 Spider 对象。
process_spider_output 必须返回包含 Request 或 Item 对象的可迭代对象。
process_spider_exception(response, exception, spider)
当 Spider 或 Spider Middleware 的 process_spider_input 方法抛出异常时,process_spider_exception 方法被调用。它有 3 个参数。
-
response:Response 对象,即异常被抛出时被处理的 Response。
-
exception:Exception 对象,被抛出的异常。
-
spider:Spider 对象,即抛出该异常的 Spider 对象。
process_spider_exception 必须返回 None 或者一个(包含 Response 或 Item 对象的)可选代对象。
-
如果它返回 None,那么 Scrapy 将继续处理该异常,调用其他 Spider Middleware 中的 process_spider_exception 方法,直到所有 Spider Middleware 都被调用。
-
如果它返回一个可选代对象,则其他 Spider Middleware 的 process_spider_output 方法被调用,其他的 process_spider_exception 不会被调用。
process_start_requests(start_requests, spider)
process_start_requests 方法以 Spider 启动的 Request 为参数被调用,执行的过程类似于 process_spider_output,只不过它没有相关联的 Response 并且必须返回 Request。它有两个参数。
-
start_requests:包含 Request 的可迭代对象,即 Start Requests。
-
spider:Spider 对象,即 Start Requests 所属的 Spider。
process_start_requests 方法必须返回另一个包含 Request 对象的可迭代对象。
实战
上面的内容理解起来还是有点抽象,下面我们结合一个实战项目来加深一下对 Spider Middleware 的认识。
首先我们新建一个 Scrapy 项目叫作 scrapyspidermiddlewaredemo,命令如下所示:
scrapy startproject scrapyspidermiddlewaredemo
然后进人项目,新建一个 Spider。我们还是以 https://www.httpbin.org/ 为例来进行演示,命令如下所示:
scrapy genspider httpbin www.httpbin.org
命令执行完毕后,新建了一个名为 httpbin 的 Spider。接下来我们修改 start_url 为 https://www.httpbin.org/get,然后自定义 start_requests 方法,构造几个 Request,回调方法还是定义为 parse 方法。随后将 parse 方法添加一行打印输出,将 response 变量的 text 属性输出,这样我们便可以看到 Scrapy 发送的 Request 信息了。
修改 Spider 内容如下所示:
from scrapy import Spider, Request
class HttpbinSpider(Spider):
name = 'httpbin'
allowed_domains = ['httpbin.org']
start_url = 'https://httpbin.org/get'
def start_requests(self):
for i in range(5):
url = f'{self.start_url}?query={i}'
yield Request(url, callback=self.parse)
def parse(self, response):
print('Status', response.text)
接下来运行此 Spider,执行如下命令:
scrapy crawl httpbin
Scrapy 运行结果包含 Scrapy 发送的 Request 信息,内容如下所示:
{
"args": {
"query": "0"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_1) AppleWebKit/533.1 (KHTML, like Gecko) Chrome/15.0.818.0 Safari/533.1",
"X-Amzn-Trace-Id": "Root=1-65b9196e-6e206d1e711257466bb7bf89"
},
"origin": "27.38.238.130",
"url": "https://httpbin.org/get?query=0"
}
这里我们可以看到几个 Request 对应的 Response 的内容就被输出了,每个返回结果带有 args 参数,query 为 0~4。
另外我们可以定义一个 Item,4 个字段就是目标站点返回的字段,相关代码如下:
import scrapy
class DemoItem(scrapy.Item):
origin = scrapy.Field()
headers = scrapy.Field()
args = scrapy.Field()
url = scrapy.Field()
可以在 parse 方法中将返回的 Response 的内容转化为 DemoItem,将 parse 方法做如下修改:
def parse(self, response):
item = DemoItem(**response.json())
yield item
这样重新运行,最终 Spider 就会产生对应的 DemoItem 了,运行效果如下:
{
"args": {
"query": "4"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/60.0.840.0 Safari/534.1",
"X-Amzn-Trace-Id": "Root=1-65b91b61-46f54a7e73ddc0de6cae8b97"
},
"origin": "27.38.238.130",
"url": "https://httpbin.org/get?query=4"
}
可以看到原本 Response 的 JSON 数据就被转化为了 DemoItem 并返回。
接下来我们实现一个 Spider Middleware,看看如何实现 Response、Item、Request 的处理吧!
在 middlewares.py 中重新声明一个 CustomizeMiddleware 类,内容如下:
class CustomizeMiddleware(object):
def process_start_requests(self, start_requests, spider):
for request in start_requests:
url = request.url
url += '&name=germey'
request = request.replace(url=url)
yield request
这里实现了 process_start_requests 方法,它可以对 start_requests 表示的每个 Request 进行处理,我们首先获取了每个 Request 的 URL,然后在 URL 的后面又拼接上了另外一个 Query 参数,name 等于 germey,然后我们利用 request 的 replace 方法将 url 属性替换,这样就成功为 Request 赋值了新的 URL。
接着我们需要将此 CustomizeMiddleware 开启,在 settings.py 中进行如下的定义:
SPIDER_MIDDLEWARES = {
'scrapyspidermiddlewaredemo.middlewares.CustomizeMiddleware': 543,
}
这样我们就开启了 CustomizeMiddleware 这个 Spider Middleware 。
重新运行 Spider,这时候我们可以看到输出结果就变成了类似下面这样的结果:
https://www.httpbin.org/get?query=2&name=germey
{
"args": {'name': 'germey', 'query': '2'},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/60.0.840.0 Safari/534.1",
"X-Amzn-Trace-Id": "Root=1-65b91b61-46f54a7e73ddc0de6cae8b97"
},
"origin": "27.38.238.130",
"url": "https://httpbin.org/get?query=2&name=germey"
}
可以观察到 url 属性成功添加了 name=germey 的内容,这说明我们利用 Spider Middleware 成功改写了 Request。
除了改写 start_requests,我们还可以对 Response 和 Item 进行改写,比如对 Response 进行改写,我们可以尝试更改其状态码,在 CustomizeMiddleware 里面增加如下定义:
def process_spider_input(self, response, spider):
response.status = 201
def process_spider_output(self, response, result, spider):
for i in result:
if isinstance(i, DemoItem):
i['origin'] = None
yield i
这里我们定义了 process_spider_input 和 process_spider_output 方法,分别来处理 Spider 的输入和输出。对于 process_spider_input 方法来说,输入自然就是 Response 对象,所以第一个参数就是 response,我们在这里直接修改了状态码。对于 process_spider_output 方法来说,输出就是 Request 或 Item 了,但是这里二者是混合在一起的,作为 result 参数传递过来。result 是一个可迭代的对象,我们遍历了 result,然后判断了每个元素的类型,在这里使用 isinstance 方法进行判定:如果 i 是 DemoItem 类型,就把它的 origin 属性设置为空。当然这里还可以针对 Request 类型做类似的处理,此处略去。
另外在 parse 方法里面添加 Response 状态码的输出结果:
print('Status', response.status)
重新运行一下 Spider 可以看到输出结果类下面这样:
Status 201
2024-01-31 00:19:43 [scrapy.core.scraper] DEBUG: Scraped from <201 https://httpbin.org/get?query=1&name=germey>
{'args': {'name': 'germey', 'query': '1'},
'headers': {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'en',
'Host': 'httpbin.org',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac '
'OS X) AppleWebKit/535.2 (KHTML, like Gecko) '
'CriOS/53.0.800.0 Mobile/59O019 Safari/535.2',
'X-Amzn-Trace-Id': 'Root=1-65b9219e-4bde88046a170b8876c15b02'},
'origin': None,
'url': 'https://httpbin.org/get?query=1&name=germey'}
状态码变成了 201,Item 的 origin 字段变成了 None,证明 CustomizeMiddleware 对 Spider 输人的 Response 和输出的 Item 都实现了处理。
到这里,我们通过自定义 Spider Middleware 的方式,实现了对 Spider 输人的 Response 以及输出的 Request 和 Item 的处理。
另外在 Scrapy 中,还有几个内置的 Spider Middleware,我们简单介绍一下。
HttpErrorMiddleware
HttpErrorMiddleware 的主要作用是过滤我们需要忽略的 Response,比如状态码为 200~299 的会处理,500 以上的不会处理。其核心实现代码如下:
def __init__(self, settings):
self.handle_httpstatus_all = settings.getbool("HTTPERROR_ALLOW_ALL")
self.handle_httpstatus_list = settings.getlist("HTTPERROR_ALLOWED_CODES")
def process_spider_input(self, response, spider):
if 200 <= response.status < 300: # common case
return
meta = response.meta
if meta.get("handle_httpstatus_all", False):
return
if "handle_httpstatus_list" in meta:
allowed_statuses = meta["handle_httpstatus_list"]
elif self.handle_httpstatus_all:
return
else:
allowed_statuses = getattr(
spider, "handle_httpstatus_list", self.handle_httpstatus_list
)
if response.status in allowed_statuses:
return
raise HttpError(response, "Ignoring non-200 response")
可以看到它实现了 process_spider_input 方法,然后判断了状态码为 200~299
就直接返回,否则会根据 handle_httpstatus_all 和 handle_httpstatus_list 来进行处理。例如状态码在 handle_httpstatus_list 定义的范围内,就会直接处理,否则抛出 HttpError 异常。这也解释了为什么刚才我们把 Response 的状态码修改为 201 却依然能被正常处理的原因,如果我们修改为非 200~299 的状态码,就会抛出异常了。
另外,如果想要针对一些错误类型的状态码进行处理,可以修改 Spider 的 handle_httpstatus_list 属性,也可以修改 Request meta 的 handle_httpstatus_list 属性,还可以修改全局 setttings HTTPERROR_ALLOWED_CODES。
比如我们想要处理 404 状态码,可以进行如下设置:
HTTPERROR_ALLOWED_CODES = [404]
OffsiteMiddleware
OffsiteMiddleware 的主要作用是过滤不符合 allowed_domains 的 Request,Spider 里面定义的 allowed_domains 其实就是在这个 Spider Middleware 里生效的。其核心代码实现如下:
def process_spider_output(self, response, result, spider):
for x in result:
if isinstance(x, Request):
if x.dont_filter or self.should_follow(x, spider):
yield x
else:
domain = urlparse_cached(x).hostname
if domain and domain not in self.domains_seen:
self.domains_seen.add(domain)
logger.debug(
"Filtered offsite request to %(domain)r: %(request)s",
{"domain": domain, "request": request},
extra={"spider": spider},
)
self.stats.inc_value("offsite/domains", spider=spider)
self.stats.inc_value("offsite/filtered", spider=spider)
else:
yield x
可以看到,这里首先遍历了 result,然后判断了 Request 类型的元素并赋值为 x。然后根据 x 的 dontfilter、url 和 Spider 的 allowed_domains 进行了过滤,如果不符合 allowed_domains,就直接输出日志并不再返回 Request,只有符合要求的 Request 才会被返回并继续调用
UrlLengthMiddleware
UrlLengthMiddleware 的主要作用是根据 Request 的 URL 长度对 Request 进行过滤:如果 URL 的长度过长,此 Request 就会被忽略。其核心代码实现如下:
@classmethod
def from_settings(cls,settings):
maxlength = settings.getint('URLLENGTH_LIMIT')
def process_spider_output(self, response, result, spider):
def _filter(request):
if isinstance(request, Request) and len(request.url) > self.maxlength:
logger:debug("Ignoring link (url length >%(maxlength)d): %(url)s",
{'maxlength': self.maxlength,'url':request.url},
extra={'spider':spider})
return False
else:
return True
return(r for r in result or () if _filter(r))
可以看到,这里利用了 process_spider_output 对 result 里面的 Request 进行过滤,如果是 Request 类型并且 URL 长度超过最大限制,就会被过滤。我们可以从中了解到,如果想要根据 URL 的长度进行过滤,可以设置 URLLENGTH_LIMIT。
比如我们只想爬取 URL 长度小于 50 的页面,那么就可以进行如下设置:
URLLENGTH_LIMIT = 50
可见 Spider Middleware 能够非常灵活地对 Spider 的输人和输出进行处理,内置的一些 Spider Middleware 在某些场景下也发挥了重要作用。另外,还有一些其他的内置 Spider Middleware,就不在此一一赞述了,更多内容可以参考官方文档: https://docs.scrapy.org/en/latest/topics/spider-middleware.html#built-in-spider-middleware-reference