網絡爬蟲是捜索引擎抓取系統的重要組成部分。爬蟲的主要目的是將互聯網上的網頁下載到本地造成一個或聯網內容的鏡像備份。這篇博客主要對爬蟲以及抓取系統進行一個簡單的概述。java
一個通用的網絡爬蟲的框架如圖所示:算法
網絡爬蟲的基本工做流程以下:api
1.首先選取一部分精心挑選的種子URL;服務器
2.將這些URL放入待抓取URL隊列;網絡
3.從待抓取URL隊列中取出待抓取在URL,解析DNS,而且獲得主機的ip,並將URL對應的網頁下載下來,存儲進已下載網頁庫中。此外,將這些URL放進已抓取URL隊列。框架
4.分析已抓取URL隊列中的URL,分析其中的其餘URL,而且將URL放入待抓取URL隊列,從而進入下一個循環。分佈式
對應的,能夠將互聯網的全部頁面分爲五個部分:網站
1.已下載未過時網頁
搜索引擎
2.已下載已過時網頁:抓取到的網頁其實是互聯網內容的一個鏡像與備份,互聯網是動態變化的,一部分互聯網上的內容已經發生了變化,這時,這部分抓取到的網頁就已通過期了。url
3.待下載網頁:也就是待抓取URL隊列中的那些頁面
4.可知網頁:尚未抓取下來,也沒有在待抓取URL隊列中,可是能夠經過對已抓取頁面或者待抓取URL對應頁面進行分析獲取到的URL,認爲是可知網頁。
5.還有一部分網頁,爬蟲是沒法直接抓取下載的。稱爲不可知網頁。
在爬蟲系統中,待抓取URL隊列是很重要的一部分。待抓取URL隊列中的URL以什麼樣的順序排列也是一個很重要的問題,由於這涉及到先抓取那個頁面,後抓取哪一個頁面。而決定這些URL排列順序的方法,叫作抓取策略。下面重點介紹幾種常見的抓取策略:
1.深度優先遍歷策略
深度優先遍歷策略是指網絡爬蟲會從起始頁開始,一個連接一個連接跟蹤下去,處理完這條線路以後再轉入下一個起始頁,繼續跟蹤連接。咱們如下面的圖爲例:
遍歷的路徑:A-F-G E-H-I B C D
2.寬度優先遍歷策略
寬度優先遍歷策略的基本思路是,將新下載網頁中發現的連接直接插入待抓取URL隊列的末尾。也就是指網絡爬蟲會先抓取起始網頁中連接的全部網頁,而後再選擇其中的一個連接網頁,繼續抓取在此網頁中連接的全部網頁。仍是以上面的圖爲例:
遍歷路徑:A-B-C-D-E-F G H I
3.反向連接數策略
反向連接數是指一個網頁被其餘網頁連接指向的數量。反向連接數表示的是一個網頁的內容受到其餘人的推薦的程度。所以,不少時候搜索引擎的抓取系統會使用這個指標來評價網頁的重要程度,從而決定不一樣網頁的抓取前後順序。
在真實的網絡環境中,因爲廣告連接、做弊連接的存在,反向連接數不能徹底等他我那個也的重要程度。所以,搜索引擎每每考慮一些可靠的反向連接數。
4.Partial PageRank策略
Partial PageRank算法借鑑了PageRank算法的思想:對於已經下載的網頁,連同待抓取URL隊列中的URL,造成網頁集合,計算每一個頁面的PageRank值,計算完以後,將待抓取URL隊列中的URL按照PageRank值的大小排列,並按照該順序抓取頁面。
若是每次抓取一個頁面,就從新計算PageRank值,一種折中方案是:每抓取K個頁面後,從新計算一次PageRank值。可是這種狀況還會有一個問題:對於已經下載下來的頁面中分析出的連接,也就是咱們以前提到的未知網頁那一部分,暫時是沒有PageRank值的。爲了解決這個問題,會給這些頁面一個臨時的PageRank值:將這個網頁全部入鏈傳遞進來的PageRank值進行彙總,這樣就造成了該未知頁面的PageRank值,從而參與排序。下面舉例說明:
5.OPIC策略策略
該算法實際上也是對頁面進行一個重要性打分。在算法開始前,給全部頁面一個相同的初始現金(cash)。當下載了某個頁面P以後,將P的現金分攤給全部從P中分析出的連接,而且將P的現金清空。對於待抓取URL隊列中的全部頁面按照現金數進行排序。
6.大站優先策略
對於待抓取URL隊列中的全部網頁,根據所屬的網站進行分類。對於待下載頁面數多的網站,優先下載。這個策略也所以叫作大站優先策略。
互聯網是實時變化的,具備很強的動態性。網頁更新策略主要是決定什麼時候更新以前已經下載過的頁面。常見的更新策略又如下三種:
1.歷史參考策略
顧名思義,根據頁面以往的歷史更新數據,預測該頁面將來什麼時候會發生變化。通常來講,是經過泊松過程進行建模進行預測。
2.用戶體驗策略
儘管搜索引擎針對於某個查詢條件可以返回數量巨大的結果,可是用戶每每只關注前幾頁結果。所以,抓取系統能夠優先更新那些現實在查詢結果前幾頁中的網頁,然後再更新那些後面的網頁。這種更新策略也是須要用到歷史信息的。用戶體驗策略保留網頁的多個歷史版本,而且根據過去每次內容變化對搜索質量的影響,得出一個平均值,用這個值做爲決定什麼時候從新抓取的依據。
3.聚類抽樣策略
前面提到的兩種更新策略都有一個前提:須要網頁的歷史信息。這樣就存在兩個問題:第一,系統要是爲每一個系統保存多個版本的歷史信息,無疑增長了不少的系統負擔;第二,要是新的網頁徹底沒有歷史信息,就沒法肯定更新策略。
這種策略認爲,網頁具備不少屬性,相似屬性的網頁,能夠認爲其更新頻率也是相似的。要計算某一個類別網頁的更新頻率,只須要對這一類網頁抽樣,以他們的更新週期做爲整個類別的更新週期。基本思路如圖:
通常來講,抓取系統須要面對的是整個互聯網上數以億計的網頁。單個抓取程序不可能完成這樣的任務。每每須要多個抓取程序一塊兒來處理。通常來講抓取系統每每是一個分佈式的三層結構。如圖所示:
最下一層是分佈在不一樣地理位置的數據中心,在每一個數據中內心有若干臺抓取服務器,而每臺抓取服務器上可能部署了若干套爬蟲程序。這就構成了一個基本的分佈式抓取系統。
對於一個數據中心內的不一樣抓去服務器,協同工做的方式有幾種:
1.主從式(Master-Slave)
主從式基本結構如圖所示:
對於主從式而言,有一臺專門的Master服務器來維護待抓取URL隊列,它負責每次將URL分發到不一樣的Slave服務器,而Slave服務器則負責實際的網頁下載工做。Master服務器除了維護待抓取URL隊列以及分發URL以外,還要負責調解各個Slave服務器的負載狀況。以避免某些Slave服務器過於悠閒或者勞累。
這種模式下,Master每每容易成爲系統瓶頸。
2.對等式(Peer to Peer)
對等式的基本結構如圖所示:
在這種模式下,全部的抓取服務器在分工上沒有不一樣。每一臺抓取服務器均可以從待抓取在URL隊列中獲取URL,而後對該URL的主域名的hash值H,而後計算H mod m(其中m是服務器的數量,以上圖爲例,m爲3),計算獲得的數就是處理該URL的主機編號。
舉例:假設對於URL www.baidu.com,計算器hash值H=8,m=3,則H mod m=2,所以由編號爲2的服務器進行該連接的抓取。假設這時候是0號服務器拿到這個URL,那麼它將該URL轉給服務器2,由服務器2進行抓取。
這種模式有一個問題,當有一臺服務器死機或者添加新的服務器,那麼全部URL的哈希求餘的結果就都要變化。也就是說,這種方式的擴展性不佳。針對這種狀況,又有一種改進方案被提出來。這種改進的方案是一致性哈希法來肯定服務器分工。其基本結構如圖所示:
一致性哈希將URL的主域名進行哈希運算,映射爲一個範圍在0-2
一致性哈希將URL的主域名進行哈希運算,映射爲一個範圍在0-232之間的某個數。而將這個範圍平均的分配給m臺服務器,根據URL主域名哈希運算的值所處的範圍判斷是哪臺服務器來進行抓取。
若是某一臺服務器出現問題,那麼本該由該服務器負責的網頁則按照順時針順延,由下一臺服務器進行抓取。這樣的話,及時某臺服務器出現問題,也不會影響其餘的工做。
jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文本內容。它提供了一套很是省力的API,可經過DOM,CSS以及相似於jQuery的操做方法來取出和操做數據。
jsoup api地址:http://www.open-open.com/jsoup/
參考代碼:
/** * 獲取網頁內容,使用代理訪問 * 設定取網頁內容延時設置爲20s(默認的Socket的延時比較短,而有些網站的響應速度比較慢,因此會發生超時的狀況) * @param url * @return<br> */ public static Document getHtmlDoc(String url) { if (T.isBlank(url)) { return null; } Document doc = null; System.getProperties().setProperty("proxySet", "true"); //用的代理服務器 System.getProperties().setProperty("http.proxyHost", 192.168.130.15); //代理端口 System.getProperties().setProperty("http.proxyPort", 8080); try { doc = Jsoup.connect(url).get(); //第一次鏈接不設延時時間,由於大多數頁面的相應時間都是正常的,鏈接超時的頁面爲少數 } catch (Exception e) { //catch到有java.net.SocketTimeoutException: Read timed out異常則從新請求,且設置延時時間 try { doc = Jsoup.connect(url).timeout(20000).get(); //20000表示延時時間設置爲20s } catch (IOException e1) { e1.printStackTrace(); } } return doc; }