dySE:一個 Java 搜索引擎的實現,第 1 部分: 網絡爬蟲

本身動手寫一個搜索引擎,想一想這有多 cool:在界面上輸入關鍵詞,點擊搜索,獲得本身想要的結果;那麼它還能夠作什麼呢?也許是本身的網站須要一個站內搜索功能,抑或是對於硬盤中文檔的搜索 —— 最重要的是,是否是以爲衆多 IT 公司都在向你招手呢?若是你心動了,那麼,Let's Go!html

這裏首先要說明使用 Java 語言而不是 C/C++ 等其它語言的緣由,由於 Java 中提供了對於網絡編程衆多的基礎包和類,好比 URL 類、InetAddress 類、正則表達式,這爲咱們的搜索引擎實現提供了良好的基礎,使咱們能夠專一於搜索引擎自己的實現,而不須要由於這些基礎類的實現而分心。java

這個分三部分的系列將逐步說明如何設計和實現一個搜索引擎。在第一部分中,您將首先學習搜索引擎的工做原理,同時瞭解其體系結構,以後將講解如何實現搜索引擎的第一部分,網絡爬蟲模塊,即完成網頁蒐集功能。在系列的第二部分中,將介紹預處理模塊,即如何處理收集來的網頁,整理、分詞以及索引的創建都在這部分之中。在系列的第三部分中,將介紹信息查詢服務的實現,主要是查詢界面的創建、查詢結果的返回以及快照的實現。正則表達式

dySE 的總體結構數據庫

在開始學習搜索引擎的模塊實現以前,您須要瞭解 dySE 的總體結構以及數據傳輸的流程。事實上,搜索引擎的三個部分是相互獨立的,三個部分分別工做,主要的關係體如今前一部分獲得的數據結果爲後一部分提供原始數據。三者的關係以下圖所示:編程


圖 1. 搜索引擎三段式工做流程
圖 1. 搜索引擎三段式工做流程  

在介紹搜索引擎的總體結構以前,咱們借鑑《計算機網絡——自頂向下的方法描述因特網特點》一書的敘事方法,從普通用戶使用搜索引擎的角度來介紹搜索引擎的具體工做流程。設計模式

自頂向下的方法描述搜索引擎執行過程:瀏覽器

  • 用戶經過瀏覽器提交查詢的詞或者短語 P,搜索引擎根據用戶的查詢返回匹配的網頁信息列表 L;
  • 上述過程涉及到兩個問題,如何匹配用戶的查詢以及網頁信息列表從何而來,根據什麼而排序?用戶的查詢 P 通過分詞器被切割成小詞組 <p1,p2 … pn> 並被剔除停用詞 ( 的、了、啊等字 ),根據系統維護的一個倒排索引能夠查詢某個詞 pi 在哪些網頁中出現過,匹配那些 <p1,p2 … pn> 都出現的網頁集便可做爲初始結果,更進一步,返回的初始網頁集經過計算與查詢詞的相關度從而獲得網頁排名,即 Page Rank,按照網頁的排名順序便可獲得最終的網頁列表;
  • 假設分詞器和網頁排名的計算公式都是既定的,那麼倒排索引以及原始網頁集從何而來?原始網頁集在以前的數據流程的介紹中,能夠得知是由爬蟲 spider 爬取網頁而且保存在本地的,而倒排索引,即詞組到網頁的映射表是創建在正排索引的基礎上的,後者是分析了網頁的內容並對其內容進行分詞後,獲得的網頁到詞組的映射表,將正排索引倒置便可獲得倒排索引;
  • 網頁的分析具體作什麼呢?因爲爬蟲收集來的原始網頁中包含不少信息,好比 html 表單以及一些垃圾信息好比廣告,網頁分析去除這些信息,並抽取其中的正文信息做爲後續的基礎數據。

在有了上述的分析以後,咱們能夠獲得搜索引擎的總體結構以下圖:安全


圖 2. 搜索引擎總體結構
圖 2. 搜索引擎總體結構  

爬蟲從 Internet 中爬取衆多的網頁做爲原始網頁庫存儲於本地,而後網頁分析器抽取網頁中的主題內容交給分詞器進行分詞,獲得的結果用索引器創建正排和倒排索引,這樣就獲得了索引數據庫,用戶查詢時,在經過分詞器切割輸入的查詢詞組並經過檢索器在索引數據庫中進行查詢,獲得的結果返回給用戶。網絡

不管搜索引擎的規模大小,其主要結構都是由這幾部分構成的,並無大的差異,搜索引擎的好壞主要是決定於各部分的內部實現。多線程

有了上述的對與搜索引擎的總體瞭解,咱們來學習 dySE 中爬蟲模塊的具體設計和實現。

回頁首

Spider 的設計

網頁收集的過程如同圖的遍歷,其中網頁就做爲圖中的節點,而網頁中的超連接則做爲圖中的邊,經過某網頁的超連接 獲得其餘網頁的地址,從而能夠進一步的進行網頁收集;圖的遍歷分爲廣度優先和深度優先兩種方法,網頁的收集過程也是如此。綜上,Spider 收集網頁的過程以下:從初始 URL 集合得到目標網頁地址,經過網絡鏈接接收網頁數據,將得到的網頁數據添加到網頁庫中而且分析該網頁中的其餘 URL 連接,放入未訪問 URL 集合用於網頁收集。下圖表示了這個過程:


圖 3. Spider 工做流程
圖 3. Spider 工做流程  

回頁首

Spider 的具體實現

網頁收集器 Gather

網頁收集器經過一個 URL 來獲取該 URL 對應的網頁數據,其實現主要是利用 Java 中的 URLConnection 類來打開 URL 對應頁面的網絡鏈接,而後經過 I/O 流讀取其中的數據,BufferedReader 提供讀取數據的緩衝區提升數據讀取的效率以及其下定義的 readLine() 行讀取函數。代碼以下 ( 省略了異常處理部分 ):


清單 1. 網頁數據抓取
URL url = new URL(「http://www.xxx.com」); 
URLConnection conn = url.openConnection(); 
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 
String line = null; 
while((line = reader.readLine()) != null) 
    document.append(line + "\n");

使用 Java 語言的好處是不須要本身處理底層的鏈接操做,喜歡或者精通 Java 網絡編程的讀者也能夠不用上述的方法,本身實現 URL 類及相關操做,這也是一種很好的鍛鍊。

網頁處理

收集到的單個網頁,須要進行兩種不一樣的處理,一種是放入網頁庫,做爲後續處理的原始數據;另外一種是被分析以後,抽取其中的 URL 鏈接,放入 URL 池等待對應網頁的收集。

網頁的保存須要按照必定的格式,以便之後數據的批量處理。這裏介紹一種存儲數據格式,該格式從北大天網的存儲格式簡化而來:

  • 網頁庫由若干記錄組成,每一個記錄包含一條網頁數據信息,記錄的存放爲順序添加;
  • 一條記錄由數據頭、數據、空行組成,順序爲:頭部 + 空行 + 數據 + 空行;
  • 頭部由若干屬性組成,有:版本號,日期,IP 地址,數據長度,按照屬性名和屬性值的方式排列,中間加冒號,每一個屬性佔用一行;
  • 數據即爲網頁數據。

須要說明的是,添加數據收集日期的緣由,因爲許多網站的內容都是動態變化的,好比一些大型門戶網站的首頁內容,這就意味着若是不是當天爬取的網頁數據,極可能發生數據過時的問題,因此須要添加日期信息加以識別。

URL 的提取分爲兩步,第一步是 URL 識別,第二步再進行 URL 的整理,分兩步走主要是由於有些網站的連接是採用相對路徑,若是不整理會產生錯誤。URL 的識別主要是經過正則表達式來匹配,過程首先設定一個字符串做爲匹配的字符串模式,而後在 Pattern 中編譯後便可使用 Matcher 類來進行相應字符串的匹配。實現代碼以下:


清單 2. URL 識別
public ArrayList<URL> urlDetector(String htmlDoc){
    final String patternString = "<[a|A]\\s+href=([^>]*\\s*>)";           
    Pattern pattern = Pattern.compile(patternString,Pattern.CASE_INSENSITIVE);   
    ArrayList<URL> allURLs = new ArrayList<URL>();
    Matcher matcher = pattern.matcher(htmlDoc);
    String tempURL;
    //初次匹配到的url是形如:<a href="http://bbs.life.xxx.com.cn/" target="_blank">
    //爲此,須要進行下一步的處理,把真正的url抽取出來,
	//能夠對於前兩個"之間的部分進行記錄獲得url
    while(matcher.find()){
        try {
            tempURL = matcher.group();            
            tempURL = tempURL.substring(tempURL.indexOf("\"")+1);        
            if(!tempURL.contains("\""))
                continue;
            tempURL = tempURL.substring(0, tempURL.indexOf("\""));        
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
    return allURLs;    
}

按照「<[a|A]\\s+href=([^>]*\\s*>)」這個正則表達式能夠匹配出 URL 所在的整個標籤,形如「<a href="http://bbs.life.xxx.com.cn/" target="_blank">」,因此在循環得到整個標籤以後,須要進一步提取出真正的 URL,咱們能夠經過截取標籤中前兩個引號中間的內容來得到這段內容。如此以後,咱們能夠獲得一個初步的屬於該網頁的 URL 集合。

接下來咱們進行第二步操做,URL 的整理,即對以前得到的整個頁面中 URL 集合進行篩選和整合。整合主要是針對網頁地址是相對連接的部分,因爲咱們能夠很容易的得到當前網頁的 URL,因此,相對連接只須要在當前網頁的 URL 上添加相對連接的字段便可組成完整的 URL,從而完成整合。另外一方面,在頁面中包含的全面 URL 中,有一些網頁好比廣告網頁是咱們不想爬取的,或者不重要的,這裏咱們主要針對於頁面中的廣告進行一個簡單處理。通常網站的廣告鏈接都有相應的顯示錶達,好比鏈接中含有「ad」等表達時,能夠將該連接的優先級下降,這樣就能夠必定程度的避免廣告連接的爬取。

通過這兩步操做時候,能夠把該網頁的收集到的 URL 放入 URL 池中,接下來咱們處理爬蟲的 URL 的派分問題。

Dispatcher 分配器

分配器管理 URL,負責保存着 URL 池而且在 Gather 取得某一個網頁以後派分新的 URL,還要避免網頁的重複收集。分配器採用設計模式中的單例模式編碼,負責提供給 Gather 新的 URL,由於涉及到以後的多線程改寫,因此單例模式顯得尤其重要。

重複收集是指物理上存在的一個網頁,在沒有更新的前提下,被 Gather 重複訪問,形成資源的浪費,主要緣由是沒有清楚的記錄已經訪問的 URL 而沒法辨別。因此,Dispatcher 維護兩個列表 ,「已訪問表」,和「未訪問表」。每一個 URL 對應的頁面被抓取以後,該 URL 放入已訪問表中,而從該頁面提取出來的 URL 則放入未訪問表中;當 Gather 向 Dispatcher 請求 URL 的時候,先驗證該 URL 是否在已訪問表中,而後再給 Gather 進行做業。

Spider 啓動多個 Gather 線程

如今 Internet 中的網頁數量數以億計,而單獨的一個 Gather 來進行網頁收集顯然效率不足,因此咱們須要利用多線程的方法來提升效率。Gather 的功能是收集網頁,咱們能夠經過 Spider 類來開啓多個 Gather 線程,從而達到多線程的目的。代碼以下:

/** 
* 啓動線程 gather,而後開始收集網頁資料
*/ 
public void start() { 
    Dispatcher disp = Dispatcher.getInstance(); 
    for(int i = 0; i < gatherNum; i++){ 
        Thread gather = new Thread(new Gather(disp)); 
        gather.start(); 
    }
}

在開啓線程以後,網頁收集器開始做業的運做,並在一個做業完成以後,向 Dispatcher 申請下一個做業,由於有了多線程的 Gather,爲了不線程不安全,須要對 Dispatcher 進行互斥訪問,在其函數之中添加 synchronized 關鍵詞,從而達到線程的安全訪問。

回頁首

小結

Spider 是整個搜索引擎的基礎,爲後續的操做提供原始網頁資料,因此瞭解 Spider 的編寫以及網頁庫的組成結構爲後續預處理模塊打下基礎。同時 Spider 稍加修改以後也能夠單獨用於某類具體信息的蒐集,好比某個網站的圖片爬取等。

回頁首

後續內容

在本系列的第 2 部分中,您將瞭解到爬蟲獲取的網頁庫如何被預處理模塊逐步提取內容信息,經過分詞並建成倒排索引;而在第 3 部分中,您將瞭解到,如何編寫網頁來提供查詢服務,而且如何顯示的返回的結果和完成快照的功能。


參考資料

學習

相關文章
相關標籤/搜索