編者按:作者通過創建和擴展自己的分布式爬蟲,介紹了一系列工具和架構, 包括分布式體系結構、擴展、爬蟲禮儀、安全、調試工具、Python 中的多任務處理等。以下為譯文:
大概600萬條記錄,每個記錄有15個左右的字段。
這是我的數據分析項目要處理的數據集,但它的記錄有一個很大的問題:許多字段缺失,很多字段要么格式不一致或者過時了。換句話說,我的數據集非常臟。
但對于我這個業余數據科學家來說還是有點希望的-至少對于缺失和過時的字段來說。大多數記錄包含至少一個到外部網站的超鏈接,在那里我可能找到我需要的信息。因此,這看起來像一個完美的網絡爬蟲的用例。
在這篇文章中,你將了解我是如何構建和擴展分布式網絡爬蟲的,特別是我如何處理隨之而來的技術挑戰。
創建網絡爬蟲的想法令人興奮。因為,你知道,爬蟲很酷,對吧?
但我很快意識到,我的要求比我想象的要復雜得多:
好吧,我曾經在以前的工作中寫過很多爬蟲,但從沒有這么大的規模。所以對我來說這是個全新的領域。
我開始的設計是這樣的:
主要組件包括:
這樣我終會有
m*n個爬蟲,從而將負載分布在許多節點上。例如,4個主控制器,每個包含8個子進程的話,就相當于32個爬蟲。
另外,所有進程間通信都將使用隊列。 所以在理論上,它將很容易擴展。 我可以添加更多的主控制器,爬網率 - 一個性能指標- 會相應增加。
現在我有一個看起來不錯的設計,我需要選擇使用哪些技術。
但別誤會我的意思:我的目標不是提出一個完美的技術棧。 相反,我主要把它看作是一個學習的機會,也是一個挑戰 - 所以如果需要,我更愿意提出自制的解決方案。
我可以選擇AWS,但是我對DigitalOcean更熟悉,恰好它是更便宜的。 所以我用了幾個5美元每月的虛擬機(很省錢啦)。
requests庫是Python里處理HTTP請求的不二選擇。
當然,我需要從每個訪問過的網頁中提取所有的超鏈接。但我也需要在一些頁面抓取具體數據。
因此,我構建了自己的ETL管道,以便能夠以我所需的數據格式提取數據并進行轉換。
它可以通過配置文件進行定制,如下所示:
{
"name": "gravatar",
"url_patterns": [
{
"type": "regex",
"pattern": "^https?:\\/\\/(?:(?:www|\\w{2})\\.)?gravatar\\.com\\/(?!avatar|support|site|connect)\\w+\\/?$" }
],
"url_parsers": [
{
"description": "URLs in the 'Find Me Online' section.",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "http://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": "http://ul[@class='list-sites']//a/@href" } }
] }
],
"fields": [
{
"name": "name",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "http://div[@class='profile-description']/h2[@class='fn']/a/text()" } },
{
"type": "trim",
"parameters": {
} }
] },
{
"name": "location",
"processors": [
{
"type": "xpath",
"parameters": {
"expression": "http://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,查找和替換,等等)以獲取所需的確切數據,并轉成我想要的格式。因此,數據在存儲在其它地方之前被規范化,這是特別有用的,因為所有網站都是不同的,并且它們表示數據的方式各不相同。
手動創建所有這些映射花費了我很多時間,因為相關網站的列表非常長(數百個)。
初,我想知道RabbitMQ是否適合。 但是我決定,我不想要單獨的服務器來管理隊列。 我想要的一切都要如閃電般快速而且要獨立運行。
所以我用了ZeroMQ的push/pull隊列,我把它們加到了queuelib的FifoDiskQueue上,以便將數據保存到磁盤,以防系統崩潰。 另外,使用push/pull隊列可以確保使用輪轉調度算法將URL分派給主控制器。
了解ZeroMQ如何工作和理解其幾個極端案例花了我一段時間。 但是學習如何實現自己的消息傳遞真的很有趣,終是值得的,尤其是性能方面。
一個好的關系數據庫可以完成這項工作。 但是我需要存儲類似對象的結果(字段),所以我選了MongoDB。
加分項:MongoDB相當容易使用和管理。
我使用了 Python 的日志模塊,加上一個 RotatingFileHandler,每個進程生成一個日志文件。這對于管理由每個主控制器管理的各個爬蟲進程的日志文件特別有用。這也有助于調試。
為了監視各種節點,我沒有使用任何花哨的工具或框架。我只是每隔幾個小時使用 MongoChef連接到 MongoDB 服務器,按照我的計算, 檢查已經處理好的記錄的平均數。如果數字變小了,很可能意味著某件事情 (壞的) 正在發生,比如一個進程崩潰了或其他別的什么事情。
當然,你知道的-所有的血,汗水和眼淚都在這里。
Web爬蟲很可能會不止一次碰到同一個URL。但是你通常不想重新抓取它,因為網頁可能沒有改變。
為了避免這個問題,我在爬蟲程序調度器上使用了一個本地SQLite數據庫來存儲每個已爬過的URL,以及與其抓取日期相對應的時間戳。因此,每當新的URL出現時,調度程序會在SQLite數據庫中搜索該URL,以查看是否已經被爬過。如果沒有,則執行爬取。否則,就忽略掉。
我選擇SQLite是因為它的快速和易于使用。每個爬取URL附帶的時間戳對調試和事件回溯都非常有用,萬一有人對我的爬蟲提出投訴的話。
我的目標不是抓取整個網絡。相反,我想自動發現我感興趣的網址,并過濾掉那些沒用的網址。
利用前面介紹的ETL配置,我感興趣的URL被列入白名單。為了過濾掉我不想要的網址,我使用Alexa的100萬頂級網站列表中的前20K個網站。
這個概念很簡單:任何出現在前20K的網站有很大的可能性是無用的,如youtube.com或amazon.com。然而,根據我自己的分析,那些20K以外的網站更有可能有與我的分析相關,比如個人網站和博客等。
我不希望任何人篡改我的 DigitalOcean 虛擬機,所以:
好吧,也許我對安全有點過分了:) 但我是故意的:這不僅是一個很好的學習機會,而且也是保護我數據的一種非常有效的方法。
一個每月5美元的DigitalOcean 虛擬機只有512MB的內存,所以它可做的相當有限。 經過多次測試運行,我確定我的所有節點都應該有1GB的內存。 所以我在每個虛擬機上創建了一個512MB的交換文件。
我對自己實現初設計的工作速度感到驚訝。事情進展順利,我的早期測試顯示了我爬蟲的令人印象深刻的性能數字(爬網率) 。所以我很興奮,那是肯定的:)!
但后來,我看到Jim Mischel的一篇文章,完全改變了我的想法。事實是,我的爬蟲根本不 “客氣”。它不停地抓取網頁,沒有任何限制。當然,它抓取速度非???,但由于同樣的原因,網站管理員可能會封殺它。
那么,禮貌對網絡爬蟲意味著什么呢?
相當容易實現,對不對?
錯。我很快意識到,我爬蟲的分布式特性使事情復雜了許多。
除了我已經實現的需求之外,我還需要:
然而,第三點有些難度。實際上,分布式Web爬蟲怎么能:
# 保持一個單一的,新的robots.txt文件緩存,并與所有進程分享?
# 避免過于頻繁地下載同一個域的robots.txt文件?
# 跟蹤每個域上次爬網的時間,以尊重抓取延遲指令?
這意味著我的爬蟲會有一些重大的變化。
這是我更新后的設計。
與以前設計的主要區別是:
此時,我擔心這些變化會減慢我爬蟲的速度。實際上幾乎肯定會。但我沒有選擇,否則我的爬蟲會使其它網站超負載。
到目前為止,我所選擇的一切都保持不變,除了幾個關鍵的區別。
我選擇了 reppy 庫而不是 urllib 的 robotparser 是因為:
所以這是一個顯而易見的選擇。
我添加了第二個專門用于緩存內容的MongoDB服務器。在服務器上,我創建了兩個不同的數據庫,以避免任何可能的數據庫級鎖爭用2:
# 數據庫(1): 保存了每個域的上次爬網日期。
# 數據庫(2): 保存了每個域的 robots.txt 文件副本。
此外,我不得不小小修改一下修改 reppy 庫,使它緩存 robots.txt 文件在 MongoDB而不是在內存中。
在開發過程中,我花了大量的時間調試、分析和優化我的爬蟲。 實際上比我預期的時間多了很多。
除了掛掉3,內存泄漏4,變慢5,崩潰6和各種其他錯誤,我遇到了一系列意想不到的問題。
內存不是無限的資源 - 特別是在每月5美元的 DigitalOcean 虛擬機上。
事實上,我不得不限制在內存中一次存放多少個Python對象。 例如,調度員非??斓貙RL推送給主控制器,比后者爬取它們要快得多。 同時,主控制器通常有8個爬取進程可供使用,因此這些進程需要不斷地提供新的URL來爬取。
因此,我設置了一個閾值,確定主控制器上可以在內存中一次處理多少個URL。 這使我能夠在內存使用和性能之間取得平衡。
我很快意識到,我不能讓我的網絡爬蟲不受約束,否則它會抓取整個網絡-這根本不是我的目標。
因此,我將爬取深度限制為 1,這意味著只會抓取指定網址及其直接的子網址。這樣我的爬蟲可以自動發現它要特別尋找的大部分網頁。
我發現很多網站都是用JavaScript動態生成的。這意味著當你使用爬蟲下載任意網頁時,你可能沒有它的全部內容。也就是說,除非你能夠解釋和執行其腳本來生成頁面的內容。要做到這一點,你需要一個JavaScript引擎。
現在有很多方法可以解決這個問題,但我還是選擇了一個非常簡單的解決方案。我指定了一些主控制器,讓它們只抓取動態生成的網頁。
在那些主控制器上:
因此,我有幾個節點能夠抓取動態生成的網頁。
我已經知道,構建一個常規爬蟲意味著要處理各種奇怪的API極端案例。但是網絡爬蟲呢?
好吧,如果你把網絡看成是一個API,它肯定是巨大的,瘋狂的,非常不一致的:
以上只是網絡爬蟲需要處理的許多問題的一部分。
使用網絡爬蟲,你通常會對爬取速度感興趣,即每秒下載的網頁數量。例如,每4個主控制器,每個使用8個子進程,我估計我的爬蟲程序速率超過每秒40頁。
但我更感興趣的是,每小時我的原始數據集有多少記錄得到正確的解析。因為,正如前面提到的,我爬蟲的初目的是通過抓取丟失的字段或刷新過時的字段來填充數據集中的空白。
因此,使用與上面相同的配置,每小時它能夠解析大約2600條記錄。當然,這是一個令人失望的數字,但仍然足夠好了,因為大多數網頁都是無用的,而且過濾掉了。
如果我不得不從頭開始的話,有幾件事情,我會采用不同的方式:
我可能會選擇 RabbitMQ 或者 Redis, 而不是ZeroMQ, 主要是為了方便和易用性,即使他們比較慢。
我可能會使用 New Relic 和 Loggly 工具來監控我虛擬機上的資源并集中處理所有節點生成的日志。
我可能會把處理 robots.txt 文件和上次爬取日期的緩存去中心話來提高總體爬取速度。這意味著,對于每個爬蟲過程,將 MongoDB 服務器 #2 替換為在每個主控制器上的緩存。
下面是可能的體系結構:
總結:
幸運的是,ZeroMQ 支持前綴匹配,因此我可以根據域名將 URL 路由到特定的主控制器節點。我已經寫了一個主要基于 SQLite的持久化緩存。我肯定會重用它,以防止多個緩存占用太多的內存。
在這篇文章中,我們已經看到了如何構建一個分布式 web 爬蟲來填補臟數據集中的缺失數據。
起初,我并不期待這個項目變得如此龐大和復雜-大多數軟件項目可能都這樣。
但終我確實得到了回報,因為我學到了大量的東西: 分布式體系結構、擴展、禮儀、安全、調試工具、Python 中的多任務處理、robots.txt文件 等等。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。