完全搞懂scrapy的中間件第二章

在上一篇文章中介紹了下載器中間件的一些簡單應用,如今再來經過案例說說如何使用下載器中間件集成Selenium、重試和處理請求異常。html

在中間件中集成Selenium

對於一些很麻煩的異步加載頁面,手動尋找它的後臺API代價可能太大。這種狀況下可使用Selenium和ChromeDriver或者Selenium和PhantomJS來實現渲染網頁。python

這是前面的章節已經講到的內容。那麼,如何把Scrapy與Selenium結合起來呢?這個時候又要用到中間件了。web

建立一個SeleniumMiddleware,其代碼以下:redis

from scrapy.http import HtmlResponse class SeleniumMiddleware(object): def __init__(self): self.driver = webdriver.Chrome('./chromedriver') def process_request(self, request, spider): if spider.name == 'seleniumSpider': self.driver.get(request.url) time.sleep(2) body = self.driver.page_source return HtmlResponse(self.driver.current_url, body=body, encoding='utf-8', request=request)

這個中間件的做用,就是對名爲「seleniumSpider」的爬蟲請求的網址,使用ChromeDriver先進行渲染,而後用返回的渲染後的HTML代碼構造一個Response對象。若是是其餘的爬蟲,就什麼都不作。在上面的代碼中,等待頁面渲染完成是經過time.sleep(2)來實現的,固然讀者也可使用前面章節講到的等待某個元素出現的方法來實現。chrome

有了這個中間件之後,就能夠像訪問普通網頁那樣直接處理須要異步加載的頁面,以下圖所示。瀏覽器

在中間件裏重試

在爬蟲的運行過程當中,可能會由於網絡問題或者是網站反爬蟲機制生效等緣由,致使一些請求失敗。在某些狀況下,少許的數據丟失是可有可無的,例如在幾億次請求裏面失敗了十幾回,損失微乎其微,沒有必要重試。但還有一些狀況,每一條請求都相當重要,容不得有一次失敗。此時就須要使用中間件來進行重試。markdown

有的網站的反爬蟲機制被觸發了,它會自動將請求重定向到一個xxx/404.html頁面。那麼若是發現了這種自動的重定向,就沒有必要讓這一次的請求返回的內容進入數據提取的邏輯,而應該直接丟掉或者重試。網絡

還有一種狀況,某網站的請求參數裏面有一項,Key爲date,Value爲發起請求的這一天的日期或者發起請求的這一天的前一天的日期。例現在天是「2017-08-10」,可是這個參數的值是今天早上10點以前,都必須使用「2017-08-09」,在10點以後才能使用「2017-08-10」,不然,網站就不會返回正確的結果,而是返回「參數錯誤」這4個字。然而,這個日期切換的時間點受到其餘參數的影響,有可能第1個請求使用「2017-08-10」能夠成功訪問,而第2個請求卻只有使用「2017-08-09」才能訪問。遇到這種狀況,與其花費大量的時間和精力去追蹤時間切換點的變化規律,不如簡單粗暴,直接先用今天去試,再用昨天的日期去試,反正最多兩次,總有一個是正確的。app

以上的兩種場景,使用重試中間件都能輕鬆搞定。less

打開練習頁面

http://exercise.kingname.info/exercise_middleware_retry.html。

這個頁面實現了翻頁邏輯,能夠上一頁、下一頁地翻頁,也能夠直接跳到任意頁數,以下圖所示。

如今須要獲取1~9頁的內容,那麼使用前面章節學到的內容,經過Chrome瀏覽器的開發者工具很容易就能發現翻頁其實是一個POST請求,提交的參數爲「date」,它的值是日期「2017-08-12」,以下圖所示。

使用Scrapy寫一個爬蟲來獲取1~9頁的內容,運行結果以下圖所示。

從上圖能夠看到,第5頁沒有正常獲取到,返回的結果是參數錯誤。因而在網頁上看一下,發現第5頁的請求中body裏面的date對應的日期是「2017-08-11」,以下圖所示。

若是測試的次數足夠多,時間足夠長,就會發現如下內容。

  1. 同一個時間點,不一樣頁數提交的參數中,date對應的日期多是今天的也多是昨天的。
  2. 同一個頁數,不一樣時間提交的參數中,date對應的日期多是今天的也多是昨天的。

因爲日期不是今天,就是昨天,因此針對這種狀況,寫一個重試中間件是最簡單粗暴且有效的解決辦法。中間件的代碼以下圖所示。

這個中間件只對名爲「middlewareSpider」的爬蟲有用。因爲middlewareSpider爬蟲默認使用的是「今天」的日期,因此若是被網站返回了「參數錯誤」,那麼正確的日期就必然是昨天的了。因此在這個中間件裏面,第119行,直接把原來請求的body換成了昨天的日期,這個請求的其餘參數不變。讓這個中間件生效之後,爬蟲就能成功爬取第5頁了,以下圖所示。

爬蟲自己的代碼,數據提取部分徹底沒有作任何修改,若是不看中間件代碼,徹底感受不出爬蟲在第5頁重試過。

除了檢查網站返回的內容外,還能夠檢查返回內容對應的網址。將上面練習頁後臺網址的第1個參數「para」改成404,暫時禁用重試中間件,再跑一次爬蟲。其運行結果以下圖所示。

此時,對於參數不正確的請求,網站會自動重定向到如下網址對應的頁面:

http://exercise.kingname.info/404.html

因爲Scrapy自帶網址自動去重機制,所以雖然第3頁、第6頁和第7頁都被自動轉到了404頁面,可是爬蟲只會爬一次404頁面,剩下兩個404頁面會被自動過濾。

對於這種狀況,在重試中間件裏面判斷返回的網址便可解決,以下圖12-21所示。

在代碼的第115行,判斷是否被自動跳轉到了404頁面,或者是否被返回了「參數錯誤」。若是都不是,說明這一次請求目前看起來正常,直接把response返回,交給後面的中間件來處理。若是被重定向到了404頁面,或者被返回「參數錯誤」,那麼進入重試的邏輯。若是返回了「參數錯誤」,那麼進入第126行,直接替換原來請求的body便可從新發起請求。

若是自動跳轉到了404頁面,那麼這裏有一點須要特別注意:此時的請求,request這個對象對應的是向404頁面發起的GET請求,而不是原來的向練習頁後臺發起的請求。因此,從新構造新的請求時必須把URL、body、請求方式、Headers所有都換一遍才能夠。

因爲request對應的是向404頁面發起的請求,因此resquest.url對應的網址是404頁面的網址。所以,若是想知道調整以前的URL,可使用以下的代碼:

request.meta['redirect_urls']

這個值對應的是一個列表。請求自動跳轉了幾回,這個列表裏面就有幾個URL。這些URL是按照跳轉的前後次序依次append進列表的。因爲本例中只跳轉了一次,因此直接讀取下標爲0的元素便可,也就是原始網址。

從新激活這個重試中間件,不改變爬蟲數據抓取部分的代碼,直接運行之後能夠正確獲得1~9頁的所有內容,以下圖所示。

在中間件裏處理異常

在默認狀況下,一次請求失敗了,Scrapy會馬上原地重試,再失敗再重試,如此3次。若是3次都失敗了,就放棄這個請求。這種重試邏輯存在一些缺陷。以代理IP爲例,代理存在不穩定性,特別是免費的代理,差很少10個裏面只有3個能用。而如今市面上有一些收費代理IP提供商,購買他們的服務之後,會直接提供一個固定的網址。把這個網址設爲Scrapy的代理,就能實現每分鐘自動以不一樣的IP訪問網站。若是其中一個IP出現了故障,那麼須要等一分鐘之後纔會更換新的IP。在這種場景下,Scrapy自帶的重試邏輯就會致使3次重試都失敗。

這種場景下,若是能馬上更換代理就馬上更換;若是不能馬上更換代理,比較好的處理方法是延遲重試。而使用Scrapy_redis就能實現這一點。爬蟲的請求來自於Redis,請求失敗之後的URL又放回Redis的末尾。一旦一個請求原地重試3次仍是失敗,那麼就把它放到Redis的末尾,這樣Scrapy須要把Redis列表前面的請求都消費之後纔會重試以前的失敗請求。這就爲更換IP帶來了足夠的時間。

從新打開代理中間件,這一次故意設置一個有問題的代理,因而能夠看到Scrapy控制檯打印出了報錯信息,以下圖所示。

從上圖能夠看到Scrapy自動重試的過程。因爲代理有問題,最後會拋出方框框住的異常,表示TCP超時。在中間件裏面若是捕獲到了這個異常,就能夠提早更換代理,或者進行重試。這裏以更換代理爲例。首先根據上圖中方框框住的內容導入TCPTimeOutError這個異常:

from twisted.internet.error import TCPTimedOutError

修改前面開發的重試中間件,添加一個process_exception()方法。這個方法接收3個參數,分別爲request、exception和spider,以下圖所示。

process_exception()方法只對名爲「exceptionSpider」的爬蟲生效,若是請求遇到了TCPTimeOutError,那麼就首先調用remove_broken_proxy()方法把失效的這個代理IP移除,而後返回這個請求對象request。返回之後,Scrapy會從新調度這個請求,就像它第一次調度同樣。因爲原來的ProxyMiddleware依然在工做,因而它就會再一次給這個請求更換代理IP。又因爲剛纔已經移除了失效的代理IP,因此ProxyMiddleware會從剩下的代理IP裏面隨機找一個來給這個請求換上。

特別提醒:圖片中的remove_broken_proxy()函數體裏面寫的是pass,可是在實際開發過程當中,讀者可根據實際狀況實現這個方法,寫出移除失效代理的具體邏輯。

下載器中間件功能總結

能在中間件中實現的功能,都能經過直接把代碼寫到爬蟲中實現。使用中間件的好處在於,它能夠把數據爬取和其餘操做分開。在爬蟲的代碼裏面專心寫數據爬取的代碼;在中間件裏面專心寫突破反爬蟲、登陸、重試和渲染AJAX等操做。

對團隊來講,這種寫法能實現多人同時開發,提升開發效率;對我的來講,寫爬蟲的時候不用考慮反爬蟲、登陸、驗證碼和異步加載等操做。另外,寫中間件的時候不用考慮數據怎樣提取。一段時間只作一件事,思路更清晰。

相關文章
相關標籤/搜索