Python 非同步網路爬蟲(2)

Yusheng發表於2016-11-21

上一部分整理了如何利用 aiohttpasyncio 執行非同步網路請求,接下來我們將在此基礎上實現一個簡潔、普適的爬蟲框架。

一般網站抓取的流程是這樣的:

Python 非同步網路爬蟲(2)

從入口頁面開始提取一組下一級頁面的連結,然後遞迴地執行下去,直到最後一層頁面為止。唯一不同的是對每一級頁面所要抓取的資訊,也就是需要的正規表示式不同,除此之外,請求頁面、分析內容、正則匹配的步驟是重複的,因此可以將上面的過程簡化為:

Python 非同步網路爬蟲(2)

其中虛線框中的步驟可以抽象出來,即模擬瀏覽器行為的 ClientCleaning 方法用於對正則匹配的結果進行清理,並將下一級所需的入口地址返回給 Client,在這一過程中也可能涉及到資料輸出到資料庫的過程。

這裡我參考了 Flask(或 Sanic)框架的設計,即利用 Python 裝飾器的語法特性,將不同頁面的 Cleaning 方法註冊到 Client 中:

這時我們遇到一個比較棘手的問題:由於從當前頁面提取資料(extract())的過程是非同步的, 而在上一個頁面執行完成之前是無法進入下一個頁面的,也就是說同一級頁面之間是非同步的,不同層級頁面之間是同步的,那麼如何在 asyncio 中安排這種任務?

這其實是一個帶有遞迴屬性的生產者/消費者模型,上級頁面作為生產者只有在經過網路請求之後才能生產出下級所有入口連結,而下一級的消費者將成為下下一級的生產者……我們可以將事件迴圈看作是一個“傳送帶”,一些可能造成阻塞的任務(如extract())會被掛起,等阻塞任務完成後重新進入佇列等待被執行:

Python 非同步網路爬蟲(2)

在上面的問題中,不同層級頁面的提取過程可以被封裝成 Task 並丟進任務佇列,只是不同任務攜帶不同的頁面地址、正規表示式、Cleaning 回撥函式等屬性,至於這些任務在具體執行時如何排程,就丟給事件迴圈去操心好了(這也是使用 asyncio 的一條基本原則):

以上就是非同步爬蟲的基本結構,有一點需要約定好的是所有的 Cleaning 方法必須以列表形式返回清洗之後的結果,且下一級頁面入口必須在第一位(最後一頁除外)。接下來做一個簡單的測試,以豆瓣電影分類頁面為入口,進入該類別列表,最後進入電影詳情頁面,並提取電影時長和評分:

Python 非同步網路爬蟲(2)

從上面的執行的結果可以看出,正規表示式有時候不能(或不便)直接精確過濾我們所需內容,因此可以在 Cleaning 函式中進行清理(如去掉多餘 Tag 或空位符等),另外:

  • 不像 Flask,這裡通過 register 註冊方法的順序必須與頁面處理順序保持一致
  • Sanic 一樣,註冊方法必須也是 Coroutine (async def),同時可以在其中非同步執行資料庫儲存操作;
  • 上級頁面資訊實際上可以通過擴充套件 Node 直接傳遞給下級頁面,這在某些相關頁面中甚至是必須的;

總結

抽象這一框架的目的主要有以下幾點:

  1. 學習使用 asyncio 庫及基於協程的非同步;
  2. 將網路爬蟲的編寫過程聚焦到頁面關係分析精確正規表示式少量資料清理上;
  3. 簡化使用,降低學習成本。

仍有以下內容需要完成:

  1. 錯誤捕捉與 logging,讓除錯過程更簡單;
  2. 尋找不適應該框架的情況,進行 upgrade;
  3. 效能測試;
  4. 完善瀏覽器模擬:Headers、proxy、Referer等……

未完待續。

參考

  1. Sanic
  2. Asyncio Doc::Producer/consumer

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

Python 非同步網路爬蟲(2)

相關文章