Downloader Middleware 的使用
Downloader Middleware 即下载中间件。在 15.1 节我们已经提到过,它是处于 Scrapy 的 Engine 和 Downloader 之间的处理模块。在 Engine 把从 Scheduler 获取的 Request 发送给 Downloader 的过程中,以及 Downloader 把 Response 发送回 Engine 的过程中,Request 和 Response 都会经过 Downloader Middleware 的处理。
也就是说,Downloader Middleware 在整个架构中起作用的位置是以下两个。
-
Engine 从 Scheduler 获取 Request 发送给 Downloader,在 Request 被 Engine 发送给 Downloader 执行下载之前,Downloader Middleware 可以对 Request 进行修改。
-
Downloader 执行 Request 后生成 Response,在 Response 被 Engine 发送给 Spider 之前,也就是在 Resposne 被 Spider 解析之前,Downloder Middleware 可以对 Response 进行修改。
不要小看 Downloader Middleware,其实它在整个爬虫执行过程中能起到非常重要的作用,功能十分强大,修改 User-Agent,处理重定向、设置代理、失败重试,设置 Cookie 等功能都需要借助它来实现。
本节我们来了解一下 Downloader Middleware 的详细用法。
使用说明
需要说明的是,Scrapy 已经提供了许多 Downloader Middleware,比如负责失败重试、自动重定向等功能的 Downloader Middleware,它们被 DOWNLOADER_MIDDLEWARES_BASE 变量所定义。
DOWNLOADER_MIDDLEWARES_BASE 变量的内容如下所示:
DOWNLOADER_MIDDLEWARES_BASE = {
# Engine side
"scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware": 100,
"scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware": 300,
"scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware": 350,
"scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware": 400,
"scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
"scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware": 560,
"scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware": 580,
"scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware": 590,
"scrapy.downloadermiddlewares.redirect.RedirectMiddleware": 600,
"scrapy.downloadermiddlewares.cookies.CookiesMiddleware": 700,
"scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 750,
"scrapy.downloadermiddlewares.stats.DownloaderStats": 850,
"scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware": 900,
# Downloader side
}
这是一个字典格式,字典的键名是 Scrapy 内置的 Downloader Middleware 的名称,键值代表了调用的优先级,优先级是一个数字,数字越小代表越靠近 Engine,数字越大代表越靠近 Downloader。
在默认情况下,Scrapy 已经为我们开启了 DOWNLOADER_MIDDLEWARES_BASE 所定义的 DownloaderMiddleware,比如 RetryMiddleware 带有自动重试功能,RedirectMiddleware 带有自动处理重定向功能,这些功能默认都是开启的。
那 Downloader Middleware 里面究竟是怎么实现的呢?
其实每个 Downloader Middleware 都可以通过定义 process_request 和 process_response 方法来分别处理 Request 和 Response,被开启的 Downloader Middleware 的 process_request 方法和 process_response 方法会根据优先级被顺次调用。
由于 Request 是从 Engine 发送给 Downloader 的,并且优先级数字越小的 Downloader Middleware 越靠近 Engine,所以优先级数字越小的 Downloader Middleware 的 process_request 方法越先被调用。process_response 方法则相反,由于 Response 是由 Downloader 发送给 Engine 的,优先级数字越大的 Downloader Middleware 越靠近Downloader,所以优先级数字越大的 Downloader Middleware 的 proces_response 越先被调用。
如果我们想将自定义的 Downloader Middleware 添加到项目中,不要直接修改 DOWNLOADER_MIDDLEWARES_BASE 变量。Scrapy 提供了另外一个设置变量DOWNLOADER_MIDDLEWARES,我们直接修改这个变量就可以添加自已定义的 Downloader MiddleWare,以及禁用 DOWNLOADER_MIDDLEWARES_BASE 里面定义的 Downloader Middleware 了。
说了这么多可能比较抽象,下面我们具体来看一看 Downloader Middleware 的使用方法,然后结合案例来体会一下 Downloader Middleware 的使用方法。
核心方法
Scrapy 内置的 Downloader Middleware 为 Scrapy 提供了基础的功能,但在项目实战中,我们往往需要单独定义 Downloader Middleware 不用担心,这个过程非常简单,我们只需要实现几个方法。
每个 Downloader Middleware 都定义了一个或多个方法的类,核心的方法有如下 3 个:
-
process_request(request, spider)
-
process_response(request, response, spider)
-
process_exception(request, exception, spider)
我们只需要实现至少一个方法,就可以定义一个 Downloader Middleware。下面我们来看看这 3 个方法的详细用法。
process_request(request, spider)
Request 被 Engine 发送给 Downloader 之前,process_request 方法就会被调用,也就是在 Request 从 Scheduler 里被调度出来发送到 Downloader 下载执行之前,我们都可以用 process_request 方法对 Request 进行处理。
这个方法的返回值必须为 None、Response 对象、Request 对象三者之一,或者抛出 IgnoreRequest 异常。
process_request 方法的参数有两个。
-
request:Request 对象,即被处理的 Request。
-
spider:Spider 对象,即此 Request 对应的 Spider 对象。
返回类型不同,产生的效果也不同。下面归纳一下不同的返回情况。
-
当返回是 None 时,Scrapy 将继续处理该 Request,接着执行其他Downloader Middleware 的 process_request 方法,一直到 Downloader 把 Request 执行得到 Response 才结束。这个过程其实就是修改 Request 的过程,不同的 Downloader Middleware 按照设置的优先级顺序依次对 Request 进行修改,最后送至 Downloader 执行。
-
当返回为 Response 对象时,更低优先级的 Downloader Middleware 的 process_request 和 process_exception 方法就不会被继续调用,每个 Downloader Middleware 的 process_response 方法转而被依次调用。调用完毕后,直接将 Response 对象发送给 Spider 处理。
-
当返回为 Request 对象时,更低优先级的 Downloader Middleware 的 process_request 方法会停止执行。这个 Request 会重新放到调度队列里,其实它就是一个全新的 Request,等待被调度。如果被 Scheduler 调度了,那么所有的 Downloader Middleware 的 process_request 方法会被重新按照顺序执行。
-
如果抛出 IgnoreRequest 异常,则所有的 Downloader Middleware 的 process_exception 方法会依次执行。如果没有一个方法处理这个异常,那么 Request 的 errorback 方法就会回调。如果该异常还没有被处理:那么它便会被忽略。
process_response(request, response, spider)
Downloader 执行 Request 下载之后,会得到对应的 Response。Engine 便会将 Response 发送给 Spider 进行解析。在发送给 Spider 之前,我们都可以用 process_response 方法来对 Response 进行处理。process_response 方法的返回值必须为 Request 对象和 Response 对象两者之一,或者抛出 IgnoreRequest 异常。
process_response 方法的参数有 3 个。
-
request:Request 对象,即此 Response 对应的 Request。
-
response:Response 对象,即被处理的 Response。
-
spider:Spider 对象,即此 Response 对应的 Spider 对象。
下面对不同的返回情况做一下归纳。
-
当返回为 Request 对象时,更低优先级的 DownloaderMiddleware 的 process_response 方法不会继续调用。该 Request 对象会重新放到调度队列里等待被调度,相当于一个全新的 Request。然后,该 Request 会被 process_request 方法顺次处理。
-
当返回为 Response 对象时,更低优先级的 Downloader Middleware 的 process_response 方法会继续被调用,对该 Response 对象进行处理。
-
如果抛出 IgnoreRequest 异常,则 Request 的 errorback 方法会回调。如果该异常还没有被处理,那么它会被忽略。
process_exception(request, exception, spider)
当 Downloader 或 process_request 方法抛出异常时,例如抛出 IgnoreRequest 异常,process_exception 方法就会被调用。方法的返回值必须为 None、Response对象,Request对象三者之一。
process_exception 方法的参数有 3 个。
-
request:Request 对象,即产生异常的 Request。
-
exception:Exception 对象,即抛出的异常。
-
spdier:Spider 对象,即 Request 对应的 Spider。
下面归纳一下不同的返回值。
-
当返回为 None 时,更低优先级的 Downloader Middleware 的 process_exception 会被继续顺次调用,直到所有的方法都被调用完毕。
-
当返回为 Response 对象时,更低优先级的 Downloader Middleware 的 process_exception 方法不再被继续调用,每个 Downloader Middleware 的 process_response 方法转而被依次调用。
-
当返回为 Request 对象时,更低优先级的 Downloader Middleware 的 process_exception 也不再被继续调用,该 Request 对象会重新放到调度队列里面等待被调度,相当于一个全新的 Request,然后,该 Request 又会被 process_request 方法顺次处理。
以上内容便是这 3 个方法的详细使用逻辑。在使用它们之前,请先对这 3 个方法的返回值的处理情况有一个清晰认识。在自定义 Downloader Middleware 的时候,也一定要注意每个方法的返回类型。
项目实战
上面的内容确实有点难以理解,下面我们可以结合一个实战项目来加深对 Downloader Middleware 的认识。
首先让我们新建一个 Scrapy 项目,名叫作 scrapydownloadermiddlewaredemo,命令如下所示:
scrapy genspider httpbin www.httpbin.org
命令执行完毕后,就新建了一个 Spider,名为 httpbin。
接下来我们修改 start_urls 为:['https://www.httpbin.oig/get'] 随后将 parse 方法添加一行打印输出,将 response 变量的 text 属性输出,这样我们便可以看到 Scrapy 发送的 Request 信息了。
修改 Spider 内容如下所示:
import scrapy
class HttpbinSpider(scrapy.Spider):
name = 'httpbin'
allowed_domains = ['httpbin.org']
start_urls = ['http://httpbin.org/get']
def parse(self, response):
print('Text:', response.text)
print('Status Code:', response.status)
接下来运行此 Spider,执行如下命令:
scrapy crawl httpbin
Scrapy的运行结果包含 Scrapy 发送的 Request 信息,内容如下所示:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en",
"Cookie": "name=germey; age=26",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-65b900dc-5cabb99640cb16556250d3e3"
},
"origin": "27.38.238.130",
"url": "https://httpbin.org/get?offset=4"
}
我们观察一下 headers,Scrapy 发送的 Request 使用的 User-Agent 是 Scrapy/2.2.1(+https://scrapy.org),这其实是由 Scrapy 内置的 UserAgentMiddleware 设置的,UserAgentMiddleware 的源码如下所示:
"""Set User-Agent header per spider or use a default value from settings"""
from scrapy import signals
class UserAgentMiddleware:
"""This middleware allows spiders to override the user_agent"""
def __init__(self, user_agent="Scrapy"):
self.user_agent = user_agent
@classmethod
def from_crawler(cls, crawler):
o = cls(crawler.settings["USER_AGENT"])
crawler.signals.connect(o.spider_opened, signal=signals.spider_opened)
return o
def spider_opened(self, spider):
self.user_agent = getattr(spider, "user_agent", self.user_agent)
def process_request(self, request, spider):
if self.user_agent:
request.headers.setdefault(b"User-Agent", self.user_agent)
在 from_crawler 方法中,UserAgentMiddleware 首先尝试获取 settings 里面的 USER_AGENT,然后把 USER_AGENT 传递给 __init__
方法进行初始化,其参数就是 user_agent。如果没有传递 USER_AGENT 参数,就会默认将其设置为 Scrapy 字符串。我们新建的项目没有设置 USER_AGENT,所以这里的 user_agent 变量就是 Scrapy。
接下来,在 process_request 方法中,将 user_agent 变量设置为 headers 变量的一个属性,这样就成功设置了 User-Agent。因此,User-Agent 就是通过此 Downloader Middleware 的 process_request 方法设置的,这就是一个典型的 Downloader Middleware 的实例,我们再看一下 DOWNLOADER_MIDDLEWARES_BASE 的配置,UserAgentMiddleware 的配置如下:
{
"scrapy.downloadermiddlewares.useragent.UserAgentMiddleware": 500
}
可以看到,UserAgentMiddleware 被配置在了默认的 DOWNLOADER_MIDDLEWARES_BASE 里,优先级为 500,这样每次 Request 在被 Downloader 执行前都会被 UserAgentMiddleware 的 process_request 方法加上默认的 User-Agent。
但如果这个默认的 User-Agent 直接去请求目标网站,很容易被检测出来,我们需要将 User-Agent 修改为常见浏览器的 User-Agent。修改 User-Agent 可以有两种方式:
-
一是修改 settings 单面的 USER_AGENT 变量。
-
二是通过 Downloader Middleware 的 process_request 方法来修改。
第一种方法非常简单,我们只需要在 setting.py 里面加一行对 USER_AGENT 的定义即可:
USER_AGENT='Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2'
一般推荐使用此方法来进行设置。但是如果想设置得更灵活,比如设置随机的 User-Agent,那就需要借助 Downloader Middleware 了。所以接下来我们用 Downloader Middleware 实现一个随机 User-Agent 的设置。
在 middlewares.py 里面添加一个 RandomUserAgentMiddleware 类,代码如下所示:
import random
class RandomUserAgentMiddleware(object):
def __init__(self):
self.user_agents = [
'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)',
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2',
'Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1'
]
def process_request(self, request, spider):
request.headers['User-Agent'] = random.choice(self.user_agents)
我们首先在类的 __init__
方法中定义了 3 个不同的 User-Agent,并用一个列表来表示。接下来实现了 process_request 方法,它有一个参数 request,我们直接修改 request 的属性即可。在这里直接设置了 request 对象的 headers 属性的 User-Agent,设置内容是随机选择的 User-Agent,这样一个 Downloader Middleware 就写好了。
不过,要使之生效还需要去调用这个 Downloader Middleware。在 settings.py 中,将 DOWNLOADER_MIDDLEWARES 取消注释,并设置成如下内容:
DOWNLOADER_MIDDLEWARES = {
'scrapydownloadermiddlewaredemo.middlewares.RandomUserAgentMiddleware': 543
}
接下来我们重新运行 Spider,就可以看到 User-Agent 被成功修改为列表中所定义的随机的一个 User-Agent 了:
{
"args": {},
"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_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-65b900dc-5cabb99640cb16556250d3e3"
},
"origin": "27.38.238.130",
"url": "https://httpbin.org/get"
}
我们通过实现 Downloader Middleware 并利用 process_request 方法,成功设置了随机的 User-Agent。
另外我们还可以借助 Downloader Middleware 来设置代理。比如这里我有一个 HTTP 代理运行在 203.184.132.103:7890,如果我想使用此代理请求目标站点,可以通过定义一个 Downloader Middleware 来设置:
class ProxyMiddleware(object):
def process_request(self, request, spider):
request.meta['proxy'] = 'http://203.184.132.103:7890'
这里我们定义了一个 ProxyMiddleware,在它的 process_request 方法里面,修改了 request 的 meta 属性的 proxy 属性,赋值为 http://203.184.132.103:7890 ,这样就相当于设置了一个 HTTP 代理。
此代理并不一定是长期可用代理,你需要将其更换成你自己的可用 HTTP 代理,有关代理的具体获取方案可以参考本书前文代理的使用相关章节。 |
要使 ProxyMiddleware 生效,我们需要进一步启用这个 ProxyMiddleware,修改 DOWNLOADER_MIDDLEWARES 为如下内容:
DOWNLOADER_MIDDLEWARES = {
'scrapydownloadermiddlewaredemo.middlewares.RandomUserAgentMiddleware': 543,
'scrapydownloadermiddlewaredemo.middlewares.ChangeResponseMiddleware': 544,
}
这样我们就启用了两个自定义的 Downloader Middleware,执行优先级分别为 543 和 544 。RandomUserAgentMiddleware 的 process_request 方法会首先被调用,为 Request 赋值 User-Agent,随后 ProxyMiddleware 的 process_request 会被调用,为 Request 赋值 meta 的 proxy 属性。
重新运行,可以发现输出结果如下:
{
"args": {},
"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_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-65b900dc-5cabb99640cb16556250d3e3"
},
"origin": "27.38.238.130",
"url": "https://httpbin.org/get"
}
这里我们看到网站返回结果的 origin 字段就是代理的 IP,所以可以验证出:User-Agent 和代理的设置都生效了。至于代理为什么生效,是因为 Scrapy 对 meta 的 proxy 属性做了针对性处理,使得最终发送的 HTTP 请求启用我们配置的代理服务器,具体的处理逻辑可以查看 Scrapy 的 Downloader Middleware 和 Downloader 的源代码。
我们使用 process_request 对 Request 进行了修改,但刚才写的两个 Downloader Middleware 的 process_request 都没有返回值,即返回值为 None,这样一个个 Dowmloader Middleware 的 process_request 就会被顺次执行。
上文我们还提到了 process_request,如果返回其他形式的内容会怎样?比如 process_request 直接返回 Request。我们修改 ProxyMiddleware 试一下:
class ProxyMiddleware(object):
def process_request(self, request, spider):
request.meta['proxy'] = 'http://127.0.0.1:7890'
return request
这里我们在方法的最后加上了返回 Request 的逻辑,根据前文介绍的内容,如果 process_request 返回的是一个 Request,那么后续其他 Downloader Middleware 的 process_request 就不会被调用,这个 Request 会直接发送给 Engine 并加回到 Scheduler,等待下一次被调度。由于现在我们只发起了一个 Request,所以下一个被调度的 Request 还是这个 Request,然后会再次经过 process_request 方法处理,接着再次被返回,又一次被加回到 Scheduler,这样这个 Request 就不断从 Scheduler 取出来放回去,导致无限循环。
因此,这时候运行会得到一个递归错误的报错信息:
RecursionError:maximum recursion depth exceeded while calling a Python object
所以说,这一句简单的返回逻辑就整个改变了 Scrapy 爬虫的执行逻辑,一定要注意。
另外,如果我们返回一个 Response 会怎么办呢?根据前文所述,更低优先级的 Downloader Middleware 的 process_request 和 process_exception 方法就不会被继续调用,每个 Downloader Middleware 的 process_response 方法转而被依次调用。调用完毕后,直接将 Response 对象发送给 Spider 来处理。所以说,如果返回的是 Response,会直接被 process_response 处理完毕后发送给 Spider,而该 Request 就不会再经由 Downloader 执行下载了。
我们再尝试改写一下 ProxyMiddleware,修改如下:
from scrapy.http import HtmlResponse
class ProxyMiddleware(object):
def process_request(self, request, spider):
return HtmlResponse(
url=request.url,
status=200,
encoding='utf-8',
body='Test Downloader Middleware'
)
这里我们直接把代理设置的逻辑去掉了,返回了一个 HtmlResponse 对象,构造 HtmlResponse 对象时传入了 url、status、encoding、body 参数,其中直接赋给 body 一个字符串。
重新运行下,看看输出结果:
Test Downloader Middleware
这就是 parse 方法的输出结果,可以看到原本 Request 应该去请求 https://www.httpbin.org/get 得到返回结果,但是这里 Response 的内容直接变成了刚才我们所定义的 HtmlResponse 的内容,丢弃了原本的 Request,因此,如果我们在 process_request 方法中直接返回 Response 对象,原先的 Request 就会被直接去弃,该 Response 经过 process_response 方法处理后会直接传递给 Spider 解析。
到现在为止,我们应该能够明白 process_request 的用法及其不同的返回值所起到的作用了。
上面我们讲了 process_request 的用法,它是用来处理 Request 的,相应地,process_response 就是用来处理 Response 的了,我们再来看一下 process_response 的用法。
Downloader 对 Request 执行下载之后会得到 Response,随后 Engine 会将 Response 发送回 Spider 进行处理。但是在 Response 被发送给 Spider 之前,我们同样可以使用 process_response 方法对 Response 进行处理。
比如这里修改一下 Response 的状态码,添加一个 ChangeResponseMiddleware 的 Downloader Middleware,代码如下:
class ChangeResponseMiddleware(object):
def process_response(self, request, response, spider):
response.status = 201
return response
我们将 response 对象的 status 属性修改为 201,随后将 response 返回,这个被修改的 Response 就会被发送到 Spider。
我们再在 Spider 里面输出修改后的状态码,在 parse 方法中添加如下的输出语句:
print('Status Code:', response.status)
然后将 DOWNLOADER_MIDDLEWARES 修改为如下内容:
DOWNLOADER_MIDDLEWARES = {
'scrapydownloadermiddlewaredemo.middlewares.RandomUserAgentMiddleware': 543,
'scrapydownloadermiddlewaredemo.middlewares.ChangeResponseMiddleware': 544,
}
接着将 ProxyMiddleware 换成了 ChangeResponseMiddleware,重新运行,控制台输出了如下内容:
Status Code: 201
可以发现,Response 的状态码被成功修改了。因此如果要想对 Response 进行处理,就可以借助 process_response 方法。
当然 process_response 方法的不同返回值有不同的作用,如果返回 Request 对象,更低优先级的 Downloader Middleware 的 process_request 方法会停止执行。这个 Request 会重新放到调度队列里,其实它就是一个全新的 Request,等待被调度。感兴趣的话可以尝试一下。
另外还有一个 process_exception 方法,它是专门用来处理异常的方法。如果需要进行异常处理,我们可以调用此方法。不过这个方法的使用频率相对低一些,不在进行实例演示。