scrapy爬虫框架(四)Downloader Middleware的使用

发布时间 2023-04-03 15:28:51作者: 乐之之

  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下载中间件的结束就结束了,细节性的知识点一定要反复琢磨哦....