徹底搞懂Scrapy的中介軟體(二)

青南發表於2018-11-20

在上一篇文章中介紹了下載器中介軟體的一些簡單應用,現在再來通過案例說說如何使用下載器中介軟體整合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)來實現的,當然讀者也可以使用前面章節講到的等待某個元素出現的方法來實現。

有了這個中介軟體以後,就可以像訪問普通網頁那樣直接處理需要非同步載入的頁面,如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

在中介軟體裡重試

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

有的網站的反爬蟲機制被觸發了,它會自動將請求重定向到一個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。
複製程式碼

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

徹底搞懂Scrapy的中介軟體(二)

現在需要獲取1~9頁的內容,那麼使用前面章節學到的內容,通過Chrome瀏覽器的開發者工具很容易就能發現翻頁實際上是一個POST請求,提交的引數為“date”,它的值是日期“2017-08-12”,如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

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

徹底搞懂Scrapy的中介軟體(二)

從上圖可以看到,第5頁沒有正常獲取到,返回的結果是引數錯誤。於是在網頁上看一下,發現第5頁的請求中body裡面的date對應的日期是“2017-08-11”,如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

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

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

由於日期不是今天,就是昨天,所以針對這種情況,寫一個重試中介軟體是最簡單粗暴且有效的解決辦法。中介軟體的程式碼如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

這個中介軟體只對名為“middlewareSpider”的爬蟲有用。由於middlewareSpider爬蟲預設使用的是“今天”的日期,所以如果被網站返回了“引數錯誤”,那麼正確的日期就必然是昨天的了。所以在這個中介軟體裡面,第119行,直接把原來請求的body換成了昨天的日期,這個請求的其他引數不變。讓這個中介軟體生效以後,爬蟲就能成功爬取第5頁了,如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

爬蟲本身的程式碼,資料提取部分完全沒有做任何修改,如果不看中介軟體程式碼,完全感覺不出爬蟲在第5頁重試過。

除了檢查網站返回的內容外,還可以檢查返回內容對應的網址。將上面練習頁後臺網址的第1個引數“para”改為404,暫時禁用重試中介軟體,再跑一次爬蟲。其執行結果如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

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

http://exercise.kingname.info/404.html
複製程式碼

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

對於這種情況,在重試中介軟體裡面判斷返回的網址即可解決,如下圖12-21所示。

徹底搞懂Scrapy的中介軟體(二)

在程式碼的第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的中介軟體(二)

在中介軟體裡處理異常

在預設情況下,一次請求失敗了,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的中介軟體(二)

從上圖可以看到Scrapy自動重試的過程。由於代理有問題,最後會丟擲方框框住的異常,表示TCP超時。在中介軟體裡面如果捕獲到了這個異常,就可以提前更換代理,或者進行重試。這裡以更換代理為例。首先根據上圖中方框框住的內容匯入TCPTimeOutError這個異常:

from twisted.internet.error import TCPTimedOutError
複製程式碼

修改前面開發的重試中介軟體,新增一個process_exception()方法。這個方法接收3個引數,分別為request、exception和spider,如下圖所示。

徹底搞懂Scrapy的中介軟體(二)

process_exception()方法只對名為“exceptionSpider”的爬蟲生效,如果請求遇到了TCPTimeOutError,那麼就首先呼叫remove_broken_proxy()方法把失效的這個代理IP移除,然後返回這個請求物件request。返回以後,Scrapy會重新排程這個請求,就像它第一次排程一樣。由於原來的ProxyMiddleware依然在工作,於是它就會再一次給這個請求更換代理IP。又由於剛才已經移除了失效的代理IP,所以ProxyMiddleware會從剩下的代理IP裡面隨機找一個來給這個請求換上。

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

下載器中介軟體功能總結

能在中介軟體中實現的功能,都能通過直接把程式碼寫到爬蟲中實現。使用中介軟體的好處在於,它可以把資料爬取和其他操作分開。在爬蟲的程式碼裡面專心寫資料爬取的程式碼;在中介軟體裡面專心寫突破反爬蟲、登入、重試和渲染AJAX等操作。

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

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

買不買書不重要,重要的是請關注我的公眾號:未聞 Code

公眾號已經連續日更三個多月了。在接下來的很長時間裡也會連續日更。

徹底搞懂Scrapy的中介軟體(二)

相關文章