Web crawler with Python - 06.海量數據的抓取策略

(P.S.你也可以在我的博客閱讀這篇文章)

到目前為止,在沒有遇到很強烈反爬蟲的情況下,我們已經可以正常抓取一般的網頁了(對於發爬機制很嚴格的、JavaScript動態生成的內容,我們將在後面探討)。可是,目前的抓取僅僅可以針對數據量比較小的網站,對於那種動輒上百萬甚至上億數據量的網站,我們又應該如何設計爬蟲結構,才能儘可能快且可控地抓取數據呢?

假設我們有這樣一個需求——有一個網站有一億條數據,按照每頁10條分布在一千萬網頁中,我們的目的是要抓到自己的資料庫中。這時候,你會發現我們之前一頁一頁循環抓取的策略會顯得力不從心了,就算一個頁面的網路請求+解析+存儲只需要1秒,那也是需要整整一千萬秒!那麼,在應對這樣的需求的時候,我們又應該怎樣處理呢?

從單個程序的效率來講,使用多線程或者協程是一個不錯的解決方案。雖然由於Python中GIL的原因,多線程並不能利用到多核的優勢,但是爬蟲這種IO密集型程序,大多數時間是在等待——等待網路IO、等待資料庫IO、等待文件IO等,所以多線程在大部分情況下是可以滿足我們想要為爬蟲程序加速的需求的。多線程在擁有這些優勢的同時,同樣存在一些劣勢:不方便利用多台機器的資源來一起抓取目標網站,因為機器間的通信會比較複雜。

這時候,我們需要考慮另一種結構:將其中一台機器專門處理任務的分發,剩下的機器用於對應任務的數據抓取和存儲工作(這兩部甚至也可以分離)。所謂「任務」,即是我們的爬蟲從宏觀上可以分割的最小單元。比如對於上面的例子,那麼就是這一千萬個網頁的頁碼。我們在一台機器中保存一千萬個頁碼,剩下的機器則通過請求這台機器獲取一個任務(頁碼),然後發出請求抓取這個頁碼對應頁面的東西,存儲之後,繼續上面的步驟,請求新的頁碼......

這樣的過程其實是講程序分離為兩個不同的模塊,用Python的對應簡單實現如下:

#!/usr/bin/env pythonn# encoding=utf-8nnimport random, time, Queuenfrom multiprocessing.managers import BaseManagernn# 發送任務和接收結果的隊列:ntask_queue, result_queue = Queue.Queue(), Queue.Queue()nn# 從BaseManager繼承的QueueManager:nclass QueueManager(BaseManager): passnnn# 把兩個Queue都註冊到網路上, callable參數關聯了Queue對象:nQueueManager.register(get_task_queue, callable=lambda: task_queue)nQueueManager.register(get_result_queue, callable=lambda: result_queue)nn# 綁定埠9999, 設置驗證碼crawler:nmanager = QueueManager(address=(, 9999), authkey=crawler)n# 啟動Queue:nmanager.start()nn# 獲得通過網路訪問的Queue對象:ntask = manager.get_task_queue()nresult = manager.get_result_queue()nn# 將一千萬網頁頁碼放進去:nfor i in xrange(10000000):n print(Put task %d... % n)n task.put(i)nn# 從result隊列讀取結果:nprint(Try get results...)nfor i in xrange(10000000):n r = result.get(timeout=10)n print(Result: %s % r)n# 關閉:nmanager.shutdown()n

這裡我們使用Python自帶的BaseManager來處理分發和回收任務(注意:這種方式非常難用且不安全,這裡只是舉例,後面會提供更好的解決方案),然後,我們的爬蟲程序只需要接收一個頁碼作為參數,抓取這個頁碼的對應數據就好了:

#!/usr/bin/env pythonn# encoding=utf-8nnimport time, sys, Queuenfrom multiprocessing.managers import BaseManagernn# 創建類似的QueueManager:nclass QueueManager(BaseManager): passnn# 由於這個QueueManager只從網路上獲取Queue,所以註冊時只提供名字:nQueueManager.register(get_task_queue)nQueueManager.register(get_result_queue)nn# 埠和驗證碼nm = QueueManager(address=(127.0.0.1, 9999), authkey=crawler)n# 從網路連接:nm.connect()n# 獲取Queue的對象:ntask = m.get_task_queue()nresult = m.get_result_queue()nn# 從task隊列取任務,並把結果寫入result隊列:nwhile True:n page = task.get(timeout=10) # 獲取任務n crawl(page) # 抓取對應頁面並存儲n result.put(page) # 彙報任務n# 處理結束:nprint(worker exit.)n

上面的代碼參考了廖雪峰的博客。通過如上的方式,我們可以在一台機器上啟動服務端,另一台或多台機器上啟動爬取端(這個兩個部分可以跑在同一台機器,所以如果你只有一台電腦一樣可以測試效果)。通過這樣的方式,我們便可以方便地通過在一台機器多啟動幾個爬蟲實例(worker)或者新增機器的方式來加快抓取速度,在新增worker的時候,並不會影響到其他已存在worker的抓取,且不會重複抓取浪費資源,還不需要機器間兩兩通信。

當然,上面的代碼只是用於演示這樣的邏輯。如果在真實的應用場景下這樣使用,其實是非常麻煩的:服務端會一次性將任務寫到task queue中,內存佔用較高甚至可能OOM,且在抓取的過程中不容易監測進度。所以,我們可以使用MessageQueue在替代我們的服務端(處理任務分發)。我們將消息按照上面的邏輯寫入到一個MessageQueue中(如ActiveMQ、RabbitMQ等),然後爬蟲端連接這個MQ,一次取出一條(或者多條)頁碼進行抓取和存儲。這樣的優勢在於:內存佔用更優,服務端穩定性更好,抓取過程中容易監測進度,更方便任務的增加與刪除。

小結

這篇博客從理論上總結了在抓取較大量數據時的一個解決方案,之所以稱為從理論上,因為在看完博客之後,你還需要實際動手完成博客所講:搭建一個MQ、編寫代碼將任務插入MQ、編寫代碼從MQ取出消息並調用爬蟲處理。下一節,我們將總結一些常見的反爬蟲機制及相關處理方案。


推薦閱讀:

致敬廖雪峰老師~

TAG:xlzd杂谈 | 我的博客 | 这篇文章 | 廖雪峰 |