我們知道,非同步IO(asyncio)非常適合使用在網路請求的場景,也就是說它很適合在爬蟲中應用。
但是,如果我們只是特定抓取某一個網站,而且該網站對IP訪問頻率做了限制,那麼asyncio並沒有什麼優勢,並且不如同步請求的爬蟲的邏輯更清晰、實現更方便。
不過,我們要是抓幾千家新聞網站的新聞呢?面對這麼多的目標網站,我們的爬蟲可以透過非同步IO同時請求這些網站,並且新聞網站幾乎都有這樣一個特點:對爬蟲敞開大門,毫不設防。
目標網站的大門敞開著,就看你如何把爬蟲程式寫得極度高效,榨乾你伺服器和網路資源,最大限度的提升爬蟲的效率。這時候,非同步IO可以毫不猶豫的登場了。
也就是說,我們可以構建一個“大規模非同步新聞爬蟲”。這樣的一個新聞爬蟲就是通非同步IO實現大規模化,“大規模”指的是單臺機器單日能下載幾百萬甚至上千萬的新聞網頁,也可以是多臺機器以分散式方式下載更多的網頁。
要實現這樣的新聞爬蟲,需要合理的模組化實現,不同的模組負責不同的功能,它們主要是:網址池,爬取器,資料儲存,內容提取器這四大模組。接下來,我們就詳細探究一下各個模組的功能和實現。
一、網址池(UrlPool)
一個網頁對應這一個網址(URL),我們要抓的幾千家新聞網站包含的網頁可以有幾億甚至上百億(近十年內的新聞),每個新聞網頁內可能連結了其它新聞網頁,這種錯綜複雜的連結關係,導致爬蟲在抓取過程中經常反覆遇到同一個URL,那麼判斷這個URL是否已經被成功抓取至關重要,這決定著爬蟲是否要重複勞動做無用功。
管理好這些URL,成為整個爬蟲的關鍵。為此,我們設計一個網址池(UrlPool)來進行URL管理。
這個 UrlPool 是一個典型的“生產者-消費者”模型:
模仿這個模型就得到UrlPool的模型:
從網址池的使用目的出發來設計網址池的介面,它應該具有以下功能:
- 往池子裡面新增URL;
- 從池子裡面取URL以下載;
- 池子內部要管理URL狀態;
URL的狀態有以下4種:
- 已經下載成功
- 下載多次失敗無需再下載
- 正在下載
- 下載失敗要再次嘗試
前兩個是永久狀態,也就是已經下載成功的不再下載,多次嘗試後仍失敗的也就不再下載,它們需要永久儲存起來,以便爬蟲重啟後,這種永久狀態記錄不會消失,已經成功下載的URL不再被重複下載。永久儲存的方法有很多種:
比如,直接寫入文字檔案,但它不利於查詢某個URL是否已經存在文字中;
比如,直接寫入MySQL等關係型資料庫,它利用查詢,但是速度又比較慢;
比如,使用key-value資料庫,查詢和速度都符合要求,是不錯的選擇!
我們選用LevelDB來作為URL狀態的永久儲存。LevelDB是Google開源的一個key-value資料庫,速度非常快,同時自動壓縮資料。
而後面兩個狀態“正在下載”和“下載失敗要再次嘗試”的URL,狀態變化較快,可以放在記憶體中,比如用Python的dict。
由此,我們的UrlPool的儲存包括leveldb和一些在記憶體的dict。
二、抓取器(crawler)
有了UrlPool,抓取器的實現就方便多了,它的工作就是從UrlPool裡面拿出URL,然後去下載網頁,再從網頁裡面提取新的URL放到UrlPool裡面。所以說,這個抓取器既是URL的“消費者”,又是URL的“生產者”。
抓取器的這個過程應該是在一個while迴圈,只要網址池裡面有待抓取的URL,那麼這迴圈就不該結束。透過使用asyncio可以大大提高這個迴圈的效率,每次迴圈都能同時下載多個網頁。
asyncio對每個下載都會產生一個協程,為了讓協程的數量匹配硬體資源(比如CPU比較差,網路頻寬很小,那麼協程數量就不能太多),需要記錄當前開啟的下載協程的數量,當這個數量超過預設的閾值時,while迴圈暫停一定時間。這個暫停時段內,會有很多下載協程完成,完成的協程會減小這個數量,從而在下次迴圈開始時,又可以產生新的下載協程。
另外,下載協程還要做的工作有:
- 儲存網頁;
- 設定它下載的URL在UrlPool中的狀態;
- 提取網頁中的URL放入UrlPool;
- 最後把當前協程數量減1。
寫好這個抓取器,要注意的細節很多,比如從網頁中提取的URL很多,這些URL都要抓取嗎?當然不是,這URL有可能是連結到別的非新聞網站,比如微博。對於提取到的URL不管三七二十一就放入UrlPool進行抓取,會導致爬蟲抓取的網頁成幾何級數增長,並且大都是非新聞網頁,導致有效資料的抓取效率嚴重下降。
即使是新聞網站,也可能連結到它的非新聞頻道。所以,界定URL是否是新聞網頁是個重點,但也是個難點。
首先,我們抓取前,要先收集一批新聞網站的網址,它們域名之外的URL不抓;
其次,確認抓取這些網站的某些頻道,其它頻道不抓;
還有一個比較重要的點就是對URL的清洗,先看下面這兩個URL有什麼不同:
http://news.ifeng.com/a/20181106/60146589_0.shtml?_zbs_baidu_news
http://news.ifeng.com/a/20181106/60146589_0.shtml
第一個多了個引數,但是它們都是指向同一個網頁。在新聞網站中有很多這樣的URL,需要進行清洗,把第一種邊成第二種。實際上,新聞網頁的url幾乎都是靜態化的,即以 .html、.htm、.shtml 結尾,其後面的引數對網頁內容沒有影響,只是對後臺日誌分析時有用。我們可以透過這樣的規則把問號後面的引數去掉。當然,也不絕對符合這樣的規則,這也是難點所在。
對URL的判斷還有很多,這些都是為了減少無效抓取的措施,提供有效抓取效率。在比如,一些通告型的新聞網頁還會附上word文件、掃描件的圖片、PDF之類的文件的下載URL,它們也不是新聞網頁,也不需要下載。
講到這裡,我們從一個看似簡單的新聞抓取任務中剖析出來了很多難點,如果把這些難點都有效的解決就可以得到一個高效的抓取器了,這對於硬體資源和網路頻寬有限的環境尤為重要。如果你的硬體資源和網路相對富裕,對URL的判別可以放寬,多抓一點也無所謂。
三、資料儲存
抓來的東西總得要儲存下來才能用。抓來的就是網頁,也就是網頁的html程式碼,但這不是我們想要的最終資料,最終資料是從一個網頁裡面提取出來的新聞標題、釋出時間、來源網站、正文內容等結構化的資料。這些最終資料肯定是要儲存下來的。
那麼,我們要不要把html儲存起來呢?對於大規模的新聞爬蟲來說,存html很有必要。
其一,網站數量很多導致提取程式會經常出錯。爬蟲抓到html就提取只存最終資料的策略,一旦遇到提取程式出錯,爬蟲就會退出,抓取效率大大降低。如果爬蟲不退出,而是忽略提取錯誤,導致提取錯誤的網站內容不會進入最終資料庫,網頁做到了但沒有資料,白白浪費了抓取。
其二,開始只想提取少量資料,後面使用時發現少提取了某些資料。如果不儲存html,就要重新抓取,費時費力。
其三,抓取任務是IO密集型任務,而提取資料是CPU型任務。兩者混在一起,效率堪憂。而分開來的話,抓取用非同步IO提供效率。提取資料用多程式來提高效率。分而治之,都可以提高整個爬蟲系統的效率。
當然,儲存html的壞處也顯而易見:對硬碟空間的需求變大。具體在另一篇微信文章中有詳細闡述,見文末的延伸閱讀。一句話就是要壓縮儲存。
用於儲存的資料庫的選擇很多,比如MySQL、MongoDB等等,可以根據自己的喜好來選擇。使用了非同步IO抓取網頁,儲存也要用上非同步IO才能配合下載提高爬蟲的效率。
四、資料提取
爬蟲爬來一堆的網頁html程式碼,直接給應用開發者的話會把他們搞瘋掉的。必須要從中提取出來有用的資料並結構化的儲存下來才能方便的使用。
對應新聞網頁的提取,可以使用比較通用的演算法,也就是一個演算法對應上千家網站的不同網頁格式,這樣的演算法好處是實現起來不費力但費腦,幾乎一勞永逸。(參見延伸閱讀)
還有另外的思路是,對應每個網站寫一個提取模板,同一個網站的不同頻道可能需要不同的模板。這樣幾千家網站就要寫成千上萬個模板,費力不費腦。模板可以是正規表示式或其它自定義的規則,一旦網站改版,模板就要重新寫,維護這麼大數量的模板是一件比較費力的事情。
前面我們講到,資料提取是一件CPU密集型的任務,因為它主要是透過以下方法實現的:
(1)正規表示式,Python的re模組來寫正規表示式提取,CPU消耗在字串查詢匹配過程中;
(2)使用xpath,使用lxml庫把網頁生成DOM樹,然後就是對節點的查詢和匹配,這也是消耗CPU的過程。
CPU密集型的任務要提高效率主要就是多程式,而且提取網頁資料是單任務型的,每個網頁的提取與其它網頁沒有關係,可以透過一個程式池(數量與伺服器可用CPU核數相等)來並行提取資料。
升級:分散式爬蟲
以上新聞爬蟲的四大模組都可以獨立執行,構造成一個分散式爬蟲系統,可以提高爬取效率,當然結構複雜了維護難度也會增加。
(1)網址池做成獨立程式,稱為爬蟲Server,轉發分發和回收URL;
(2)抓取器做成獨立程式,稱為爬蟲Client,向Server請求URL,併傳送新提取的URL;
(3)資料儲存伺服器獨立於爬蟲,提高爬蟲的寫入效率;
(4)資料提取程式分佈執行在多臺伺服器,提高並行提取的能力。
最後,這個爬蟲架構總結為一張圖:
相關閱讀:
(1)詳細的非同步爬蟲設計教程,從Python爬蟲開始看起。
延伸閱讀:
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***