Heritrix的架構

[b]10.2 Heritrix的架構[/b]
在上一節中,詳細介紹了Heritrix的使用入門。讀者通過上一節的介紹,應該已經能夠使用Heritrix來進行簡單的網頁抓取了。那麼,Heritrix的內容究竟是如何工作的呢?它的設計方面有什麼突出之處?
本節就將介紹Heritrix的幾個主要組件,以此讓讀者瞭解其主要架構和工作方式。爲後續的擴展Heritrix做一些鋪墊。
10.2.1 抓取任務CrawlOrder
之所以選擇從CrawlOrder這個類說起,是因爲它是整個抓取工作的起點。在上一節中已經說過,一次抓取任務包括許多的屬性,建立一個任務的方式有很多種,最簡單的一種就是根據默認的order.xml來配置。在內存中,order使用CrawlOrder這個類來進行表示。看一下API文檔中CrawlOrder的繼承關係圖,如圖10-52所示。

圖10-52 CrawlOrder類的繼承關係圖
從繼承關係圖中可以看到,CrawlOrder繼承自一系列的與屬性設置相關的基類。另外,它的最頂層基類是javax.management.Attribute,這是一個JMX中的類,它可以動態的反映出Java容器內某個MBean的屬性變化。關於這一部分的內容不是我們所要討論的重點,只需知道,CrawlOrder中的屬性,是需要被隨時讀取和監測的。
那麼究竟使用什麼工具來讀取order.xml文件中的各種屬性呢。另外,一個CrawlOrder的對象又該如何構建呢?Heritrix提供了很好的工具支持對於order.xml的讀取。在org.archive.crawler.settings包下有一個XMLSettingsHandler類,它可以用來幫助讀取order.xml。
public XMLSettingsHandler(File orderFile) throws InvalidAttributeValueException
在XMLSettingsHandler的構造函數中,其所傳入的參數orderFile正是一個經過對象封裝的order.xml的File。這樣,就可以直接調用其構造函數,來創建一個XMLSettingsHandler的實例,以此做爲一個讀取order.xml的工具。
當一個XMLSettingsHandler的實例被創建後,可以通過getOrder()方法來獲取CrawlOrder的實例,這樣也就可以進行下一步的工作了。
10.2.2 中央控制器CrawlController
中央控制器是一次抓取任務中的核心組件。它將決定整個抓取任務的開始和結束。CrawlController位於org.archive.crawler.framework中,在它的Field聲明中,看到如下代碼片段。
代碼10.2
// key subcomponents which define and implement a crawl in progress
private transient CrawlOrder order;
private transient CrawlScope scope;
private transient ProcessorChainList processorChains;
private transient Frontier frontier;
private transient ToePool toePool;
private transient ServerCache serverCache;
// This gets passed into the initialize method.
private transient SettingsHandler settingsHandler;
可以看到,在CrawlController類中,定義了以下幾個組件:
l CrawlOrder:這就不用說了,因爲一個抓取工作必須要有一個Order對象,它保存了對該次抓取任務中,order.xml的屬性配置。
l CrawlScope:在10.1.4節中已經介紹過了,這是決定當前的抓取範圍的一個組件。
l ProcessorChainList:從名稱上很明顯就能看出,它表示了處理器鏈,在這個列表中的每一項都可以和10.1.4節中所介紹的處理器鏈對應上。
l Frontier:很明顯,一次抓取任務需要設定一個Frontier,以此來不斷爲其每個線程提供URI。
l ToePool:這是一個線程池,它管理了所有該抓取任務所創建的子線程。
l ServerCache:這是一個緩存,它保存了所有在當前任務中,抓取過的Host名稱和Server名稱。
以上組件應該是一次正常的抓取過程中所必需的幾項,它們各自的任務很獨立,分工明確,但在後臺中,它們之間卻有着千絲萬縷的聯繫,彼此互相做爲構造函數或初始化的參數傳入。
那麼,究竟該如何獲得CrawlController的實例,並且通過自主的編程來使用Heritrix提供的API進行一次抓任務呢?
事實上CrawlController有一個不帶參數的構造函數,開發者可以直接通過它的構造函數來構造一個CrawlController的實例。但是值得注意的一點,在構造一個實例並進行抓取任務時,有幾個步驟需要完成:
(1)首先構造一個XMLSettingsHandler對象,將order.xml內的屬性信息裝入。
(2)調用CrawlController的構造函數,構造一個CrawlController的實例。
(3)調用CrawlController的intialize(SettingsHandler)方法,初始化CrawlController實例。其中,傳入的參數是在第一步是構造的XMLSettingsHandler實例。
(4)當上述3步完成後,CrawlController就已經具備運行的條件,可以開始運行了。此時,只需調用它的requestCrawlStart()方法,就可以啓運線程池和Frontier,然後就可以開始不斷的抓取網頁了。
上述過程可以用圖10-53所示。

圖10-53 使用CrawlController啓運抓取任務
在CrawlController的initialize()方法中,Heritrix主要做了以下幾件事:
(1)從XMLSettingsHandler中取出Order。
(2)檢查了用戶設定的UserAgent等信息,看是否符合格式。
(3)設定了開始抓取後保存文件信息的目錄結構。
(4)初始化了日誌信息的記錄工具。
(5)初始化了使用Berkley DB的一些工具。
(6)初始化了Scope、Frontier以及ProcessorChain。
(7)最後實例化了線程池。
在正常情況下,以上順序不能夠被隨意變動,因爲後一項功能的初始化很有可能需要前幾項功能初始化的結果。例如線程池的初始化,必須要在先有了Frontier的實例的基礎上來進行。讀者可能對其中的Berkeley DB感到費解,在後面的小節將詳細說明。
從圖10-53中看到,最終啓動抓取工作的是requestCrawlStart()方法。其代碼如下。
代碼10.3
public void requestCrawlStart() {
// 初始化處理器鏈
runProcessorInitialTasks();
// 設置一下抓取狀態的改變,以便能夠激發一些Listeners
// 來處理相應的事件
sendCrawlStateChangeEvent(STARTED, CrawlJob.STATUS_PENDING);
String jobState;
state = RUNNING;
jobState = CrawlJob.STATUS_RUNNING;
sendCrawlStateChangeEvent(this.state, jobState);
// A proper exit will change this value.
this.sExit = CrawlJob.STATUS_FINISHED_ABNORMAL;
// 開始日誌線程
Thread statLogger = new Thread(statistics);
statLogger.setName("StatLogger");
statLogger.start();
// 啓運Frontier,抓取工作開始
frontier.start();
}
可以看到,啓動抓取工作的核心就是要啓動Frontier(通過調用其start()方法),以便能夠開始向線程池中的工作線程提供URI,供它們抓取。
下面的代碼就是BdbFrontier的父類AbstractFrontier中的start()方法和unpause()方法:
代碼10.4
public void start() {
if (((Boolean)getUncheckedAttribute(null, ATTR_PAUSE_AT_START))
.booleanValue()) {
// 若配置文件中不允許該次抓取開始
// 則停止
controller.requestCrawlPause();
} else {
// 若允許開始,則開始
unpause();
}
}
synchronized public void unpause() {
// 去除當前阻塞變量
shouldPause = false;
// 喚醒所有阻塞線程,開始抓取任務
notifyAll();
}
在start()方法中,首先判斷配置中的屬性是否允許當前線程開始。若不允許,則令controller停止抓取。若允許開始,則簡單的調用unpause()方法。unpause()方法更爲簡單,它首先將阻塞線程的信號量設爲false,即允許線程開始活動,然後通過notifyAll()方法,喚醒線程池中所有被阻塞的線程,開始抓取。
10.2.3 Frontier鏈接製造工廠
Frontier在英文中的意思是“前線,領域”,在Heritrix中,它表示一種爲線程提供鏈接的工具。它通過一些特定的算法來決定哪個鏈接將接下來被送入處理器鏈中,同時,它本身也負責一定的日誌和狀態報告功能。
事實上,要寫出一個合格並且真正能夠使用的Frontier絕非一件簡單的事情,儘管有了Frontier接口,其中的方法約束了Frontier的行爲,也給編碼帶來了一定的指示。但是其中還存在着很多問題,需要很好的設計和處理纔可以解決。
在Heritrix的官方文檔上,有一個Frontier的例子,在此拿出來進行一下講解,以此來向讀者說明一個最簡單的Frontier都能夠做什麼事。以下就是這個Frontier的代碼。
代碼10.5
public class MyFrontier extends ModuleType implements Frontier,
FetchStatusCodes {
// 列表中保存了還未被抓取的鏈接
List pendingURIs = new ArrayList();

// 這個列表中保存了一系列的鏈接,它們的優先級
// 要高於pendingURIs那個List中的任何一個鏈接
// 表中的鏈接表示一些需要被滿足的先決條件
List prerequisites = new ArrayList();

// 一個HashMap,用於存儲那些已經抓取過的鏈接
Map alreadyIncluded = new HashMap();

// CrawlController對象
CrawlController controller;
// 用於標識是否一個鏈接正在被處理
boolean uriInProcess = false;

// 成功下載的數量
long successCount = 0;
// 失敗的數量
long failedCount = 0;
// 拋棄掉鏈接的數量
long disregardedCount = 0;
// 總共下載的字節數
long totalProcessedBytes = 0;
// 構造函數
public MyFrontier(String name) {
super(Frontier.ATTR_NAME, "A simple frontier.");
}
// 初始化,參數爲一個CrawlController
public void initialize(CrawlController controller)
throws FatalConfigurationException, IOException {

// 注入
this.controller = controller;

// 把種子文件中的鏈接加入到pendingURIs中去
this.controller.getScope().refreshSeeds();
List seeds = this.controller.getScope().getSeedlist();
synchronized(seeds) {
for (Iterator i = seeds.iterator(); i.hasNext();) {
UURI u = (UURI) i.next();
CandidateURI caUri = new CandidateURI(u);
caUri.setSeed();
schedule(caUri);
}
}
}
// 該方法是給線程池中的線程調用的,用以取出下一個備處理的鏈接
public synchronized CrawlURI next(int timeout) throws InterruptedException {
if (!uriInProcess && !isEmpty()) {
uriInProcess = true;
CrawlURI curi;
/*
* 算法很簡單,總是先看prerequistes隊列中是否有
* 要處理的鏈接,如果有,就先處理,如果沒有
* 再看pendingURIs隊列中是否有鏈接
* 每次在處理的時候,總是取出隊列中的第一個鏈接
*/
if (!prerequisites.isEmpty()) {
curi = CrawlURI.from((CandidateURI) prerequisites.remove(0));
} else {
curi = CrawlURI.from((CandidateURI) pendingURIs.remove(0));
}
curi.setServer(controller.getServerCache().getServerFor(curi));
return curi;
} else {
wait(timeout);
return null;
}
}
public boolean isEmpty() {
return pendingURIs.isEmpty() && prerequisites.isEmpty();
}
// 該方法用於將新鏈接加入到pendingURIs隊列中,等待處理
public synchronized void schedule(CandidateURI caURI) {
/*
* 首先判斷要加入的鏈接是否已經被抓取過
* 如果已經包含在alreadyIncluded這個HashMap中
* 則說明處理過了,即可以放棄處理
*/
if (!alreadyIncluded.containsKey(caURI.getURIString())) {
if(caURI.needsImmediateScheduling()) {
prerequisites.add(caURI);
} else {
pendingURIs.add(caURI);
}
// HashMap中使用url的字符串來做爲key
// 而將實際的CadidateURI對象做爲value
alreadyIncluded.put(caURI.getURIString(), caURI);
}
}
public void batchSchedule(CandidateURI caURI) {
schedule(caURI);
}
public void batchFlush() {
}
// 一次抓取結束後所執行的操作,該操作由線程池
// 中的線程來進行調用
public synchronized void finished(CrawlURI cURI) {
uriInProcess = false;

// 成功下載
if (cURI.isSuccess()) {
successCount++;
// 統計下載總數
totalProcessedBytes += cURI.getContentSize();
// 如果成功,則觸發一個成功事件
// 比如將Extractor解析出來的新URL加入隊列中
controller.fireCrawledURISuccessfulEvent(cURI);
cURI.stripToMinimal();
}
// 需要推遲下載
else if (cURI.getFetchStatus() == S_DEFERRED) {
cURI.processingCleanup();
alreadyIncluded.remove(cURI.getURIString());
schedule(cURI);
}
// 其他狀態
else if (cURI.getFetchStatus() == S_ROBOTS_PRECLUDED
|| cURI.getFetchStatus() == S_OUT_OF_SCOPE
|| cURI.getFetchStatus() == S_BLOCKED_BY_USER
|| cURI.getFetchStatus() == S_TOO_MANY_EMBED_HOPS
|| cURI.getFetchStatus() == S_TOO_MANY_LINK_HOPS
|| cURI.getFetchStatus() == S_DELETED_BY_USER) {
// 拋棄當前URI
controller.fireCrawledURIDisregardEvent(cURI);
disregardedCount++;
cURI.stripToMinimal();
} else {
controller.fireCrawledURIFailureEvent(cURI);
failedCount++;
cURI.stripToMinimal();
}
cURI.processingCleanup();
}

// 返回所有已經處理過的鏈接數量
public long discoveredUriCount() {
return alreadyIncluded.size();
}
// 返回所有等待處理的鏈接的數量
public long queuedUriCount() {
return pendingURIs.size() + prerequisites.size();
}
// 返回所有已經完成的鏈接數量
public long finishedUriCount() {
return successCount + failedCount + disregardedCount;
}
// 返回所有成功處理的鏈接數量
public long successfullyFetchedCount() {
return successCount;
}
// 返回所有失敗的鏈接數量
public long failedFetchCount() {
return failedCount;
}
// 返回所有拋棄的鏈接數量
public long disregardedFetchCount() {
return disregardedCount;
}
// 返回總共下載的字節數
public long totalBytesWritten() {
return totalProcessedBytes;
}
public String report() {
return "This frontier does not return a report.";
}
public void importRecoverLog(String pathToLog) throws IOException {
throw new UnsupportedOperationException();
}
public FrontierMarker getInitialMarker(String regexpr,
boolean inCacheOnly) {
return null;
}
public ArrayList getURIsList(FrontierMarker marker, int numberOfMatches,
boolean verbose) throws InvalidFrontierMarkerException {
return null;
}
public long deleteURIs(String match) {
return 0;
}
}
在Frontier中,根據筆者給出的中文註釋,相信讀者已經能夠了解這個Frontier中的大部分玄機。以下給出詳細的解釋。
首先,Frontier是用來向線程提供鏈接的,因此,在上面的代碼中,使用了兩個ArrayList來保存鏈接。其中,第一個pendingURIs保存的是等待處理的鏈接,第二個prerequisites中保存的也是鏈接,只不過它裏面的每個鏈接的優先級都要高於pendingURIs裏的鏈接。通常,在prerequisites中保存的都是如DNS之類的鏈接,只有當這些鏈接被首先解析後,其後續的鏈接才能夠被解析。
除了這兩個ArrayList外,在上面的Frontier還有一個名稱爲alreadyIncluded的HashMap。它用於記錄那些已經被處理過的鏈接。每當調用Frontier的schedule()方法來加入一個新的鏈接時,Frontier總要先檢查這個正要加入到隊列中的鏈接是不是已經被處理過了。
很顯然,在分析網頁的時候,會出現大量相同的鏈接,如果沒有這種檢查,很有可能造成抓取任務永遠無法完成的情況。同時,在schedule()方法中還加入了一些邏輯,用於判斷當前要進入隊列的鏈接是否屬於需要優先處理的,如果是,則置入prerequisites隊列中,否則,就簡單的加入pendingURIs中即可。
注意:Frontier中還有兩個關鍵的方法,next()和finished(),這兩個方法都是要交由抓取的線程來完成的。Next()方法的主要功能是:從等待隊列中取出一個鏈接並返回,然後抓取線程會在它自己的run()方法中完成對這個鏈接的處理。而finished()方法則是在線程完成對鏈接的抓取和後續的一切動作後(如將鏈接傳遞經過處理器鏈)要執行的。它把整個處理過程中解析出的新的鏈接加入隊列中,並且在處理完當前鏈接後,將之加入alreadyIncluded這個HashMap中去。

需要讀者記住的是,這僅僅是一個最基礎的代碼,它有很多的功能缺失和性能問題,甚至可能出現重大的同步問題。不過儘管如此,它應當也起到了拋磚引玉的作用,能夠從結構上揭示了一個Frontier的作用。
10.2.4 用Berkeley DB實現的BdbFrontier
簡單的說,Berkeley DB就是一個HashTable,它能夠按“key/value”方式來保存數據。它是由美國Sleepycat公司開發的一套開放源代碼的嵌入式數據庫,它爲應用程序提供可伸縮的、高性能的、有事務保護功能的數據管理服務。
那麼,爲什麼不使用一個傳統的關係型數據庫呢?這是因爲當使用BerkeleyDB時,數據庫和應用程序在相同的地址空間中運行,所以數據庫操作不需要進程間的通訊。然而,當使用傳統關係型數據庫時,就需要在一臺機器的不同進程間或在網絡中不同機器間進行進程通訊,這樣所花費的開銷,要遠遠大於函數調用的開銷。
另外,Berkeley DB中的所有操作都使用一組API接口。因此,不需要對某種查詢語言(比如SQL)進行解析,也不用生成執行計劃,這就大大提高了運行效率。
當然,做爲一個數據庫,最重要的功能就是事務的支持,Berkeley DB中的事務子系統就是用來爲其提供事務支持的。它允許把一組對數據庫的修改看作一個原子單位,這組操作要麼全做,要麼全不做。在默認的情況下,系統將提供嚴格的ACID事務屬性,但是應用程序可以選擇不使用系統所作的隔離保證。該子系統使用兩段鎖技術和先寫日誌策略來保證數據的正確性和一致性。這種事務的支持就要比簡單的HashTable中的Synchronize要更加強大。
注意:在Heritrix中,使用的是Berkeley DB的Java版本,這種版本專門爲Java語言做了優化,提供了Java的API接口以供開發者使用。

爲什麼Heritrix中要用到Berkeley DB呢?這就需要再回過頭來看一下Frontier了。
在上一小節中,當一個鏈接被處理後,也即經過處理器鏈後,會生成很多新的鏈接,這些新的鏈接需要被Frontier的一個schedule方法加入到隊列中繼續處理。但是,在將這些新鏈接加入到隊列之前,要首先做一個檢查,即在alreadyIncluded這個HashMap中,查看當前要加入到隊列中的鏈接是否在先前已經被處理過了。
當使用HashMap來存儲那些已經被處理過的鏈接時,HashMap中的key爲url,而value則爲一個對url封裝後的對象。很顯然的,這裏有幾個問題。
l 對這個HashMap的讀取是多線程的,因爲每個線程都需要訪問這個HashMap,以決定當前要加入鏈接是否已經存在過了。
l 對這個HashMap的寫入是多線程的,每個線程在處理完畢後,都會訪問這個HashMap,以寫入最新處理的鏈接。
l 這個HashMap的容量可能很大,可以試想,一次在廣域網範圍上的網頁抓取,可能會涉及到上十億個URL地址,這種地址包括網頁、圖片、文件、多媒體對象等,所以,不可能將這麼大一張表完全的置放於內存中。
綜合考慮以上3點,僅用一個HashMap來保存所有的鏈接,顯然已經不能滿足“大數據量,多併發”這樣的要求。因此,需要尋找一個替代的工具來解決問題。Heritrix中的BdbFrontier就採用了Berkeley DB,來解決這種URL存放的問題。事實上,BdbFrontier就是Berkeley DB Frontier的簡稱。
爲了在BdbFrontier中使用Berkeley DB,Heritrix本身構造了一系列的類來幫助實現這個功能。這些類如下:
l BdbFrontier
l BdbMultipleWorkQueues
l BdbWorkQueue
l BdbUriUniqFilter
上述的4個類,都以Bdb3個字母開頭,這表明它們都是使用到了Berkeley DB的功能。其中:
(1)BdbMultipleWorkQueues代表了一組鏈接隊列,這些隊列有各自不同的key。這樣,由Key和鏈接隊列可以形成一個“Key/Value”對,也就成爲了Berkeley DB裏的一條記錄(DatabaseEntry)如圖10-54所示。

圖10-54 BdbMultipleWorkQueues示意
圖10-54清楚的顯示了Berkeley DB中的“key/value”形式。可以說,這就是一張Berkeley DB的數據庫表。其中,數據庫的一條記錄包含兩個部分,左邊是一個由右邊的所有URL鏈接計算出來的公共鍵值,右邊則是一個URL的隊列。
(2)BdbWorkQueue代表了一個基於Berkeley DB的隊列,與BdbMutipleWorkQueues所不同的是,該隊列中的所有的鏈接都具有相同的鍵值。事實上,BdbWorkQueue只是對BdbMultipleWorkQueues的封裝,在構造一個BdbWorkQueue時,需傳入一個健值,以此做爲該Queue在數據庫中的標識。事實上,在工作線程從Frontier中取出鏈接時,Heritrix總是先取出整個BdbWorkQueue,再從中取出第一個鏈接,然後將當前這個BdbWorkQueue置入一個線程安全的同步容器內,等待線程處理完畢後纔將該Queue釋放,以便該Queue內的其他URI可以繼續被處理。
(3)BdbUriUniqFilter是一個過濾器,從名稱上就能知道,它是專門用來過濾當前要進入等待隊列的鏈接對象是否已經被抓取過。很顯然,在BdbUriUniqFilter內部嵌入了一個Berkeley DB數據庫用於存儲所有的被抓取過的鏈接。它對外提供了
public void add(String key, CandidateURI value)
這樣的接口,以供Frontier調用。當然,若是參數的CandidateURI已經存在於數據庫中了,則該方法會禁止它加入到等待隊列中去。
(4)BdbFrontier就是Heritrix中使用了Berkeley DB的鏈接製造工廠。它主要使用BdbUriUniqFilter,做爲其判斷當前要進入等待隊列的鏈接對象是否已經被抓取過。同時,它還使用了BdbMultipleWorkQueues來做爲所有等待處理的URI的容器。這些URI根據各自的內容會生成一個Hash值成爲它們所在隊列的鍵值。
在Heritrix1.10的版本中,可以說BdbFrontier是惟一一個具有實用意義的鏈接製造工廠了。雖然Heritrix還提供了另外兩個Frontier:
org.archive.crawler.frontier.DomainSensitiveFrontier
org.archive.crawler.frontier.AdaptiveRevisitFrontier
但是,DomainSensitiveFrontier已經被廢棄不再推薦使用了。而AdaptiveRevisitFrontier的算法是不管遇到什麼新鏈接,都義無反顧的再次抓取,這顯然是一種很落後的算法。因此,瞭解BdbFrontier的實現原理,對於更好的瞭解Heritrix對鏈接的處理有實際意義。
BdbFrontier的代碼相對比較複雜,筆者在這裏也只能簡單將其輪廓進行介紹,讀者仍須將代碼仔細研讀,方能把文中的點點知識串聯起來,進而更好的理解Heritrix作者們的巧妙匠心。
10.2.5 Heritrix的多線程ToeThread和ToePool
想要更有效更快速的抓取網頁內容,則必須採用多線程。Heritrix中提供了一個標準的線程池ToePool,它用於管理所有的抓取線程。
ToePool和ToeThread都位於org.archive.crawler.framework包中。前面已經說過,ToePool的初始化,是在CrawlController的initialize()方法中完成的。來看一下ToePool以及ToeThread是如何被初始化的。以下代碼是在CrawlController中用於對ToePool進行初始化的。
// 構造函數
toePool = new ToePool(this);
// 按order.xml中的配置,實例化並啓動線程
toePool.setSize(order.getMaxToes());
ToePool的構造函數很簡單,如下所示:
public ToePool(CrawlController c) {
super("ToeThreads");
this.controller = c;
}
它僅僅是調用了父類java.lang.ThreadGroup的構造函數,同時,將注入的CrawlController賦給類變量。這樣,便建立起了一個線程池的實例了。但是,那些真正的工作線程又是如何建立的呢?
下面來看一下線程池中的setSize(int)方法。從名稱上看,這個方法很像是一個普通的賦值方法,但實際上,它並不是那麼簡單。
代碼10.6
public void setSize(int newsize)
{
targetSize = newsize;
int difference = newsize - getToeCount();

// 如果發現線程池中的實際線程數量小於應有的數量
// 則啓動新的線程
if (difference > 0) {
for(int i = 1; i <= difference; i++) {
// 啓動新線程
startNewThread();
}
}
// 如果線程池中的線程數量已經達到需要
else
{

int retainedToes = targetSize;
// 將線程池中的線程管理起來放入數組中
Thread[] toes = this.getToes();

// 循環去除多餘的線程
for (int i = 0; i < toes.length ; i++) {
if(!(toes[i] instanceof ToeThread)) {
continue;
}
retainedToes--;
if (retainedToes>=0) {
continue;
}
ToeThread tt = (ToeThread)toes[i];
tt.retire();
}
}
}
// 用於取得所有屬於當前線程池的線程
private Thread[] getToes()
{
Thread[] toes = new Thread[activeCount()+10];
// 由於ToePool繼承自java.lang.ThreadGroup類
// 因此當調用enumerate(Thread[] toes)方法時,
// 實際上是將所有該ThreadGroup中開闢的線程放入
// toes這個數組中,以備後面的管理
this.enumerate(toes);
return toes;
}
// 開啓一個新線程
private synchronized void startNewThread()
{
ToeThread newThread = new ToeThread(this, nextSerialNumber++);
newThread.setPriority(DEFAULT_TOE_PRIORITY);
newThread.start();
}
通過上面的代碼可以得出這樣的結論:線程池本身在創建的時候,並沒有任何活動的線程實例,只有當它的setSize方法被調用時,纔有可能創建新線程;如果當setSize方法被調用多次而傳入不同的參數時,線程池會根據參數裏所設定的值的大小,來決定池中所管理線程數量的增減。
當線程被啓動後,所執行的是其run()方法中的片段。接下來,看一個ToeThread到底是如何處理從Frontier中獲得的鏈接的。
代碼10.7
public void run()
{
String name = controller.getOrder().getCrawlOrderName();
logger.fine(getName()+" started for order '"+name+"'");
try {
while ( true )
{
// 檢查是否應該繼續處理
continueCheck();

setStep(STEP_ABOUT_TO_GET_URI);

// 使用Frontier的next方法從Frontier中
// 取出下一個要處理的鏈接
CrawlURI curi = controller.getFrontier().next();

// 同步當前線程
synchronized(this) {
continueCheck();
setCurrentCuri(curi);
}

/*
* 處理取出的鏈接
*/
processCrawlUri();

setStep(STEP_ABOUT_TO_RETURN_URI);

// 檢查是否應該繼續處理
continueCheck();

// 使用Frontier的finished()方法
// 來對剛纔處理的鏈接做收尾工作
// 比如將分析得到的新的鏈接加入
// 到等待隊列中去
synchronized(this) {
controller.getFrontier().finished(currentCuri);
setCurrentCuri(null);
}

// 後續的處理
setStep(STEP_FINISHING_PROCESS);
lastFinishTime = System.currentTimeMillis();
// 釋放鏈接
controller.releaseContinuePermission();
if(shouldRetire) {
break; // from while(true)
}
}
} catch (EndedException e) {
} catch (Exception e) {
logger.log(Level.SEVERE,"Fatal exception in "+getName(),e);
} catch (OutOfMemoryError err) {
seriousError(err);
} finally {
controller.releaseContinuePermission();
}
setCurrentCuri(null);

// 清理緩存數據
this.httpRecorder.closeRecorders();
this.httpRecorder = null;
localProcessors = null;

logger.fine(getName()+" finished for order '"+name+"'");
setStep(STEP_FINISHED);
controller.toeEnded();
controller = null;
}
在上面的方法中,很清楚的顯示了工作線程是如何從Frontier中取得下一個待處理的鏈接,然後對鏈接進行處理,並調用Frontier的finished方法來收尾、釋放鏈接,最後清理緩存、終止單步工作等。另外,其中還有一些日誌操作,主要是爲了記錄每次抓取的各種狀態。
很顯然,以上代碼中,最重要的一行語句是processCrawlUri(),它是真正調用處理鏈來對鏈接進行處理的代碼。其中的內容,放在下一個小節中介紹。
10.2.6 處理鏈和Processor
在本章第一節中介紹了設置處理器鏈相關的內容。從中知道,處理器鏈包括以下幾種:
l PreProcessor
l Fetcher
l Extractor
l Writer
l PostProcessor
爲了很好的表示整個處理器鏈的邏輯結構,以及它們之間的鏈式調用關係,Heritrix設計了幾個API來表示這種邏輯結構。
org.archive.crawler.framework.Processor
org.archive.crawler.framework.ProcessorChain
org.archive.crawler.framework.ProcessorChainList
下面進行詳細講解。
1.Processor類
該類代表着單個的處理器,所有的處理器都是它的子類。在Processor類中有一個process()方法,它被標識爲final類型的,也就是說,它不可以被它的子類所覆蓋。代碼如下。
代碼10.8
public final void process(CrawlURI curi) throws InterruptedException
{
// 設置下一個處理器
curi.setNextProcessor(getDefaultNextProcessor(curi));
try
{
// 判斷當前這個處理器是否爲enabled
if (!((Boolean) getAttribute(ATTR_ENABLED, curi)).booleanValue()) {
return;
}
} catch (AttributeNotFoundException e) {
logger.severe(e.getMessage());
}
// 如果當前的鏈接能夠通過過濾器
// 則調用innerProcess(curi)方法
// 來進行處理
if(filtersAccept(curi)) {
innerProcess(curi);
}
// 如果不能通過過濾器檢查,則調
// 用innerRejectProcess(curi)來處理
else
{
innerRejectProcess(curi);
}
}
方法的含義很簡單。即首先檢查是否允許這個處理器處理該鏈接,如果允許,則檢查當前處理器所自帶的過濾器是否能夠接受這個鏈接。當過濾器的檢查也通過後,則調用innerProcess(curi)方法來處理,如果過濾器的檢查沒有通過,就使用innerRejectProcess(curi)方法處理。
其中innerProcess(curi)和innerRejectProcess(curi)方法都是protected類型的,且本身沒有實現任何內容。很明顯它們是留在子類中,實現具體的處理邏輯。不過大部分的子類都不會重寫innerRejectProcess(curi)方法了,這是因爲反正一個鏈接已經被當前處理器拒絕處理了,就不用再有什麼邏輯了,直接跳到下一個處理器繼續處理就行了。
2.ProcessorChain類
該類表示一個隊列,裏面包括了同種類型的幾個Processor。例如,可以將一組的Extractor加入到同一個ProcessorChain中去。
在一個ProcessorChain中,有3個private類型的類變量:
private final MapType processorMap;
private ProcessorChain nextChain;
private Processor firstProcessor;
其中,processorMap中存放的是當前這個ProcessorChain中所有的Processor。nextChain的類型是ProcessorChain,它表示指向下一個處理器鏈的指針。而firstProcessor則是指向當前隊列中的第一個處理器的指針。
3.ProcessorChainList
從名稱上看,它保存了Heritrix一次抓取任務中所設定的所有處理器鏈,將之做爲一個列表。正常情況下,一個ProcessorChainList中,應該包括有5個ProcessorChain,分別爲PreProcessor鏈、Fetcher鏈、Extractor鏈、Writer鏈和PostProcessor鏈,而每個鏈中又包含有多個的Processor。這樣,就將整個處理器結構合理的表示了出來。
那麼,在ToeThread的processCrawlUri()方法中,又是如何來將一個鏈接循環經過這樣一組結構的呢?請看下面的代碼:
代碼10.9
private void processCrawlUri() throws InterruptedException {
// 設定當前線程的編號
currentCuri.setThreadNumber(this.serialNumber);
// 爲當前處理的URI設定下一個ProcessorChain
currentCuri.setNextProcessorChain(controller.getFirstProcessorChain());

// 設定開始時間
lastStartTime = System.currentTimeMillis();
try {

// 如果還有一個處理鏈沒處理完
while (currentCuri.nextProcessorChain() != null)
{
setStep(STEP_ABOUT_TO_BEGIN_CHAIN);

// 將下個處理鏈中的第一個處理器設定爲
// 下一個處理當前鏈接的處理器
currentCuri.setNextProcessor(currentCuri
.nextProcessorChain().getFirstProcessor());
// 將再下一個處理器鏈設定爲當前鏈接的
// 下一個處理器鏈,因爲此時已經相當於
// 把下一個處理器鏈置爲當前處理器鏈了
currentCuri.setNextProcessorChain(currentCuri
.nextProcessorChain().getNextProcessorChain());

// 開始循環處理當前處理器鏈中的每一個Processor
while (currentCuri.nextProcessor() != null)
{
setStep(STEP_ABOUT_TO_BEGIN_PROCESSOR);
Processor currentProcessor = getProcessor(currentCuri.nextProcessor());
currentProcessorName = currentProcessor.getName();
continueCheck();
// 調用Process方法
currentProcessor.process(currentCuri);
}
}
setStep(STEP_DONE_WITH_PROCESSORS);
currentProcessorName = "";
}
catch (RuntimeExceptionWrapper e) {
// 如果是Berkeley DB的異常
if(e.getCause() == null) {
e.initCause(e.getDetail());
}
recoverableProblem(e);
} catch (AssertionError ae) {
recoverableProblem(ae);
} catch (RuntimeException e) {
recoverableProblem(e);
} catch (StackOverflowError err) {
recoverableProblem(err);
} catch (Error err) {
seriousError(err);
}
}
代碼使用了雙重循環來遍歷整個處理器鏈的結構,第一重循環首先遍歷所有的處理器鏈,第二重循環則在鏈內部遍歷每個Processor,然後調用它的process()方法來執行處理邏輯。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章