scrapy下載中間件(downloader middleware)和蜘蛛中間件(spider middleware)

https://docs.scrapy.org/en/latest/_images/scrapy_architecture_02.png

scrapy組件

首先我們看下scrapy官網(wǎng)提供的新結(jié)構(gòu)圖乘盼,乍一看這畫的是啥啊凉夯,這需要你慢慢的理解其原理就很容易看懂了隐轩,這些都是一個(gè)通用爬蟲框架該具有的一些基本組件。上一篇博客說了項(xiàng)目管道(也就是圖中的ITEM PIPELINES)物喷,可以看到中間的引擎(ENGINE)將item傳遞給了項(xiàng)目管道卤材,也就是讓項(xiàng)目管道來處理抓取到的內(nèi)容。另外圖中的所謂的組件只是抽象出來的東西比較容易讓人理解峦失,其實(shí)這些都是python的類實(shí)例化的東西扇丛。

下載中間件處于引擎和下載器的中間,就像是引擎和下載器有一根管道尉辑,管道上面有兩個(gè)路障就是下載中間件了帆精,而引擎和下載器之間只傳遞請(qǐng)求(REQUESTS)和響應(yīng)(RESPONSE),所以這兩個(gè)路障很明顯的作用就是攔截請(qǐng)求和響應(yīng)(至于攔截之后想干嘛就隨便了隧魄,比如更改請(qǐng)求或響應(yīng)內(nèi)容卓练,更改請(qǐng)求頭或響應(yīng)頭,或者什么都不干购啄,記錄一下就放過去)襟企。

蜘蛛中間件處于蜘蛛和引擎之間,這里說的蜘蛛就是我們?cè)趕pider目錄下編寫的蜘蛛類狮含,已經(jīng)寫過蜘蛛的應(yīng)該知道顽悼,在蜘蛛中一般會(huì)處理響應(yīng)(默認(rèn)解析是parse方法),返回item,或者返回(準(zhǔn)確點(diǎn)應(yīng)該是迭代)請(qǐng)求(scrapy.Request)辉川。而在圖中也很明顯表蝙,蜘蛛和引擎之間會(huì)傳遞請(qǐng)求(REQUESTS)、響應(yīng)(RESPONSE)和items乓旗。響應(yīng)是由下載器從網(wǎng)站上下載得到的府蛇,在傳遞給引擎后給蜘蛛來處理,而items一般是由蜘蛛從響應(yīng)內(nèi)容中解析出來傳遞給引擎在給項(xiàng)目管道處理屿愚,而請(qǐng)求一般是由蜘蛛中的start_urls中定義的URL和蜘蛛解析出的新的URL準(zhǔn)備傳遞給引擎在交由調(diào)度器統(tǒng)一管理汇跨。

上面的一堆屁話說的:中間件的作用就是攔截兩個(gè)組件傳遞的內(nèi)容。

看了這些組件的功能和使用你會(huì)發(fā)現(xiàn)為什么要這么麻煩妆距,你看我用requests庫(kù)只需要請(qǐng)求一下得到響應(yīng)直接處理就行了穷遂,這引擎和調(diào)度器感覺沒什么存在的意義啊。這就需要慢慢理解了娱据,或者自己寫一個(gè)簡(jiǎn)單通用爬蟲框架可能就知道了蚪黑。

中間件的編寫

中間件的啟用

DOWNLOADER_MIDDLEWARES  = {
  'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
  'newspider.middlewares.UAMiddleware': 500   #newspider為scrapy項(xiàng)目名稱,也就是settings.py中BOT_NAME的值,UAMiddleware為定義的中間件的名字
}

字典的值如何選擇中剩,500這個(gè)值改成其他的行不行呢忌穿?先看一下官方是怎么說的:

scrapy會(huì)將DOWNLOADER_MIDDLEWARES設(shè)置與DOWNLOADER_MIDDLEWARES_BASE設(shè)置合并,然后按值的順序排序以獲取啟用的中間件的最終排序列表:值最小的中間件離引擎更近结啼,最大的中間件離下載器更近掠剑。換句話說,每個(gè)中間件的process_request() 方法將以遞增的順序被調(diào)用郊愧,而每個(gè)中間件的process_response()方法將以遞減的順序調(diào)用朴译。

要決定分配給中間件的順序井佑,請(qǐng)查看 DOWNLOADER_MIDDLEWARES_BASE設(shè)置并根據(jù)您要插入中間件的位置選擇一個(gè)值。順序很重要眠寿,因?yàn)槊總€(gè)中間件執(zhí)行不同的操作躬翁,并且您的中間件可能取決于所應(yīng)用的某些先前(或后續(xù))中間件。

如果要禁用內(nèi)置中間件(DOWNLOADER_MIDDLEWARES_BASE默認(rèn)情況下在定義和啟用的中間件 )澜公,則必須在項(xiàng)目的DOWNLOADER_MIDDLEWARES設(shè)置為None姆另。

上面的一些話中有一句很重要:每個(gè)中間件的process_request() 方法將以遞增的順序被調(diào)用,而每個(gè)中間件的process_response()方法將以遞減的順序調(diào)用坟乾。這應(yīng)該就很清楚值怎么選吧迹辐,不過一般中間件并沒有確定的順序,只要不產(chǎn)生矛盾就行了甚侣。

舉個(gè)例子吧:你已經(jīng)寫了一個(gè)中間件newspider.middlewares.UAMiddleware明吩,但是你在啟用的時(shí)候沒有禁止內(nèi)置的UserAgentMiddleware,怎么才能使你的中間件生效呢殷费?代碼如下:

DOWNLOADER_MIDDLEWARES  = {
  #'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
  'newspider.middlewares.UAMiddleware': 501  #newspider為scrapy項(xiàng)目名稱,也就是settings.py中BOT_NAME的值印荔,UAMiddleware為定義的中間件的名字
}

因?yàn)槟J(rèn)的UserAgentMiddleware值為500,因?yàn)樾薷恼?qǐng)求頭是在process_request详羡,而process_request是以遞增的形式被調(diào)用仍律,所以會(huì)先調(diào)用內(nèi)置UserAgentMiddleware,在調(diào)用你定義的UAMiddleware实柠,則內(nèi)置的會(huì)被覆蓋水泉。不過因?yàn)楹芏嗳诵薷恼?qǐng)求頭都是以字典訪問的形式修改(request.headers["User-Agent"] = ""),這樣的話即使你設(shè)置值為499,結(jié)果還是你設(shè)定的User-Agent窒盐,而不是默認(rèn)內(nèi)置的草则。

其實(shí)看一下內(nèi)置的代碼就可以理解了:源代碼
代碼使用的是request.headers.setdefault(b'User-Agent', self.user_agent),setdefault是當(dāng)不存在鍵時(shí)設(shè)置蟹漓,存在則不設(shè)置炕横。

內(nèi)置下載中間件

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,
}

下載中間件的編寫

  • process_request(request,spider): 所有請(qǐng)求都會(huì)調(diào)用此方法
  • process_response(request, response, spider): 這里的參數(shù)比上面的多了response葡粒,肯定是用來處理response的
  • process_exception(request, exception, spider):處理異常
  • from_crawler(cls, crawler):從settings.py獲取配置

假設(shè)我們需要一個(gè)自動(dòng)管理cookie的中間件份殿,應(yīng)該怎么編寫?首先管理cookie一般就是將服務(wù)器返回的Set-cookie更新一下已保存的cookie嗽交。偽代碼如下:

from scrapy.exceptions import NotConfigured


class CookieMiddleware(object):
    def __init__(self):
        # 實(shí)際上cookies并不能使用字典來保存伯铣,因?yàn)閏ookies是可以包含相同的鍵的
        # 這里只做簡(jiǎn)單的演示,正陈秩遥可以使用scrapy.http.cookies.CookieJar
        self.cookies = {}
    
    def process_request(request, spider):
        self.addcookies(request, self.cookies)
    
    def process_response(request, response, spider):
        newcookie = response.headers.getlist('Set-Cookie')
        self.updatecookies(self.cookies, newcookie)
        return response
    
    def process_exception(request, exception, spider):
        pass
    
    def addcookies(self, request, cookies):
        '''
        給請(qǐng)求加cookie
        '''
        pass
    
    def updatecookie(self, cookies, cookie):
        '''
        更新cookie字典(對(duì)象)
        '''
        pass
        
    @classmethod
    def from_crawler(cls, crawler):
        if not crawler.settings.get('是否開啟cookie'):
            #這個(gè)方法在__init__之前,拋出異常的話焚鲜,類中的所有方法就都不執(zhí)行了
            raise NotConfigured
        

其實(shí)scrapy已經(jīng)內(nèi)置了cookies管理的中間件掌唾,可以參考一下源代碼:https://docs.scrapy.org/en/latest/_modules/scrapy/downloadermiddlewares/cookies.html#CookiesMiddleware

想要一個(gè)隨機(jī)User-Agent的中間件也很簡(jiǎn)單放前,只需要在process_request方法中改變請(qǐng)求頭就行了。其實(shí)process_request和process_response這兩個(gè)方法是有返回值的糯彬,在上面的例子中只是對(duì)請(qǐng)求頭和響應(yīng)頭做了修改凭语,所以沒有涉及到返回值。如果我們需要對(duì)請(qǐng)求和響應(yīng)整個(gè)做更改的話就需要重新構(gòu)造請(qǐng)求和響應(yīng)撩扒。這里最典型的例子就是selenium中間件了似扔。

spider示例代碼:

import scrapy
from selenium import webdriver


class SeleniumSpider(scrapy.Spider):
    name = 'seleniumspider'
    start_urls = ['https://example.com']

    # 實(shí)例化瀏覽器對(duì)象, 因?yàn)闉g覽器對(duì)象只需要實(shí)例化一次, 所以不在中間件中進(jìn)行實(shí)例化
    bro = webdriver.Chrome(executable_path=r'E:\spiderman\day13\wynews\wynews\wynews\chromedriver.exe')

    def parse(self, response):
        '''
        和正常一樣,該怎么解析怎么解析
        '''
        pass

selenium中間件示例代碼(只是簡(jiǎn)單演示搓谆,為了更好理解中間件怎么寫):

from selenium.common.exceptions import TimeoutException
from scrapy.http import HtmlResponse


class SeleniumMiddleware(object):
    def __init__(self, timeout=None, options=None):
        pass

    def process_reqeust(self, request, spider):
        try:
            url = request.url
            spider.bro.get(url)
            return HtmlResponse(url=url, body=spider.bro.page_source, request=request,encoding='utf-8',status=200)
        except TimeoutException:
            return HtmlResponse(url=url, status=500, request=request)

    @classmethod
    def from_crawler(cls, crawler):
        pass

在代碼中HtmlResponse類就是我們新構(gòu)建的響應(yīng)炒辉,這樣前面的spider處理的response就是這個(gè)響應(yīng)了。這里有個(gè)疑問泉手?為什么要在process_reqeust這個(gè)方法中返回響應(yīng)黔寇,就不能在process_response中嗎?其實(shí)也可以斩萌,區(qū)別在于process_reqeust返回的話缝裤,會(huì)忽略其他中間件中的process_reqeust和process_exception方法,只執(zhí)行其他中間件的process_response方法颊郎。試想一下如果你還有一個(gè)cookie管理的中間件憋飞,process_reqeust的操作是不是就和SeleniumMiddleware中的矛盾了,所以我們要在process_reqeust返回response來使其他中間件的process_reqeust方法失效姆吭。

返回值

process_reqeust方法返回值

  • None:如果返回None榛做,則一切正常傳遞
  • request對(duì)象:如果返回request對(duì)象,scrapy會(huì)停止調(diào)用接下來的中間件而重新處理request對(duì)象(也就是交由引擎重新管理)
  • response對(duì)象:...猾编,Scrapy不再調(diào)用其他中間件的process_request()或process_exception()方法瘤睹,它將直接調(diào)用其他中間件的process_response方法
  • IgnoreRequest異常:依次調(diào)用所有中間件的process_exception方法

process_response方法返回值(注意這個(gè)方法必須返回一個(gè)對(duì)象,不能是None)

  • response對(duì)象:傳遞給下一個(gè)中間件的process_response方法
  • request對(duì)象:scrapy會(huì)停止調(diào)用接下來的中間件而重新處理request對(duì)象(也就是交由引擎重新管理)
  • IgnoreRequest異常:依次調(diào)用所有中間件的process_exception方法

process_exception方法返回值

  • None:繼續(xù)傳遞
  • request對(duì)象:scrapy會(huì)停止調(diào)用接下來的中間件的process_exception方法而重新處理request對(duì)象(也就是交由引擎重新管理)
  • response對(duì)象:停止傳遞接下來的中間件中的process_exception答倡,而開始調(diào)用所有process_response方法

蜘蛛中間件

  • process_spider_input(response, spider):所有請(qǐng)求都會(huì)調(diào)用這個(gè)方法
  • process_spider_output(response, result, spider):spider解析完response之后調(diào)用該方法轰传,result就是解析的結(jié)果(是一個(gè)可迭代對(duì)象),其中可能是items也可能是request對(duì)象
  • process_spider_exception(response, exception, spider):處理異常
  • process_start_requests(start_requests, spider):同process_spider_output瘪撇,不過只處理spider中start_requests方法返回的結(jié)果
  • from_crawler(cls, crawler):...

同樣是攔截請(qǐng)求和響應(yīng)获茬,那么和下載中間件有什么區(qū)別呢?因?yàn)橄螺d中間件是連通引擎和下載器的倔既,所以如果修改請(qǐng)求只會(huì)影響下載器返回的結(jié)果恕曲。如果修改響應(yīng),會(huì)影響蜘蛛處理渤涌。而蜘蛛中間件是連通引擎和蜘蛛的佩谣,如果修改請(qǐng)求則會(huì)影響整個(gè)scrapy的請(qǐng)求,因?yàn)閟crapy的所有請(qǐng)求都來自于蜘蛛实蓬,當(dāng)然包括調(diào)度器和下載器茸俭。如果修改響應(yīng)吊履,則只會(huì)影響蜘蛛的解析,因?yàn)轫憫?yīng)是由引擎?zhèn)鬟f給蜘蛛的调鬓。

上面說的有點(diǎn)亂艇炎,整理一下這兩個(gè)的功能:
蜘蛛中間件:記錄深度、丟棄非200狀態(tài)碼響應(yīng)腾窝、丟棄非指定域名請(qǐng)求等
下載中間件:修改請(qǐng)求頭缀踪、修改響應(yīng)、管理cookies虹脯、丟棄非200狀態(tài)碼響應(yīng)驴娃、丟棄非指定域名請(qǐng)求等

丟棄非指定域名請(qǐng)求一般使用蜘蛛中間件,如果使用下載中間件會(huì)導(dǎo)致引擎和調(diào)度器的無效任務(wù)變多归形,丟棄非200狀態(tài)碼響應(yīng)我感覺應(yīng)該用下載中間件托慨,但是scrapy內(nèi)置的使用的是蜘蛛中間件,可能是我理解不夠透徹吧暇榴。

scrapy已經(jīng)提供了很多內(nèi)置的中間件厚棵,只需要啟用即可。另外蔼紧,蜘蛛中間件一般用于操作蜘蛛返回的request婆硬,而下載中間件用于操作向互聯(lián)網(wǎng)發(fā)起請(qǐng)求的request和返回的response。更多的示例可以看一些內(nèi)置中間件的源碼奸例,蜘蛛中間件一般不需要自己編寫彬犯,使用內(nèi)置的幾個(gè)也足夠了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市查吊,隨后出現(xiàn)的幾起案子谐区,更是在濱河造成了極大的恐慌,老刑警劉巖逻卖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宋列,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡评也,警方通過查閱死者的電腦和手機(jī)炼杖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盗迟,“玉大人坤邪,你說我怎么就攤上這事》B疲” “怎么了艇纺?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我黔衡,道長(zhǎng)消约,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任员帮,我火速辦了婚禮,結(jié)果婚禮上导饲,老公的妹妹穿的比我還像新娘捞高。我一直安慰自己,他們只是感情好渣锦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布硝岗。 她就那樣靜靜地躺著,像睡著了一般袋毙。 火紅的嫁衣襯著肌膚如雪型檀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天听盖,我揣著相機(jī)與錄音胀溺,去河邊找鬼。 笑死皆看,一個(gè)胖子當(dāng)著我的面吹牛仓坞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腰吟,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼无埃,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了毛雇?” 一聲冷哼從身側(cè)響起嫉称,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎灵疮,沒想到半個(gè)月后织阅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡始藕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蒲稳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伍派。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡江耀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诉植,到底是詐尸還是另有隱情祥国,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站舌稀,受9級(jí)特大地震影響啊犬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜壁查,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一觉至、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧睡腿,春花似錦语御、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至挂捻,卻和暖如春碉纺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刻撒。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工骨田, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疫赎。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓盛撑,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親捧搞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抵卫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345