創建一個分散式網路爬蟲的故事
關鍵時刻,第一時間送達!
作者丨Benoit Bernard
譯者丨roy
編者按:作者通過創建和擴展自己的分散式爬蟲,介紹了一系列工具和架構, 包括分散式體系結構、擴展、爬蟲禮儀、安全、調試工具、Python 中的多任務處理等。以下為譯文:
大概600萬條記錄,每個記錄有15個左右的欄位。
這是我的數據分析項目要處理的數據集,但它的記錄有一個很大的問題:許多欄位缺失,很多欄位要麼格式不一致或者過時了。換句話說,我的數據集非常臟。
但對於我這個業餘數據科學家來說還是有點希望的-至少對於缺失和過時的欄位來說。大多數記錄包含至少一個到外部網站的超鏈接,在那裡我可能找到我需要的信息。因此,這看起來像一個完美的網路爬蟲的用例。
在這篇文章中,你將了解我是如何構建和擴展分散式網路爬蟲的,特別是我如何處理隨之而來的技術挑戰。
初始需求
創建網路爬蟲的想法令人興奮。因為,你知道,爬蟲很酷,對吧?
但我很快意識到,我的要求比我想像的要複雜得多:
給定指定 URL,爬蟲程序需要自動發現特定記錄中缺失欄位的值。因此,如果一個網頁不包含我正在尋找的信息,爬蟲程序需要跟蹤出站鏈接,直到找到該信息。
它需要是某種爬蟲和抓取的混合功能,因為它必須同時跟蹤出站鏈接並從網頁中提取特定信息。
整個程序需要分散式處理,因為有可能有數億個URL需要訪問。
抓取的數據需要存儲在某處,很可能是在資料庫中。
爬蟲程序需要7*24小時不間斷工作,所以不能在我的筆記本電腦上運行它。
我不希望在雲服務上花費太多 1。
需要用Python編碼,這是我選擇的語言。
好吧,我曾經在以前的工作中寫過很多爬蟲,但從沒有這麼大的規模。所以對我來說這是個全新的領域。
初始設計
我最開始的設計是這樣的:
主要組件包括:
一個爬蟲調度器,負責把URL分派給 m 個爬蟲主控制器,並從它們收集結果(欄位)。
m個爬蟲主控制器,負責管理 n 個子進程。這些子過程執行實際的爬取操作。為方便起見,我把他們稱為爬蟲。
一個資料庫伺服器,負責存儲初始URL和提取的欄位。
這樣我最終會有m*n個爬蟲,從而將負載分布在許多節點上。例如,4個主控制器,每個包含8個子進程的話,就相當於32個爬蟲。
另外,所有進程間通信都將使用隊列。 所以在理論上,它將很容易擴展。 我可以添加更多的主控制器,爬網率 - 一個性能指標- 會相應增加。
初始實現
現在我有一個看起來不錯的設計,我需要選擇使用哪些技術。
但別誤會我的意思:我的目標不是提出一個完美的技術棧。 相反,我主要把它看作是一個學習的機會,也是一個挑戰 - 所以如果需要,我更願意提出自製的解決方案。
1. 雲託管
我可以選擇AWS,但是我對DigitalOcean更熟悉,恰好它是更便宜的。 所以我用了幾個5美元每月的虛擬機(很省錢啦)。
2. HTTP 庫
requests庫是Python里處理HTTP請求的不二選擇。
3. ETL 管道
當然,我需要從每個訪問過的網頁中提取所有的超鏈接。但我也需要在一些頁面抓取具體數據。
因此,我構建了自己的ETL管道,以便能夠以我所需的數據格式提取數據並進行轉換。
它可以通過配置文件進行定製,如下所示:
{
"name": "gravatar",
"url_patterns": [
{
"type": "regex",
"pattern": "^https?:\/\/(?:(?:www|\w)\.)?gravatar\.com\/(?!avatar|support|site|connect)\w+\/?$"
}
],
"url_parsers": [
{
"description": "URLs in the Find Me Online section.",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "//h3[contains(text(), Find Me Online )]/following-sibling::ul[@class= list-details ][1]//a/@href"
}
}
]
},
{
"description": "URLs in the Websites section.",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "//ul[@class= list-sites ]//a/@href"
}
}
]
}
],
"fields": [
{
"name": "name",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "//div[@class= profile-description ]/h2[@class= fn ]/a/text()"
}
},
{
"type": "trim",
"parameters": {
}
}
]
},
{
"name": "location",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "//div[@class= profile-description ]/p[@class= location ]/text()"
}
},
{
"type": "trim",
"parameters": {
}
}
]
}
]
}
你在上面看到的是一個Gravatar 用戶個人資料頁面的映射。它告訴爬蟲程序應該從這些頁面中抓取什麼數據以及如何抓取:
url_patterns 定義了與當前頁URL 進行試探性匹配的模式。如果有一個匹配,那麼當前頁面確實是Gravatar的用戶配置文件。
url_parsers 定義了能夠在頁面中抓取特定URL的解析器,比如那些指向用戶的個人網站或社交媒體資料的URL。
fields 欄位定義了要從頁面抓取的數據。在Gravatar的用戶配置文件里,我想抓取用戶的全名和位置信息。
url_parsers 和 fields 都包含了一系列針對 web 頁面 HTML 數據的處理器。它們執行轉換(XPath,JSONPath,查找和替換,等等)以獲取所需的確切數據,並轉成我想要的格式。因此,數據在存儲在其它地方之前被規範化,這是特別有用的,因為所有網站都是不同的,並且它們表示數據的方式各不相同。
手動創建所有這些映射花費了我很多時間,因為相關網站的列表非常長(數百個)。
4. 消息處理
最初,我想知道RabbitMQ是否適合。 但是我決定,我不想要單獨的伺服器來管理隊列。 我想要的一切都要如閃電般快速而且要獨立運行。
所以我用了ZeroMQ的push/pull隊列,我把它們加到了queuelib的FifoDiskQueue上,以便將數據保存到磁碟,以防系統崩潰。 另外,使用push/pull隊列可以確保使用輪轉調度演算法將URL分派給主控制器。
了解ZeroMQ如何工作和理解其幾個極端案例花了我一段時間。 但是學習如何實現自己的消息傳遞真的很有趣,最終是值得的,尤其是性能方面。
5. 存儲處理
一個好的關係資料庫可以完成這項工作。 但是我需要存儲類似對象的結果(欄位),所以我選了MongoDB。
加分項:MongoDB相當容易使用和管理。
6. 日誌記錄和監控
我使用了 Python 的日誌模塊,加上一個 RotatingFileHandler,每個進程生成一個日誌文件。這對於管理由每個主控制器管理的各個爬蟲進程的日誌文件特別有用。這也有助於調試。
為了監視各種節點,我沒有使用任何花哨的工具或框架。我只是每隔幾個小時使用 MongoChef連接到 MongoDB 伺服器,按照我的計算, 檢查已經處理好的記錄的平均數。如果數字變小了,很可能意味著某件事情 (壞的) 正在發生,比如一個進程崩潰了或其他別的什麼事情。
當然,你知道的-所有的血,汗水和眼淚都在這裡。
7. 管理已經爬過的URLs
Web爬蟲很可能會不止一次碰到同一個URL。但是你通常不想重新抓取它,因為網頁可能沒有改變。
為了避免這個問題,我在爬蟲程序調度器上使用了一個本地SQLite資料庫來存儲每個已爬過的URL,以及與其抓取日期相對應的時間戳。因此,每當新的URL出現時,調度程序會在SQLite資料庫中搜索該URL,以查看是否已經被爬過。如果沒有,則執行爬取。否則,就忽略掉。
我選擇SQLite是因為它的快速和易於使用。每個爬取URL附帶的時間戳對調試和事件回溯都非常有用,萬一有人對我的爬蟲提出投訴的話。
8. URL過濾
我的目標不是抓取整個網路。相反,我想自動發現我感興趣的網址,並過濾掉那些沒用的網址。
利用前面介紹的ETL配置,我感興趣的URL被列入白名單。為了過濾掉我不想要的網址,我使用Alexa的100萬頂級網站列表中的前20K個網站。
這個概念很簡單:任何出現在前20K的網站有很大的可能性是無用的,如youtube.com或amazon.com。然而,根據我自己的分析,那些20K以外的網站更有可能有與我的分析相關,比如個人網站和博客等。
9. 安全
我不希望任何人篡改我的 DigitalOcean 虛擬機,所以:
我關閉了每個虛擬機上使用 iptables的所有埠。我選擇性地打開了我絕對需要的埠(80、443、22、27017等)。
我在 MongoDB 上啟用了 SSL 身份驗證,因此只有擁有適當證書的用戶才能登錄。
我在所有虛擬機上都使用了加密的磁碟。
我在每個虛擬機上都啟用了fail2ban,以阻止多次失敗的登錄請求。
我在所有虛擬機上都配置了基於SSH密鑰的身份驗證。
我在 ZeroMQ 中啟用了 SSL身份驗證。
好吧,也許我對安全有點過分了:) 但我是故意的:這不僅是一個很好的學習機會,而且也是保護我數據的一種非常有效的方法。
10. 內存
一個每月5美元的DigitalOcean 虛擬機只有512MB的內存,所以它可做的相當有限。 經過多次測試運行,我確定我的所有節點都應該有1GB的內存。 所以我在每個虛擬機上創建了一個512MB的交換文件。
禮貌…是啥?
我對自己實現最初設計的工作速度感到驚訝。事情進展順利,我的早期測試顯示了我爬蟲的令人印象深刻的性能數字(爬網率) 。所以我很興奮,那是肯定的:)!
但後來,我看到Jim Mischel的一篇文章,完全改變了我的想法。事實是,我的爬蟲根本不 「客氣」。它不停地抓取網頁,沒有任何限制。當然,它抓取速度非常快,但由於同樣的原因,網站管理員可能會封殺它。
那麼,禮貌對網路爬蟲意味著什麼呢?
它必須通過適當的用戶代理字元串標識自己。
它必須尊重 robots.txt 的規則。
它不能太快地向網站發送連續請求。
相當容易實現,對不對?
錯。我很快意識到,我爬蟲的分散式特性使事情複雜了許多。
更新的要求
除了我已經實現的需求之外,我還需要:
創建一個頁面描述我的爬蟲在做什麼。
在我的爬蟲所做的每一個HTTP請求中傳遞User-Agent頭,並包含一個指向我創建的說明頁面的鏈接。
為每個域定期下載robots.txt,並根據以下條件檢查是否允許抓取URL:
包含/排除規則。
抓取延遲指令。在不存在的情況下,對同一域的後續請求需要以保守的秒數(例如15秒)間隔開。這是為了確保爬蟲不會在網站上造成額外的負載。
然而,第三點有些難度。實際上,分散式Web爬蟲怎麼能:
保持一個單一的,最新的robots.txt文件緩存,並與所有進程分享?
避免過於頻繁地下載同一個域的robots.txt文件?
跟蹤每個域上次爬網的時間,以尊重抓取延遲指令?
這意味著我的爬蟲會有一些重大的變化。
更新的設計
這是我更新後的設計。
與以前設計的主要區別是:
將為每個域下載Robots.txt文件。
Robots.txt 文件將被緩存在資料庫中。每隔一小時左右,每個文件將根據需要單獨失效 並根據域重新下載。 這樣做是為了確保爬蟲能夠遵守robots.txt文件里的任何更改。
最後一個抓取日期也將被緩存到每個域的資料庫中。這將用作參考,以遵守 robots.txt 中包含的抓取延遲指令。
此時,我擔心這些變化會減慢我爬蟲的速度。實際上幾乎肯定會。但我沒有選擇,否則我的爬蟲會使其它網站超負載。
更新後的實現
到目前為止,我所選擇的一切都保持不變,除了幾個關鍵的區別。
1. 處理 robots.txt
我選擇了 reppy 庫而不是 urllib 的 robotparser 是因為:
它支持抓取延遲(crawl-delay)指令。
它會自動處理已過期的robots.txt文件的下載。
它支持目錄包含規則 (即允許指令),基於Google 自己的 robots.txt 的實現。 這些規則在網路上的robots.txt文件中很常見。
所以這是一個顯而易見的選擇。
2. 緩存 robots.txt 和上次爬網日期
我添加了第二個專門用於緩存內容的MongoDB伺服器。在伺服器上,我創建了兩個不同的資料庫,以避免任何可能的資料庫級鎖爭用2:
資料庫(1): 保存了每個域的上次爬網日期。
資料庫(2): 保存了每個域的 robots.txt 文件副本。
此外,我不得不小小修改一下修改 reppy 庫,使它緩存 robots.txt 文件在 MongoDB而不是在內存中。
處理 bug 和問題
在開發過程中,我花了大量的時間調試、分析和優化我的爬蟲。 實際上比我預期的時間多了很多。
除了掛掉3,內存泄漏4,變慢5,崩潰6和各種其他錯誤,我遇到了一系列意想不到的問題。
1. 內存管理
內存不是無限的資源 - 特別是在每月5美元的 DigitalOcean 虛擬機上。
事實上,我不得不限制在內存中一次存放多少個Python對象。 例如,調度員非常快地將URL推送給主控制器,比後者爬取它們要快得多。 同時,主控制器通常有8個爬取進程可供使用,因此這些進程需要不斷地提供新的URL來爬取。
因此,我設置了一個閾值,確定主控制器上可以在內存中一次處理多少個URL。 這使我能夠在內存使用和性能之間取得平衡。
2. 瓶頸
我很快意識到,我不能讓我的網路爬蟲不受約束,否則它會抓取整個網路-這根本不是我的目標。
因此,我將爬取深度限制為 1,這意味著只會抓取指定網址及其直接的子網址。這樣我的爬蟲可以自動發現它要特別尋找的大部分網頁。
3. 動態生成的內容
我發現很多網站都是用JavaScript動態生成的。這意味著當你使用爬蟲下載任意網頁時,你可能沒有它的全部內容。也就是說,除非你能夠解釋和執行其腳本來生成頁面的內容。要做到這一點,你需要一個JavaScript引擎。
現在有很多方法可以解決這個問題,但我還是選擇了一個非常簡單的解決方案。我指定了一些主控制器,讓它們只抓取動態生成的網頁。
在那些主控制器上:
我安裝了谷歌瀏覽器和Chrome驅動程序。
我安裝了Selenium的Python綁定。
我安裝了xvfb來模擬監視器的存在,因為Chrome有一個GUI,而CentOS默認沒有。
因此,我有幾個節點能夠抓取動態生成的網頁。
4. 極端情況
我已經知道,構建一個常規爬蟲意味著要處理各種奇怪的API極端案例。但是網路爬蟲呢?
好吧,如果你把網路看成是一個API,它肯定是巨大的,瘋狂的,非常不一致的:
頁面並非都是以同樣的方式構建的。
頁面通常包含無效字元(即與頁面編碼不兼容)。
伺服器經常返回各種HTTP錯誤(500,404,400等等),包括自定義的錯誤(999,有人能告訴我這是啥不?)。
伺服器經常無法訪問,導致超時。域名/網站可能不再存在,或者可能存在DNS問題,或者可能是負載過重,或者伺服器可能配置不正確或者…你明白的:)
有些頁面是巨大的,有幾十兆位元組或者更多7。這意味著,如果你一次下載完全,並將它們全部載入到內存中的話,你很可能會在某個時刻耗盡內存8。
伺服器有時返回不正確的HTML,或非HTML內容,如JSON、XML或其他內容。誰知道為什麼?!
網頁通常包含無效和不正確的URL。或你不想爬取的URL,比如像大的二進位文件(如PDF文件,視頻,等等)。
以上只是網路爬蟲需要處理的許多問題的一部分。
性能數據
使用網路爬蟲,你通常會對爬取速度感興趣,即每秒下載的網頁數量。例如,每4個主控制器,每個使用8個子進程,我估計我的爬蟲程序速率超過每秒40頁。
但我更感興趣的是,每小時我的原始數據集有多少記錄得到正確的解析。因為,正如前面提到的,我爬蟲的最初目的是通過抓取丟失的欄位或刷新過時的欄位來填充數據集中的空白。
因此,使用與上面相同的配置,每小時它能夠解析大約2600條記錄。當然,這是一個令人失望的數字,但仍然足夠好了,因為大多數網頁都是無用的,而且過濾掉了。
未來的改進
如果我不得不從頭開始的話,有幾件事情,我會採用不同的方式:
1. 消息傳遞
我可能會選擇 RabbitMQ 或者 Redis, 而不是ZeroMQ, 主要是為了方便和易用性,即使他們比較慢。
2. 監控/日誌
我可能會使用 New Relic 和 Loggly 工具來監控我虛擬機上的資源並集中處理所有節點生成的日誌。
3. 設計
我可能會把處理 robots.txt 文件和上次爬取日期的緩存去中心話來提高總體爬取速度。這意味著,對於每個爬蟲過程,將 MongoDB 伺服器 #2 替換為在每個主控制器上的緩存。
下面是可能的體系結構:
總結:
在每個主控制器節點上,每個爬蟲程序進程都將有自己的 robots.txt 文件 和上次爬取的日期緩存;這將替換集中式緩存 (MongoDB 伺服器 #2)。
由於這個原因,調度員需要將每個 URL 發送到一個非常特定的主控制器節點。
當接收到一個新的要爬取的URL,一個主控制器節點需要發送到一個非常特定的爬蟲。否則,不同主控制器下面的的多個爬蟲進程可能同時抓取完全相同的網站。我的爬蟲可能會被禁止,因為它沒有遵循 robots.txt 的規則。
幸運的是,ZeroMQ 支持前綴匹配,因此我可以根據域名將 URL 路由到特定的主控制器節點。我已經寫了一個主要基於 SQLite的持久化緩存。我肯定會重用它,以防止多個緩存佔用太多的內存。
最後的思考
在這篇文章中,我們已經看到了如何構建一個分散式 web 爬蟲來填補臟數據集中的缺失數據。
起初,我並不期待這個項目變得如此龐大和複雜-大多數軟體項目可能都這樣。
但最終我確實得到了回報,因為我學到了大量的東西: 分散式體系結構、擴展、禮儀、安全、調試工具、Python 中的多任務處理、robots.txt文件 等等。
現在,有一個問題,我沒有在我的文章里回答。哪一個數據集可以證明所有的工作都是正確的?這一切背後的原因是什麼?
這是你在我以後的文章中會看到的!
後記: 請在下面的評論欄中留下你的問題和意見!
更新(2017/09/19): 這篇文章發表在Reddit。它也發表在Python Weekly,Pycoders Weekly 和Programming Digest。如果你有機會訂閱他們,你不會失望的!謝謝大家的支持和反饋!
1: 我只花了35美元每月 (5美元/月/VM * 7 VMs = 35美元/月)。我曾想給文章取標題為「一個窮人關於創建一個分散式網路爬蟲的的建議」。
2: 現在回想起來,有2個不同的MongoDB資料庫可能是不必要的。這是因為在MongoDB 3 以上版本寫鎖是針對每個文件的,而不是針對每個資料庫。這似乎與3之前版本相反,據MongoDB的文檔和這個Stackoverflow答案。
3: 關於更多掛機的細節,請看這裡和這裡
4: 關於更多內存泄露的細節,請看這裡和這裡
5: 關於更多運行緩慢的細節,請看這裡
6: 關於更多崩潰的細節,請看這裡
7: 這就是你為什麼要按塊下載網頁
8: 有些網頁就是這樣設計的。其他的輸出一條錯誤信息或者看起來無限長的堆棧跟蹤信息。無論哪種方式,它們都很大!
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
※TIOBE 10 月編程語言排行榜:Swift 正過時?
※GitHub的12 個實用技巧,你 get 了幾個?
※基於REST微服務的5個最佳實踐
※程序員的進階之路
※國慶去哪裡人最多?用 Python 抓取的熱力圖告訴你!
TAG:CSDN |