Scrapy框架的使用之Downloader Middleware的用法

崔慶才丨靜覓發表於2018-05-09

Downloader Middleware即下載中介軟體,它是處於Scrapy的Request和Response之間的處理模組。我們首先來看看它的架構,如下圖所示。

Scrapy框架的使用之Downloader Middleware的用法

Scheduler從佇列中拿出一個Request傳送給Downloader執行下載,這個過程會經過Downloader Middleware的處理。另外,當Downloader將Request下載完成得到Response返回給Spider時會再次經過Downloader Middleware處理。

也就是說,Downloader Middleware在整個架構中起作用的位置是以下兩個:

  • 在Scheduler排程出佇列的Request傳送給Doanloader下載之前,也就是我們可以在Request執行下載之前對其進行修改。

  • 在下載後生成的Response傳送給Spider之前,也就是我們可以在生成Resposne被Spider解析之前對其進行修改。

Downloader Middleware的功能十分強大,修改User-Agent、處理重定向、設定代理、失敗重試、設定Cookies等功能都需要藉助它來實現。下面我們來了解一下Downloader Middleware的詳細用法。

一、使用說明

需要說明的是,Scrapy其實已經提供了許多Downloader Middleware,比如負責失敗重試、自動重定向等功能的Middleware,它們被DOWNLOADER_MIDDLEWARES_BASE變數所定義。

DOWNLOADER_MIDDLEWARES_BASE變數的內容如下所示:

{
    '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,
}複製程式碼

這是一個字典格式,字典的鍵名是Scrapy內建的Downloader Middleware的名稱,鍵值代表了呼叫的優先順序,優先順序是一個數字,數字越小代表越靠近Scrapy引擎,數字越大代表越靠近Downloader,數字小的Downloader Middleware會被優先呼叫。

如果自己定義的Downloader Middleware要新增到專案裡,DOWNLOADER_MIDDLEWARES_BASE變數不能直接修改。Scrapy提供了另外一個設定變數DOWNLOADER_MIDDLEWARES,我們直接修改這個變數就可以新增自己定義的Downloader Middleware,以及禁用DOWNLOADER_MIDDLEWARES_BASE裡面定義的Downloader Middleware。下面我們具體來看看Downloader Middleware的使用方法。

二、核心方法

Scrapy內建的Downloader Middleware為Scrapy提供了基礎的功能,但在專案實戰中我們往往需要單獨定義Downloader Middleware。不用擔心,這個過程非常簡單,我們只需要實現某幾個方法即可。

每個Downloader Middleware都定義了一個或多個方法的類,核心的方法有如下三個。

  • process_request(request, spider)

  • process_response(request, response, spider)

  • process_exception(request, exception, spider)

我們只需要實現至少一個方法,就可以定義一個Downloader Middleware。下面我們來看看這三個方法的詳細用法。

1. process_request(request, spider)

Request被Scrapy引擎排程給Downloader之前,process_request()方法就會被呼叫,也就是在Request從佇列裡排程出來到Downloader下載執行之前,我們都可以用process_request()方法對Request進行處理。方法的返回值必須為None、Response物件、Request物件之一,或者丟擲IgnoreRequest異常。

process_request()方法的引數有如下兩個。

  • request,是Request物件,即被處理的Request。

  • spider,是Spdier物件,即此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()方法就會回撥。如果該異常還沒有被處理,那麼它便會被忽略。

2. process_response(request, response, spider)

Downloader執行Request下載之後,會得到對應的Response。Scrapy引擎便會將Response傳送給Spider進行解析。在傳送之前,我們都可以用process_response()方法來對Response進行處理。方法的返回值必須為Request物件、Response物件之一,或者丟擲IgnoreRequest異常。

process_response()方法的引數有如下三個。

  • request,是Request物件,即此Response對應的Request。

  • response,是Response物件,即此被處理的Response。

  • spider,是Spider物件,即此Response對應的Spider。

下面歸納一下不同的返回情況。

  • 當返回為Request物件時,更低優先順序的Downloader Middleware的process_response()方法不會繼續呼叫。該Request物件會重新放到排程佇列裡等待被排程,它相當於一個全新的Request。然後,該Request會被process_request()方法順次處理。

  • 當返回為Response物件時,更低優先順序的Downloader Middleware的process_response()方法會繼續呼叫,繼續對該Response物件進行處理。

  • 如果IgnoreRequest異常丟擲,則Request的errorback()方法會回撥。如果該異常還沒有被處理,那麼它便會被忽略。

3. process_exception(request, exception, spider)

當Downloader或process_request()方法丟擲異常時,例如丟擲IgnoreRequest異常,process_exception()方法就會被呼叫。方法的返回值必須為None、Response物件、Request物件之一。

process_exception()方法的引數有如下三個。

  • 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()方法順次處理。

以上內容便是這三個方法的詳細使用邏輯。在使用它們之前,請先對這三個方法的返回值的處理情況有一個清晰的認識。在自定義Downloader Middleware的時候,也一定要注意每個方法的返回型別。

下面我們用一個案例實戰來加深一下對Downloader Middleware用法的理解。

三、專案實戰

新建一個專案,命令如下所示:

scrapy startproject scrapydownloadertest複製程式碼

新建了一個Scrapy專案,名為scrapydownloadertest。進入專案,新建一個Spider,命令如下所示:

scrapy genspider httpbin httpbin.org複製程式碼

新建了一個Spider,名為httpbin,原始碼如下所示:

import scrapy
class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['httpbin.org']
    start_urls = ['http://httpbin.org/']

    def parse(self, response):
        pass複製程式碼

接下來我們修改start_urls為:[http://httpbin.org/](http://httpbin.org/)。隨後將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):
        self.logger.debug(response.text)複製程式碼

接下來執行此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,br", 
    "Accept-Language": "en", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "Scrapy/1.4.0 (+http://scrapy.org)"
  }, 
  "origin": "60.207.237.85", 
  "url": "http://httpbin.org/get"
}複製程式碼

我們觀察一下Headers,Scrapy傳送的Request使用的User-Agent是Scrapy/1.4.0(+http://scrapy.org),這其實是由Scrapy內建的`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, spider):
        if self.user_agent:
            request.headers.setdefault(b'User-Agent', self.user_agent)複製程式碼

from_crawler()方法中,首先嚐試獲取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()方法設定的。

修改請求時的User-Agent可以有兩種方式:一是修改settings裡面的USER_AGENT變數;二是通過Downloader Middleware的process_request()方法來修改。

第一種方法非常簡單,我們只需要在setting.py裡面加一行USER_AGENT的定義即可:

USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'複製程式碼

一般推薦使用此方法來設定。但是如果想設定得更靈活,比如設定隨機的User-Agent,那就需要藉助Downloader Middleware了。所以接下來我們用Downloader Middleware實現一個隨機User-Agent的設定。

在middlewares.py裡面新增一個RandomUserAgentMiddleware的類,如下所示:

import random

class RandomUserAgentMiddleware():
    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__()方法中定義了三個不同的User-Agent,並用一個列表來表示。接下來實現了process_request()方法,它有一個引數request,我們直接修改request的屬性即可。在這裡我們直接設定了request變數的headers屬性的User-Agent,設定內容是隨機選擇的User-Agent,這樣一個Downloader Middleware就寫好了。

不過,要使之生效我們還需要再去呼叫這個Downloader Middleware。在settings.py中,將DOWNLOADER_MIDDLEWARES取消註釋,並設定成如下內容:

DOWNLOADER_MIDDLEWARES = {
   'scrapydownloadertest.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,br", 
    "Accept-Language": "en", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)"
  }, 
  "origin": "60.207.237.85", 
  "url": "http://httpbin.org/get"
}複製程式碼

我們就通過實現Downloader Middleware並利用process_request()方法成功設定了隨機的User-Agent。

另外,Downloader Middleware還有process_response()方法。Downloader對Request執行下載之後會得到Response,隨後Scrapy引擎會將Response傳送回Spider進行處理。但是在Response被髮送給Spider之前,我們同樣可以使用process_response()方法對Response進行處理。比如這裡修改一下Response的狀態碼,在RandomUserAgentMiddleware新增如下程式碼:

def process_response(self, request, response, spider):
    response.status = 201
    return response複製程式碼

我們將response變數的status屬性修改為201,隨後將response返回,這個被修改後的Response就會被髮送到Spider。

我們再在Spider裡面輸出修改後的狀態碼,在parse()方法中新增如下的輸出語句:

self.logger.debug('Status Code: ' + str(response.status))複製程式碼

重新執行之後,控制檯輸出瞭如下內容:

[httpbin] DEBUG: Status Code: 201複製程式碼

可以發現,Response的狀態碼成功修改了。

因此要想對Response進行後處理,就可以藉助於process_response()方法。

另外還有一個process_exception()方法,它是用來處理異常的方法。如果需要異常處理的話,我們可以呼叫此方法。不過這個方法的使用頻率相對低一些,在此不用例項演示。

四、本節程式碼

本節原始碼為:https://github.com/Python3WebSpider/ScrapyDownloaderTest。

五、結語

本節講解了Downloader Middleware的基本用法。此元件非常重要,是做異常處理和反爬處理的核心。後面我們會在實戰中應用此元件來處理代理、Cookies等內容。



相關文章