徹底搞懂Scrapy的中間件(二)

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

在中間件中集成Selenium

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

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

創建一個SeleniumMiddleware,其代碼如下:

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)來實現的,當然讀者也可以使用前面章節講到的等待某個元素出現的方法來實現。

有了這個中間件以後,就可以像訪問普通網頁那樣直接處理需要異步加載的頁面,如下圖所示。

在中間件裏重試

在爬蟲的運行過程中,可能會因爲網絡問題或者是網站反爬蟲機制生效等原因,導致一些請求失敗。在某些情況下,少量的數據丟失是無關緊要的,例如在幾億次請求裏面失敗了十幾次,損失微乎其微,沒有必要重試。但還有一些情況,每一條請求都至關重要,容不得有一次失敗。此時就需要使用中間件來進行重試。

有的網站的反爬蟲機制被觸發了,它會自動將請求重定向到一個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”才能訪問。遇到這種情況,與其花費大量的時間和精力去追蹤時間切換點的變化規律,不如簡單粗暴,直接先用今天去試,再用昨天的日期去試,反正最多兩次,總有一個是正確的。

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

打開練習頁面

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等操作。

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

本文節選自我的新書《Python爬蟲開發 從入門到實戰》完整目錄可以在京東查詢到 https://item.jd.com/12436581.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章