以前沒有寫過,這是第一次寫,用詞不當,表述不清楚的地方請見諒。希望大家多提建議,謝謝。
網絡爬蟲常常被人所忽略,特別是和搜索引擎的光環相比,它似乎有些暗淡無光。我很少看見有詳細介紹爬蟲實現的文章或者文檔。然而,爬蟲其實是非常重要的一個系統,特別是在今天這個數據為王的時代。如果你是一個剛剛開始的公司或者項目,沒有任何原始的數據積累,那么通過爬蟲去Internet上找到那些有價值的數據再進行數據的清洗和整理,是一個可以快速得到數據的重要手段。
本文側重於爬蟲的系統設計和實現的部分細節,內容來源於三方面,一是我2013年3月份到10月份的一個數據采集的項目,對數據的要求是普通PC單機日下載量不少於80萬個有效頁面,第二則是來源於大量的網絡資料作參考,因為當時的公司的保密性不是很高,我們在開發的過程中遇到問題隨時可以求助網絡,然而我使用最多的是Google搜索和 http://stackoverflow.com/ 網站是我去的最多的地方,第三則是最近看的一本在華為公司的角落里存放很久的一本書《數學之美》。大部分關於爬蟲的系統方面的文獻都是2000年左右的,此后寥寥無幾,說明關於爬蟲的系統設計在十幾年前已經基本解決了。此外,既然本文側重於系統方面的問題,那么某些內容就不會涉及,比如如何抓取那些隱藏的web數據,如何抓取ajax的頁面,如何動態調整抓取頻率等等。
正文
一個正規的,完整的網絡爬蟲其實是一個很復雜的系統:首先,它是一個海量數據處理系統,因為它所要面對的是整個互聯網的網頁,即便是一個小型的,垂直類的爬蟲,一般也需要抓取上十億或者上百億的網頁;其次,它也是一個對性能要求很好的系統,可能需要同時下載成千上萬的網頁,快速的提取網頁中的url,對海量的url進行去重,等等;最后,它確實是一個不面向終端用戶的系統,所以,雖然也很需要穩定性,但偶然的當機並不會是災難,而且,不會出現類似訪問量激增這樣的情況,同時,如果短暫的時間內出現性能的下滑也不算是個問題,從這一點來看,爬蟲的系統設計在某些部分又變得簡單了許多。
上圖是一個爬蟲的系統框架,它基本上包括了一個爬蟲系統所需要的所有模塊。
任何一個爬蟲系統的設計圖,會發現都有一個環路,這個環代表着爬蟲大致的工作流程:根據URL將對應的網頁下載下來,然后提取出網頁中包含的URL,再根據這些新的URL下載對應的網頁,周而復始。爬蟲系統的子模塊都位於這個環路中,並完成某項特定的功能。
這些子模塊一般包括:
Fetcher:用於根據url下載對應的網頁;
DNS Resolver:DNS的解析;
Content Seen:網頁內容的去重;
Extractor:提取網頁中的url或者其它的一些內容;
URL Filter:過濾掉不需要下載的url;
URL Seen:url的去重;
URL Set:存儲所有的url;
URL Frontier:調度器,決定接下來哪些下載哪些url對應的網頁;
Fetcher和DNS Resolver
這兩個模塊是兩個非常簡單的獨立的服務:DNS Resolver負責域名的解析;Fetcher的輸入是域名解析后的url,返回的則是該url對應的網頁內容。對於任何一次網頁的抓取,它都需要調用這兩個模塊。
對一般的爬蟲,兩個模塊可以做得非常的簡單,甚至合並到一起。但是對於性能要求很高的系統,它們可能成為潛在的性能瓶頸。主要原因是無論是域名解析還是抓取,都是很耗時的工作。比如抓取網頁,一般的延遲都在百毫秒級別,如果遇上慢的網站,可能要幾秒甚至十幾秒,這導致工作線程會長時間的處於阻塞等待的狀態。如果希望Fetcher能夠達到每秒幾千個網頁甚至更高的下載,就需要啟動大量的工作線程。
因此,對於性能要求高的爬蟲系統,一般會采用epoll或者類似的技術將兩個模塊改成異步機制。另外,對於DNS的解析結果也會緩存下來,大大降低了DNS解析的操作。
Content Seen
Internet上的一些站點常常存在着鏡像網站(mirror),即兩個網站的內容一樣但網頁對應的域名不同。這樣會導致對同一份網頁爬蟲重復抓取多次。為了避免這種情況,對於每一份抓取到的網頁,它首先需要進入Content Seen模塊。該模塊會判斷網頁的內容是否和已下載過的某個網頁的內容一致,如果一致,則該網頁不會再被送去進行下一步的處理。這樣的做法能夠顯著的降低爬蟲需要下載的網頁數。
至於如果判斷兩個網頁的內容是否一致,一般的思路是這樣的:並不會去直接比較兩個網頁的內容,而是將網頁的內容經過計算生成FingerPrint(信息指紋),通常FingerPrint是一個固定長度的字符串,要比網頁的正文短很多。如果兩個網頁的FingerPrint一樣,則認為它們內容完全相同。
Extractor和Url Filter
Extractor的工作是從下載的網頁中將它包含的所有url提取出來。這是個細致的工作,你需要考慮到所有可能的url的樣式,比如網頁中常常會包含相對路徑的url,提取的時候需要將它轉換成絕對路徑。
Url Filter則是對提取出來的url再進行一次篩選。不同的應用篩選的標准是不一樣的,比如對於baidu/google的搜索,一般不進行篩選,但是對於垂直搜索或者定向抓取的應用,那么它可能只需要滿足某個條件的url,比如不需要圖片的url,比如只需要某個特定網站的url等等。Url Filter是一個和應用密切相關的模塊。
Url Seen
Url Seen用來做url去重。關於url去重之后會介紹,這里就不再詳談了。
對於一個大的爬蟲系統,它可能已經有百億或者千億的url,新來一個url如何能快速的判斷url是否已經出現過非常關鍵。因為大的爬蟲系統可能一秒鍾就會下載幾千個網頁,一個網頁一般能夠抽取出幾十個url,而每個url都需要執行去重操作,可想每秒需要執行大量的去重操作。因此Url Seen是整個爬蟲系統中非常有技術含量的一個部分。(Content Seen其實也存在這個問題)
Url Set
當url經過前面的一系列處理后就會被放入到Url Set中等待被調度抓取。因為url的數量很大,所以只有一小部分可能被放在內存中,而大部分則會寫入到硬盤。一般Url Set的實現就是一些文件或者是數據庫。
URL Frontier
URL Frontier之所以放在最后,是因為它可以說是整個爬蟲系統的引擎和驅動,組織和調用其它的模塊。
當爬蟲啟動的時候,Froniter內部會有一些種子url,它先將種子url送入Fetcher進行抓取,然后將抓取下來的網頁送入Extractor提取新的url,再將新的url去重后放入到Url Set中;而當Froniter內部的url都已經抓取完畢后,它又從Url Set中提取那些新的沒有被抓取過的url,周而復始。Frontier的調度實現有很多種,這里只介紹最常見的一種實現方法。
在此之前,需要先解釋一點,盡管在介紹Fetcher的時候我們說,好的Fetcher每秒能夠下載百千個網頁,但是對於某個特定的目標網站,比如www.sina.com,爬蟲系統對它的抓取是非常慢速的,十幾秒才會抓取一次,這是為了保證目標網站不至於被爬蟲給抓垮。
為了做到這一點,Frontier內部對於每個域名有一個對應的FIFO隊列,這個隊列保存了該域名下的url。Frontier每次都會從某個隊列中拿出一個url進行抓取。隊列會保存上一次被Frontier調用的時間,如果該時間距離現在已經超過了一定值,那么該隊列才可以再次被調用。
Frontier內部同時可能擁有成千上萬個這樣的隊列,它會輪詢的獲取一個可以被調用的隊列,然后從該隊列中pull一個url進行抓取。而一旦所有隊列中的url被消耗到一定程度,Frontier又會從Url Set中提取一批新的url放入對應的隊列。
分布式
當單機版的爬蟲性能不能滿足要求的時候,就應該考慮用多台機器組成分布式的爬蟲系統。分布式的爬蟲架構其實要比想象的簡單得多,一個朴素的做法是:假設有N台機器,每台機器上有運行了一個完整的爬蟲系統,每台機器的爬蟲在從Extractor模塊獲得新的url之后,根據url的域名進行hash然后取模N得到結果n,然后該url會被放入第n台機器的Url Set中。這樣,不同網站的url會被放在不同的機器上處理。
當時我們設計之處的目標是把爬蟲程序放到路由器這樣的設備上也可以正常、穩定的運行,因為不需要再設備上存放沒用的信息,所有的有用頁面通過socket通信被存放到指定的服務器中。但當我離開公司的時候,這個方案已經沒有人再提到了,因為我們發現這樣的做法,我們沒有硬件支持,而且影響了路由器的運行速度。
以上就是一個完整爬蟲的系統實現。當然,由於篇幅有限回避了一些細節。比如爬蟲抓取每個網站前需要先讀取該網站的robots.txt來判斷該網站是否允許被抓取(關於robots.txt的一下解釋可以參考http://www.cnblogs.com/yuzhongwusan/archive/2008/12/06/1348969.html);再比如,一些網站提供了sitemap,這樣可以直接從sitemap上獲取該網站的所有url;等等。還有一張來自網絡的抓取平台的結構圖,這個圖的結構和上面的那張圖的結構基本相同。