scrapy爬虫框架(四)Downloader Middleware的使用
Downloader Middleware是处于Engine和Downloader之间的模块,其重要作用就是处理schduler调度器发送到Engine的Request和经过Downloader响应后的response返回至Engine过程中的处理。如图所示:
也就是说,Downloader Middlerware在整个架构中起到作用的位置是以下两个:
- Engine从Scheduler获取Request发送给Downloader,在Request被Engine发送给Downloader执行下载之前,Downloader Middleware 可以对Request进行修改。
- Downloader执行Request后生成Response,在Response被Engine发送给Spider之前,也就是在Response被Spider解析之前,Downloader Middleware可以对Response进行修改。
Downloader Middlerware在整个爬虫执行过程中起到非常重要的作用,功能十分强大,可以修改User-Agent、处理重定向、设置代理、失败重试、设置Cookie等功能都需要借助它来实现。
接下来我们看一下其详细用法。
一、简介
scrapy框架中的Downloader Middlerware已经存在了很多,比如负责失败重试、自动重定向等功能的Downloader Middlerware,它们被DOWNLOADER_MIDDLEWARES_BASE变量所定义。
DOWNLOADER_MIDDLEWARES_BASE变量的具体内容如下所示:
{
'scrapy.downloadermiddlerwares.robotstxt.RobotsTxtMiddleware': 100,
'scrapy.downloadermiddlerwares.httpauth.HttpAuthMiddleware': 300,
'scrapy.downloadermiddlerwares.downloadtimeout.DownloadTimeoutMiddleware': 350,
'scrapy.downloadermiddlerwares.defaultheaders.DefaultHeadersMiddleware': 400,
'scrapy.downloadermiddlerwares.useragent.UserAgentMiddleware': 500,
'scrapy.downloadermiddlerwares.retry.RetryMiddleware': 550,
'scrapy.downloadermiddlerwares.ajaxcrawl.AjaxCrawlMiddleware': 560,
'scrapy.downloadermiddlerwares.redirect.MetaRefreshMiddleware': 580,
'scrapy.downloadermiddlerwares.httpcompression.HttpCompressionMiddleware': 500,
'scrapy.downloadermiddlerwares.redirect.RedirectMiddleware': 600,
'scrapy.downloadermiddlerwares.cookies.CookiesMiddleware': 700,
'scrapy.downloadermiddlerwares.httpproxy.HttpproxyMiddleware': 750,
'scrapy.downloadermiddlerwares.stats.DownloaderStats': 850,
'scrapy.downloadermiddlerwares.httpcache.HttpCacheMiddleware': 900,
}
这是一个字典格式,字典的键名是Scrapy内置的Downloader Middleware的名称,键值代表了调用的优先级,优先级是一个数字,数字越小代表越靠近Engine,数字越大代表越靠近Downloader。
默认情况下,Scrapy已经为我们开启了DOWNLOADER_MIDDLEWARES_BASE所定义的Downloader Middleware,比如RetryMiddleware带有自动重试功能,RedirectMiddleware带有自动处理重定向功能,这些功能默认都是开启的。
那么Downloader Middleware里面究竟是怎么实现的?我们来看一下其固定内部代码:
class ScrapyDemoDownloaderMiddleware:
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
def process_response(self, request, response, spider):
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
def process_exception(self, request, exception, spider):
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
其实每个Downloader Middleware都可以通过定义process_request和process_reponse方法来分别处理Request和Response,被开启的Downloader Middleware的process_request方法和process_response方法会根据优先级顺序调用。
Downloader Middleware优先级:
process_request:由于Request是从Engine发送给Downloader的,并且优先级数字越小的Downloader Middleware越靠近Engine,所以优先级数字越小的Downloader Middleware的process_request方法越先被调用。
process_response:process_response方法则相反,由于Response是由Downloader发送给Engine的,优先级数字越大的Downloader Middleware越靠近Downloader,所以优先级数字越大的Downloader Middleware的process_response越先被调用。
如果我们想将自定义的Downloader Middleware添加到项目中,不要直接修改DOWNLOADER_MIDDLEWARES_BASE变量,Scrapy提供了另外一个设置变量DOWNLOADER_MIDDLEWARES,我们直接修改这个变量就可以添加自己定义的Downloader Middleware,以及禁用DOWNLOADER_MIDDLEWARES_BASE里面定义的Downloader Middleware了。
二、核心方法
Scrapy内置的Downloader Middleware为Scrapy提供了基础的功能,但在项目实战中,我们往往需要单独定义Downloader Middleware。其实这个过程只需要实现如下3个方法中的一个或多个类方法即可:
- process_request(request, spider)
- process_response(request, response ,spider)
- process_exception(request, exception, spider)
接下来我们详细看一下每一个方法的具体内容:
1、process_request(request, spider)
Request被Engine发送给Downloader之前,process_request方法就会被调用,也就是在Request从Scheduler里被调度出来发送到Downloader下载执行之前,我们都可以用process_request方法对Request进行处理。
(1)参数
process_request方法的参数有两个。
- request:Request对象,即被处理的Request。
- spider:Spider对象,即此Request对应的Spider对象。
(2)返回值
这个方法的返回值必须为None、Response对象、Request对象三者之一,或者抛出IgnoreRequest异常。返回类型不同,产生的效果也不同,下面归纳一下不同的返回情况。
- None:当返回的是None时,Scrapy将继续处理该Request,接着执行其他Downloader Middleware的process_request方法,一直到Downloader把Request执行得到Response才结束。这个过程其实就是修改Request的过程,不同的Downloader Middleware按照设置的优先级顺序依次对Request进行修改,最后送至Downloader执行。
- Response:当返回为Response对象时,更低优先级的Downloader Middleware的process_request和process_exception方法就不会被继续调用,每个Downloader Middleware的process_response方法转而被依次调用,调用完毕后,直接将Response对象发送给Spider处理。
- Request:当返回为Request对象时,更低优先级的Downloader Middleware的process_request方法会停止执行。这个Request会重新放到调度队列里,其实它就是一个全新的Request,等待被调度。如果Scheduler调度了,那么所有的Downloader Middleware的process_request方法会被重新按照顺序执行。
- IgnoreRequest:如果抛出IgnoreRequest异常,则所有的Downloader Middleware的process_exception方法会依次执行。如果没有一个方法处理这个异常,那么Request的errorback方法就会回调。如果该异常还没有被处理,那么它便会被忽略。
2、process_response(request, response, spider)
Downloader执行Request下载之后,会得到对应的Response。Engine便会将Response发送给Spider进行解析,在发送给Spider之前,我们都可以用process_response方法来对Response进行处理。
(1)参数
process_response方法的参数有3个:
- request:Request对象,即此Response对应的Request。
- response:Response对象,即被处理的Response。
- spider:Spider对象,即此Response对应的Spider对象。
(2)返回值
process_response方法的返回值必须为Request对象和Response对象两者之一。或者抛出IgnoreRequest异常。那么对不同的返回情况在下面做一下归纳。
- Request:当返回为Request对象时,更低优先级的Downloader Middleware的process_response方法不会继续调用,该Request对象会重新放到调度队列里等待被调度,相当于一个全新的Request。然后,该Request会被process_request方法顺次处理。
- Response:当返回为Response对象时,更低优先级的Downloader Middleware的process_response方法会继续被调用,对该Response对象进行处理。
- IgnoreRequest:当抛出IgnoreRequest异常时,Request的errorback方法会回调。如果该异常还没有被处理,那么它会被忽略。
3、process_exception(request, exception, spider)
当Downloader或process_request方法抛出异常时,例如抛出IgnoreRequest异常,process_exception方法就会被调用。
(1)参数
process_exception方法的参数有3个。
- request:Request对象,即产生异常的Request。
- exception:Exception对象,即抛出的异常。
- spider:Spider对象,即Request对应的Spider。
(2)返回值
方法的返回值必须为None、Response对象、Request对象三者之一。
- None:当返回值为None时,更低优先级的Downloader Middleware的process_exception会被继续顺次调用,直到所有的方法都被调用完毕。
- Response:当返回值为Response时,更低优先级的Downloader Middleware的process_exception不再被继续调用,每个Downloader Middleware的process_response方法转而被依次调用。
- Request:当返回为Request对象时,更低优先级的Downloader Middleware的process_exception也不再被继续调用,该Request对象会重新放到调度队列里面等待被调度,相当于一个全新的Request。然后,该Request又会被process_request方法顺次处理。
以上便是这3个方法的详细使用逻辑,在使用他们之前,一定要对这三个方法的返回值的处理情况有一个清晰认识。在自定义Downloader Middleware的时候,也一定要注意每个方法的返回类型。
三、项目实战
1、新建项目
首先,我们先新创建一个scarpy项目,名字叫做testdownloadermiddleware,命令如下:
- scrapy startproject testdownloadermiddleware
接下来,进入项目,新建一个Spider,我们还是以https://httpbin.org为例。命令如下:
- scrapy crawl httpbin www.httpbin.org
执行完命令后就会出现一个Spider,名为httpbin。
修改start_urls为['http://www.httpbin.org/get'],随后将parse方法添加一行打印输出,如下所示。
2、修改User-Agent
我们可以仔细观察一下以上的结果,发现ua内容为:"Scrapy/2.7.1 (+https://scrapy.org)",这其实是由scraoy内置的UserAgentMiddleware设置的,UserAgentMiddleware的源码如下:
from scrapy import signals
class UserAgentMiddleware(object):
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, spdier):
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.downloadermiddlerwares.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的定义即可:
setting.py文件中的UA设置默认为
我们只需要将UA对应的字符串改为自己的ua就可以了。
一般小型爬虫用这种方法就足够了,那么想要设置的更灵活点,可以设置随机的User-Agent,那就需要借助Downloader Middleware了,所以接下来我们用Downloader Middleware实现一个随机User-Agent的设置。
在middlewares.py里面添加一个RandomUserAgentMiddleware类,如下所示:
不过,在使其生效之前,我们还需要去调用这个Downloader Middleware。在setting.py中,将DOWNLOADER_MIDDLEWARES取消注释,并设置成如下内容:
接下来运行Spider,就可以看到User-Agent被成功修改为列表中所定义的随机的一个User-Agent了:
3、设置代理
接下来我们需要借助Downloader Middleware设置代理,其实在代理的设置方法应用的大概流程同上一样,我们只需要在Downloader Middleware中设置一个代理类即可,下面我们来看一下具体定义的代码:
这里我们定义了一个proxyMiddleware类,在它的process_request方法里面,修改了request的meta属性的proxy属性,赋值为'http://183.162.226.252:40274',这样就相当于设置了一个HTTP代理,同样的要使其生效,我们需要到setting.py文件中修改DOWNLOADER_MIDDLEWARES为如下内容:
这样我们就自定义了两个Downloader Middleware,执行优先级为543和544,543先被调用,为Request赋值User-Agent,随后ProxyMiddleware的process_request会被调用,为Request赋值meta的proxy属性。运行结果如下:
从上面可以看出,我们的ip原本是120.68.0.2,现在已经更换为代理ip114.239.222.190。
4、返回值
(1)Request
我们在上面通过使用process_request对Request进行修改,但刚才写的两个Downloader Middleware的process_request都没有返回值,即返回值为None,这样一个个Downloader Middleware的process_request就会被顺次执行。
之前我们提到了process_request的返回值的理论和逻辑,那么如果我们修改返回值其形式和内容会怎样?比如process_request直接返回request,我们修改ProxyMiddleware试一下:
这个我们在方法的最后加上返回Request的逻辑,根据前面的介绍,如果process_request直接返回的是一个Request,那么后续其他Downloader Middleware的process_request就不会被调用,这个Request会直接反送给Engine,又返回到Scheduler队列等待下一次被调度,由于现在我们只发起了一个Request,所以下一个被调度的Request还是这个Request。然后会再次经过process_request方法处理,接着再次被返回,又一次被加回到Scheduler,这样这个Request就不断从Scheduler取出来放回去,导致无限循环。
所以结果会得到一个递归错误的报错信息:
所以这一句简单的返回逻辑就整个改变了Scrapy爬虫的执行逻辑,一定要注意。
(2)Request返回值
如果我们返回一个Response会怎么办呢?根据前文所述,更低优先级的Downloader Middleware的process_request和process_exception方法就不会被继续调用,每个Downloader Middleware的process_response方法转而被依次调用。调用完毕后,直接将Response对象发送给Spider来处理。所以说,如果返回的是Response,会直接被process_response处理完毕后发送给Spider,而该Request就不会再经由Downloader执行下载了。
我们改写一下ProxyMiddleware,修改如下:
这里,我们直接把代理设置的逻辑去掉了,返回了一个HtmlResponse对象,构造HtmlResponse对象时传入了url、status、encoding、body参数,其中直接赋给body一个字符串。重新运行一下看一下结果:
这就是Spider中的parse方法的输出结果,可以看到原本Request应该去请求https://www.httpbin.org/get得到返回结果,但是这里Response的内容直接变成了刚才我们所定义的HtmlResponse内容,丢弃了原本的Request。因此,如果我们在process_request方法中直接返回对象,原先的Request就会被直接丢弃,该Response经过process_response方法处理后会直接传递给Spider解析。
(3)Response
Downloader对Request执行下载之后会得到Response,随后Engine会将Response发送回Spider进行处理,但是在Response被发送给Spider之前,我们同样可以使用process_response方法对Response进行处理。
比如这里修改一下Response的状态码,添加一个ChangeResponseMiddleware的Downloader Middleware,代码如下:
我们将response对象的status属性修改为201,随后将response返回,这个被修改的Response就会被发送到Spider。
我们在Spider里面输出修改后的状态码,在parse方法中添加如下的输出语句:
然后将DOWNLOADER_MIDDLEWARES修改为如下内容:
接着将ProxyMiddleware换成了ChangeResponseMiddleware,重新运行,控制台输出了如下内容:
可以发现,Response的状态码被成功修改了,因此如果要想对Response进行处理,就可以借助process_response方法。
当然process_response方法的不同返回值有不同的作用,如果返回Request对象,更低优先级的Downloader Middleware的process_request方法会停止执行。这个Request会重新放到调度队列里,其实它就是一个全新的Request,等待被调度。
另外还有一个process_exception方法,它是专门用来处理异常的方法。如果需要进行异常处理,我们可以调用此方法,不过这个方法的使用频率相对低一些,这里就不再进行实例演示了。
到这里,关于Downloader Middleware下载中间件的结束就结束了,细节性的知识点一定要反复琢磨哦....