邵曉文
(長春理工大學計算機科學與技術學院,長春130022)
進入21 世紀以來,互聯(lián)網技術進入了飛速發(fā)展階段,互聯(lián)網上的信息量成爆炸式增長,谷歌、百度、雅虎等搜索引擎,每天都會抓住數(shù)以億計的網頁,不同領域、不同背景的用戶,對于信息的需求不同,盡管這些搜索引擎幫助我們抓取了互聯(lián)網上的大部分信息,但是返回的結果包含大量用戶不關心的網頁。網絡爬蟲[1]是搜索引擎的基礎,目的是為了對互聯(lián)網中的海量數(shù)據(jù)進行抓取,當需要對具體網站(如知乎)數(shù)據(jù)進行抓取,通用搜索引擎無法完成這部分工作,需要設計專門的主題爬蟲[3-4]程序,自動抓取特定網頁中的信息。
知乎作為國內知名的問答社區(qū),連接著各行各業(yè)的用戶。用戶分享著彼此的知識、經驗和見解,為中文互聯(lián)網源源不斷的提供多種多樣的信息。目前知乎的用戶已經突破1 億,但是知乎官方并沒有提供相應的數(shù)據(jù)接口,以供使用。Python 語言常被用于爬蟲程序編寫[2],但相比于Java 而言Python 的執(zhí)行效率低,并且Java 語言對多線程的支持更加完善。因此本文設計了一款基于Java 語言的多線程并發(fā)網絡爬蟲,爬取知乎用戶的相關信息。實驗結果表明:多線程爬蟲,具有較快的爬取效率,可以更快地爬取數(shù)據(jù)。
整個互聯(lián)網可以看成一個無限大的有向圖,網絡爬蟲的抓取流程是以一個URL 為初始點,根據(jù)URL 發(fā)送請求,獲取對應的HTML 頁面,提取出需要采集的信息保存,同時解析出更多的URL 加入URL 隊列,從而實現(xiàn)從一個點擴展到整個網絡的過程。直到符合爬蟲的停止條件。爬蟲的抓取流程如圖1 所示。
圖1 普通爬蟲的抓取流程
知乎的用戶之間的關系可以看做一個有向圖,如圖2 所示。
圖2 知乎用戶關系示意圖
對圖的搜索主要有兩種方式,深度優(yōu)先搜索和寬度優(yōu)先搜索。深度優(yōu)先遍歷測試是指網絡爬蟲會從起始頁開始,一個鏈接一個鏈接跟蹤下去,處理完這條線路的鏈接之后,再轉入下一個起始頁,繼續(xù)跟蹤鏈接。寬度優(yōu)先策略是按照樹的層次進行搜索,如果此層沒有搜索完成,不會進入下一層搜索。即首先完成一個層次的搜索,其次再進行下一層次,也稱之為分層處理。
本文采用的是寬度優(yōu)先搜索。因為離種子URL比較近的,往往是重要的網頁,隨著瀏覽的不斷深入,所看到的網頁重要性會越來越低。寬度優(yōu)先搜索也有利于多爬蟲的合作抓取。
知乎中的每個用戶都有唯一的Urltoken 與之對應,首先選取一名知乎活躍用戶作為起點,使用httpclient 工具發(fā)送請求,返回JSON 格式的數(shù)據(jù),提取用戶的相關信息存入MySQL 數(shù)據(jù)庫,同時獲取該用戶關注的用戶列表,用戶列表中的數(shù)據(jù)以Urltoken 的方式給出,通過Urltoken 組成新的URL 加入隊列。
爬蟲隊列的設計是網絡爬蟲的關鍵部分,小型爬蟲可以直接使用鏈表實現(xiàn),但是需要爬取的數(shù)據(jù)量很大時,僅僅靠Java 自身提供的數(shù)據(jù)結構是遠遠不夠的,并且當爬取時間很長時,爬蟲程序不一定能夠在一次的啟動過程中就完成所有爬取工作,可能需要重復啟動多次,這就需要將隊列中的數(shù)據(jù)固化到硬盤中,否則再次啟動爬蟲,將重新從開始節(jié)點爬取。因此這里選用Redis 數(shù)據(jù)庫來實現(xiàn)爬蟲隊列。
Redis 是一款高性能的key-value 數(shù)據(jù)庫,主要具有以下特點:
●Redis 支持數(shù)據(jù)的持久化,可以將內存中的數(shù)據(jù)保存在磁盤中,重啟的時候可以再次加載進行使用,這一點對網絡爬蟲來說非常重要。
●Redis 不僅僅支持簡單的key-value 類型的數(shù)據(jù),同時還提供list、set、hash 等數(shù)據(jù)結構的存儲。
●Redis 性能極高,讀取速度高達110000 次/s,寫入速度高達81000 次/s。
因此Redis 非常適合用來做網絡爬蟲的隊列,本文使用Redis 的List 結構,作為爬蟲程序的URL 隊列。
在采用深度優(yōu)先的爬取策略時,需要對已經爬取過的URL 進行過濾,避免重復爬取,可以使用Java 提供的哈希表對已經爬取過的URL 進行記錄,從而達到避免重復的效果。對于小型爬蟲而言,通過Java 提供的哈希表來實現(xiàn)過濾器,能滿足需求。但是知乎的用戶數(shù)量已經超過一億,當爬取的數(shù)據(jù)量很大時,需要非常大的內存空間,一般服務器很難滿足這樣的存儲需求。因此本文采用了布隆過濾器[5-6]來過濾已爬取的URL。
布隆過濾器是由巴頓·布隆于1970 年提出,它是一種數(shù)學工具,只需要哈希表的1/8~1/4 的大小就能解決同樣的問題。它主要是由一個二進制的向量和一系列的hash 函數(shù)組成。本文通過下面的示例說明其工作原理。
利用內存中的一個長度是m 的位數(shù)組B,對其中所有位都置為0,如圖3 所示。
圖3 位數(shù)組的初始狀態(tài)
對于每一個URL 地址,選取8 個不同的隨機質數(shù),根據(jù)這8 個質數(shù),計算出八個不同的hash 值,并將位數(shù)組中對應hash 值的位置設為1,如圖4 所示。
圖4 布隆過濾器的實現(xiàn)
對于5000 萬級別的URL,布隆過濾器值占用200M 左右的空間,大大節(jié)省了存儲空間。
正如上文所述,當爬取數(shù)據(jù)量很大,爬取時間很長的時候,當遇到爬蟲程序意外終止的情況,再次啟動爬蟲程序,就得重新開始爬取,對于過濾器也一樣,如果使用HashMap 或者Set,再次啟動爬蟲時,過濾器中的數(shù)據(jù)丟失,因此,鑒于前面對Redis 的介紹,這里采用Redis 的List 結構來實現(xiàn)。
爬蟲主要消耗三種資源:網絡帶寬、中央處理器和磁盤。三者中的任何一個都可能成為會爬蟲程序的瓶頸。在這三者都無法控制的前提下,單線程爬蟲的效率低下,爬取速度很慢。為了增加爬蟲的效率,最直接方便的辦法就是使用多線程技術[7]并行抓取,Java 語言對多線程提供了很好的支持。在爬蟲系統(tǒng)中,要處理的URL 隊列是唯一的,多個線程依次從隊列中取出URL,各自對用戶信息進行抓取,再將用戶關注者的URL 加入隊列,因此,線程既是生產者也是消費者,這里使用線程池來管理線程。
使用多線程并發(fā)爬取數(shù)據(jù)時可以大大提升爬取的效率,但同時也帶來數(shù)據(jù)競爭的問題。即存在同一塊內存上,存在兩個并發(fā)的操作,其中至少有一個為寫操作。數(shù)據(jù)競爭會導致讀取的數(shù)據(jù)和預期的不一致。本文通過Redis 實現(xiàn)了URL 隊列和布隆過濾器,Redis 的基本操作,在并發(fā)環(huán)境下是具有原子性的,因此不用考慮數(shù)據(jù)競爭。
本文的爬蟲程序結束邏輯是使用了一個共享變量count 作為計數(shù)器,記錄已爬取的URL 數(shù)量,當達到指定數(shù)量,就不再向URL 隊列中加入新的URL,直至將URL 隊列中的數(shù)據(jù)爬取完畢,URL 隊列為空,爬蟲程序終止,因此爬取頁面的時間遠遠長于向隊列中加入URL 的時間,因此不會出現(xiàn)URL 隊列為空,而count 沒有達到指定數(shù)量的情況。由于多線程的執(zhí)行順序使無法確定的,且線程之間的操作是相互透明的,因此對count 的操作是存在線程安全問題的,會產生數(shù)據(jù)競爭的問題。對此,通常的解決方案是將操作count 的臨界區(qū)通過Java 提供的同步鎖進行保護,保證每一次只有一個線程修改count 變量,但是當線程數(shù)量很多是,鎖機制會大大降低爬蟲的效率。因此采用樂觀鎖來解決count 的數(shù)據(jù)競爭問題,樂觀鎖的主要元素是通過CAS(Compare And Swap)操作實現(xiàn),使用Java 提供的原子操作類:AtomicInteger 原子更新整型類,該類保證更新的原子性,性能高效,線程安全。本文的爬蟲程序結構如圖5 所示。
圖5 本文的爬蟲架構
表1
表2
首先從種子URL 開始,使用HttpClient,根據(jù)URL,向服務器發(fā)送請求,知乎服務器響應一段HTML代碼,其中包含用戶相關信息的JSON 格式數(shù)據(jù),使用Jsoup 解析HTML 網頁,獲取用戶的信息,通過JsonObject,解析出用戶的相關數(shù)據(jù)并存入MySQL 數(shù)據(jù)庫,根據(jù)用戶的token,發(fā)起新的請求,獲取用戶關注列表,得到JSON 格式數(shù)據(jù),解析出關注列表的Urltoken,組成新的URL,存入隊列,最終完成隊列的初始化工作。
完成初始化工作后,開啟線程開始爬取工作,從隊列中取出URL,判斷URL 是否已經被爬取,如果沒有,就執(zhí)行爬取工作,否則重新從隊列中取出新的URL,當URL 計數(shù)器達到指定數(shù)量時,停止向隊列中加入新的URL,繼續(xù)爬取隊列中剩下的URL,最終URL 隊列為空,結束爬蟲程序。
表3
實驗結果表明:相比單線程爬蟲的抓取速度遠遠小于多線程爬蟲,因此本文設計的爬蟲,能夠有效提高了爬蟲的效率。
本文的爬蟲采用Java 語言針對知乎的用戶數(shù)據(jù)進行抓取,設計了多線程爬蟲架構,實現(xiàn)了多數(shù)據(jù)的并發(fā)抓起,加快了爬取的速度,并使用了Redis 實現(xiàn)了URL隊列,提升了存取的效率。本文實現(xiàn)了布隆過濾器,過濾已爬取的URL,大大減少了內存的消耗,放棄使用同步鎖,使用Java 提供的原子操作類AtomicInteger,保證了線程的安全,提升了爬蟲的效率,最終達到預期的爬取效果。