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

作者:xlzd

鏈接:https://zhuanlan.zhihu.com/p/20435541

來源:知乎

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

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

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

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

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

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

#!/usr/bin/env python# encoding=utf-8importrandom,time,Queuefrommultiprocessing.managersimportBaseManager# 發送任務和接收結果的隊列:task_queue,result_queue=Queue.Queue(),Queue.Queue()# 從BaseManager繼承的QueueManager:classQueueManager(BaseManager):pass# 把兩個Queue都注冊到網絡上, callable參數關聯了Queue對象:QueueManager.register('get_task_queue',callable=lambda:task_queue)QueueManager.register('get_result_queue',callable=lambda:result_queue)# 綁定端口9999, 設置驗證碼'crawler':manager=QueueManager(address=('',9999),authkey='crawler')# 啟動Queue:manager.start()# 獲得通過網絡訪問的Queue對象:task=manager.get_task_queue()result=manager.get_result_queue()# 將一千萬網頁頁碼放進去:foriinxrange(10000000):print('Put task%d...'%n)task.put(i)# 從result隊列讀取結果:print('Try get results...')foriinxrange(10000000):r=result.get(timeout=10)print('Result:%s'%r)# 關閉:manager.shutdown()

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

#!/usr/bin/env python# encoding=utf-8importtime,sys,Queuefrommultiprocessing.managersimportBaseManager# 創建類似的QueueManager:classQueueManager(BaseManager):pass# 由于這個QueueManager只從網絡上獲取Queue,所以注冊時只提供名字:QueueManager.register('get_task_queue')QueueManager.register('get_result_queue')# 端口和驗證碼m=QueueManager(address=('127.0.0.1',9999),authkey='crawler')# 從網絡連接:m.connect()# 獲取Queue的對象:task=m.get_task_queue()result=m.get_result_queue()# 從task隊列取任務,并把結果寫入result隊列:whileTrue:page=task.get(timeout=10)# 獲取任務crawl(page)# 抓取對應頁面并存儲result.put(page)# 匯報任務# 處理結束:print('worker exit.')

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

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

小結

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容