heritrix源碼
㈠ java url參數去重
言歸正傳。
所謂的Url去重(我一直沒找到對應的英文,URL Filtering ?),就是爬蟲將重復抓取的URL去除,避免多次抓取同一網頁。爬蟲一般會將待抓取的URL放在一個隊列中,從抓取後的網頁中提取到新的URL,在他們被放入隊列之前,首先要確定這些新的URL沒有被抓取過,如果之前已經抓取過了,就不再放入隊列。
最直觀的做法 – hash表
為了盡快把整個爬蟲搭建起來,最開始的URL去重採用方案是一個內存中的HashSet,這是最直觀的方法,所有人都能想得到。HashSet中放置的就是URL的字元串,任何一個新的URL首先在HashSet中進行查找,如果HashSet中沒有,就將新的URL插入HashSet,並將URL放入待抓取隊列。
這個方案的好處是它的去重效果精確,不會漏過一個重復的URL。它的缺點是,我的爬蟲第二天早上就掛了,Out Of Memory。因為隨著抓取網頁的增加,HashSet會一直無限制的增長。另外,網路中的很多URL其實是很長的,有大量的URL長度達到上百個字元。當然,因為我的爬蟲是跑在一個小伺服器上,JVM的內存本來就不多,否則它應該能再多撐1-2天。
簡單估算一下,假設單個URL的平均長度是100 byte(我覺著這已經非常保守了),那麼抓取1000萬的URL就需要:
100 byte * 10 000 000 = 1 GB
而1000萬URL在整個互聯網中實在是滄海一粟。可以了解,需要多大的內存才能裝下所有URL的HashSet。
壓縮URL
為了我的爬蟲能再多撐幾天,同時不想改動太多的代碼,第二個版本增加了一個小功能,就是HashSet中不存儲原始的URL,而是將URL壓縮後再放進去。貌似有不少paper中討論過如何對URL進行壓縮,包括新浪微博中的短URL其實也是個不錯的方案,可惜這些方法我都不會。為了偷懶,我直接用MD5對URL做編碼。
MD5的結果是128 bit也就是16 byte的長度。相比於之間估計的URL平均長度100byte已經縮小了好幾倍,可以多撐好多天了。
當然,哪怕找個一個可以壓縮到極致的演算法,隨著URL越來越多,終有一天會Out Of Memory。所以,這個方案不解決本質問題。
MD5另外一個問題是,有可能兩個相同的URL被映射成同一個MD5值,這樣的話,它們中有一個就永遠不會被抓取了。我不太確定的是,這個概率會有多大。如果非常小的話,這微小的誤差倒也不會有太大影響。
Bloom Filter
基於內存的HashSet的方法存在一個本質的問題,就是它消耗的內存是隨著URL的增長而不斷增長的。除非能夠保證內存的大小能夠容納下所有需要抓取的URL,否則這個方案終有一天會到達瓶頸。
這時候就會想,要找一個類似於HashSet的但所消耗的內存相對固定而不會不斷增長的方案,於是自然想到了Bloom Filter。關於Bloom Filter的概念這里就不多談了,網上隨處可以找到。我簡單嘗試了一下Bloom Filter,但是很快就放棄了。基於Bloom Filter的方案有幾個問題:
第一個是理論上的。Bloom Filter會將一些正常的樣本(在我這就是沒有抓取過的URL)過濾掉,即所謂的False Positive。當然,這概率有多大,取決於Bloom Filter的參數設置。但這引出了下一個問題;
第二個是實踐中的,即Bloom Filter的那幾個參數應該如何設置?m,k,n應該設置成多少才合適,這個我沒有經驗,而且可能需要反復的實驗和測試才能夠比較好的確定下來;
以上兩個問題還不是我放棄Bloom Filter的根本原因,真實的原因是我在做的是一個爬蟲框架,上面可以會啟動很多的爬蟲任務,每個任務可能抓取自己特定的URL,而且任務之間是獨立的。這樣,對於每個任務都需要有一個Bloom Filter,雖然對於單一任務它使用Bloom Filter所消耗的內存是固定的,但是任務的增多會導致更多的Bloom Filter,從而導致更多的內存消耗。仍然存在內存溢出的可能。
但如果只是一個抓取任務,那麼採用Bloom Filter應該是一個非常不錯的選擇。
BerkeleyDB
我終於明白我所需要的其實是一個可以放在disk上的去重方案,這樣,內存溢出將永遠成不了可能。很早就知道有BerkeleyDB這么一個東西,但第一次真正了解還是在Amazon的Dynamo那篇論文中提到過採用了BerkeleyDB作為單機上的底層存儲。當時覺著這東西真另類,原來還有叫做「DB」的東西卻不支持SQL。那時候還沒有NOSQL這詞,把這樣的東西叫做non-relational database。
BerkeleyDB是一個key-value database,簡單的說,就是一個在disk上的hash表,這也是為什麼它可以被用來做URL去重的原因。它另外一個另類的地方是,它是和程序運行在同一個進程空間中的,而不像一般的db,是做為單獨的程序運行。
這里附上Heritrix中使用BerkeleyDB做URL去重的代碼,一探究竟:(代碼位於Heritrix源代碼的org.archive.crawler.util.BdbUriUniqFilter)
有一堆做初始化和配置的函數就直接忽略了,真正相關的函數就只有兩個:
[java] view plain
/**
* Create fingerprint.
* Pubic access so test code can access createKey.
* @param uri URI to fingerprint.
* @return Fingerprint of passed <code>url</code>.
*/
public static long createKey(CharSequence uri) {
String url = uri.toString();
int index = url.indexOf(COLON_SLASH_SLASH);
if (index > 0) {
index = url.indexOf('/', index + COLON_SLASH_SLASH.length());
}
CharSequence hostPlusScheme = (index == -1)? url: url.subSequence(0, index);
long tmp = FPGenerator.std24.fp(hostPlusScheme);
return tmp | (FPGenerator.std40.fp(url) >>> 24);
}
[java] view plain
/**
* value: only 1 byte
*/
private static DatabaseEntry ZERO_LENGTH_ENTRY = new DatabaseEntry(
new byte[0]);
protected boolean setAdd(CharSequence uri) {
DatabaseEntry key = new DatabaseEntry();
LongBinding.longToEntry(createKey(uri), key);
long started = 0;
OperationStatus status = null;
try {
if (logger.isLoggable(Level.INFO)) {
started = System.currentTimeMillis();
}
status = alreadySeen.putNoOverwrite(null, key, ZERO_LENGTH_ENTRY);
if (logger.isLoggable(Level.INFO)) {
aggregatedLookupTime +=
(System.currentTimeMillis() - started);
}
} catch (DatabaseException e) {
logger.severe(e.getMessage());
}
if (status == OperationStatus.SUCCESS) {
count++;
if (logger.isLoggable(Level.INFO)) {
final int logAt = 10000;
if (count > 0 && ((count % logAt) == 0)) {
logger.info("Average lookup " +
(aggregatedLookupTime / logAt) + "ms.");
aggregatedLookupTime = 0;
}
}
}
if(status == OperationStatus.KEYEXIST) {
return false; // not added
} else {
return true;
}
}
簡單解釋一下:
第一個函數createKey是在做URL的壓縮,它將任意長度的URL轉換成一個long型的值。long型的取值范圍有2^64,因此兩個URL映射成同一個long型值的概率應該挺低的。但我也沒太細看這個函數,所以它的效果到底如何不確定。
第二個函數setAdd就是將被壓縮的URL寫入到BerkeleyDB。之前說過,BerkeleyDB是一個key-value database,它的每條記錄都包括了一個key和一個value。但是在URL去重中,value不重要(比如我們之前內存中用的也是HashSet而不是HashMap),因此這里統一用一個byte長度的值來表示value,就是這個static變數ZERO_LENGTH_ENTRY。
別看setAdd有這么多行,真正有用的就這一行:
[java] view plain
status = alreadySeen.putNoOverwrite(null, key, ZERO_LENGTH_ENTRY);
將壓縮後得到的long型值作為key,ZERO_LENGTH_ENTRY作為value插入到BerkeleyDB中,如果db中已經有了這個long型值,就會返回OperationStatus.KEYEXIST,表示對應的URL之前已經抓取到了,那麼這個URL就不會放入待抓取隊列中。
最後
比較遺憾的是,我還沒抽出空對BerkeleyDB這個方案做性能測試,不確定它每秒能執行多少次setAdd操作,是否足夠滿足我們性能的要求。以後補上。
另外,雖然我不了解,但我認為像網路這樣專業的搜索引擎,它的爬蟲的URL去重方案可能比這里列舉的要復雜的多,畢竟那個的各方面的要求也要更高。
㈡ java開源web爬蟲哪個好用
Lucene+nutch+heritrix網上可以找得到源代碼,開源的搜索引擎,包含爬蟲、檢索等功能。
Heritrix是一個爬蟲框架,可加如入一些可互換的組件。 它的執行是遞歸進行的,主要有以下幾步: 1。在預定的URI中選擇一個。 2。獲取URI 3。分析,歸檔結果 4。選擇已經發現的感興趣的URI。加入預定隊列。 5。標記已經處理過的URI 是IA的開放源代碼,可擴展的,基於整個Web的,歸檔網路爬蟲工程 Heritrix工程始於2003年初,IA的目的是開發一個特殊的爬蟲,對網上的 資源進行歸檔,建立網路數字圖書館,在過去的6年裡,IA已經建立了400TB的數據。 最新版本:heritrix-1.15.4 IA期望他們的crawler包含以下幾種: 寬頻爬蟲:能夠以更高的帶寬去站點爬。 主題爬蟲:集中於被選擇的問題。 持續爬蟲:不僅僅爬更當前的網頁還負責爬日後更新的網頁。 實驗爬蟲:對爬蟲技術進行實驗,以決定該爬什麼,以及對不同協議的爬蟲 爬行結果進行分析的。 Heritrix的主頁是http://crawler.archive.org Heritrix是一個爬蟲框架,可加如入一些可互換的組件。 它的執行是遞歸進行的,主要有以下幾步: 1。在預定的URI中選擇一個。 2。獲取URI 3。分析,歸檔結果 4。選擇已經發現的感興趣的URI。加入預定隊列。 5。標記已經處理過的URI
[編輯本段]部件
主要部件
Heritrix主要有三大部件:范圍部件,邊界部件,處理器鏈 范圍部件:主要按照規則決定將哪個URI入隊。 邊界部件:跟蹤哪個預定的URI將被收集,和已經被收集的URI,選擇下一個 URI,剔除已經處理過的URI。 處理器鏈:包含若干處理器獲取URI,分析結果,將它們傳回給邊界部件
其餘部件
WEB管理控制台:大多數都是單機的WEB應用,內嵌JAVA HTTP 伺服器。 操作者可以通過選擇Crawler命令來操作控制台。 Crawler命令處理部件:包含足夠的信息創建要爬的URI。 Servercache(處理器緩存):存放伺服器的持久信息,能夠被爬行部件隨時查到,包括IP地址,歷史記錄,機器人策略。 處理器鏈: 預取鏈:主要是做一些准備工作,例如,對處理進行延遲和重新處理,否決隨後的操作。 提取鏈:主要是獲得資源,進行DNS轉換,填寫請求和響應表單 抽取鏈:當提取完成時,抽取感興趣的HTML,JavaScript,通常那裡有新的也適合的URI,此時URI僅僅被發現,不會被評估 寫鏈:存儲爬行結果,返回內容和抽取特性,過濾完存儲。 提交鏈:做最後的維護,例如,測試那些不在范圍內的,提交給邊界部件
[編輯本段]關鍵特性
Heritrix 1.0.0包含以下關鍵特性: 1.用單個爬蟲在多個獨立的站點一直不斷的進行遞歸的爬。 2。從一個提供的種子進行爬,收集站點內的精確URI,和精確主機。 3。主要是用廣度優先演算法進行處理。 4。主要部件都是高效的可擴展的 5。良好的配置,包括: a。可設置輸出日誌,歸檔文件和臨時文件的位置 b。可設置下載的最大位元組,最大數量的下載文檔,和最大的下載時間。 c。可設置工作線程數量。 d。可設置所利用的帶寬的上界。 e。可在設置之後一定時間重新選擇。 f。包含一些可設置的過濾機制,表達方式,URI路徑深度選擇等等。 Heritrix的局限: 1。單實例的爬蟲,之間不能進行合作。 2。在有限的機器資源的情況下,卻要復雜的操作。 3。只有官方支持,僅僅在Linux上進行了測試。 4。每個爬蟲是單獨進行工作的,沒有對更新進行修訂。 5。在硬體和系統失敗時,恢復能力很差。