在《爬蟲/蜘蛛程序的制作(C#語言)》一文中,已經介紹了爬蟲程序實現的基本方法,可以說,已經實現了爬蟲的功能。只是它存在一個效率問題,下載速度可能很慢。這是兩方面的原因造成的:
1.分析和下載不能同步進行。在《爬蟲/蜘蛛程序的制作(C#語言)》中已經介紹了爬蟲程序的兩個步驟:分析和下載。在單線程的程序中,兩者是無法同時進行的。也就是說,分析時會造成網絡空閒,分析的時間越長,下載的效率越低。反之也是一樣,下載時無法同時進行分析,只有停下下載後才能進行下一步的分析。問題浮出水面,我想大家都會想到:把分析和下載用不同的線程進行,問題不就解決了嗎?
2.只是單線程下載。相信大家都有用過網際快車等下載資源的經歷,它裡面是可以設置線程數的(近年版本默認是10,曾經默認是5)。它會將文件分成與線程數相同的部分,然後每個線程下載自己的那一部分,這樣下載效率就有可能提高。相信大家都有加多線程數,提升下載效率的經歷。但細心的用戶會發現,在帶寬一定的情況下,並不是線程越多,速度越快,而是在某一點達到峰值。爬蟲作為特殊的下載工具,不具備多線程的能力何以有效率可談?爬蟲在信息時代的目的,難道不是快速獲取信息嗎?所以,爬蟲需要有多線程(可控數量)同時下載網頁。
好了,認識、分析完問題,就是解決問題了:
多線程在C#中並不難實現。它有一個命名空間:System.Threading,提供了多線程的支持。
要開啟一個新線程,需要以下的初始化:
ThreadStart startDownload = new ThreadStart( DownLoad );
//線程起始設置:即每個線程都執行DownLoad(),注意:DownLoad()必須為不帶有參數的方法
Thread downloadThread = new Thread( startDownload ); //實例化要開啟的新類
downloadThread.Start();//開啟線程
由於線程起始時啟動的方法不能帶有參數,這就為多線程共享資源添加了麻煩。不過我們可以用類級變量(當然也可以使用其它方法,筆者認為此方法最簡單易用)來解決這個問題。知道開啟多線程下載的方法後,大家可能會產生幾個疑問:
1.如何控制線程的數量?
2.如何防止多線程下載同一網頁?
3.如何判斷線程結束?
4.如何控制線程結束?
下面就這幾個問題提出解決方法:
1.線程數量我們可以通過for循環來實現,就如同當年初學編程的打點程序一樣。
比如已知用戶指定了n(它是一個int型變量)個線程吧,可以用如下方法開啟五個線程。
Thread[] downloadThread;
//聲名下載線程,這是C#的優勢,即數組初始化時,不需要指定其長度,可以在使用時才指定。
這個聲名應為類級,這樣也就為其它方法控件它們提供了可能
ThreadStart startDownload = new ThreadStart( DownLoad );
//線程起始設置:即每個線程都執行DownLoad()
downloadThread = new Thread[ n ];//為線程申請資源,確定線程總數
for( int i = 0; i < n; i++ )//開啟指定數量的線程數
{
downloadThread[i] = new Thread( startDownload );//指定線程起始設置
downloadThread[i].Start();//逐個開啟線程
}
好了,實現控制開啟線程數是不是很簡單啊?
2.下面出現的一個問題:所有的線程都調用DonwLoad()方法,這樣如何避免它們同時下載同一個網頁呢?
這個問題也好解決,只要建立一下Url地址表,表中的每個地址只允許被一個線程申請即可。具體實現:
可以利用數據庫,建立一個表,表中有四列,其中一列專門用於存儲Url地址,另外兩列分別存放地址對應的線程以及該地址被申請的次數,最後一列存放下載的內容。(當然,對應線程一列不是必要的)。當有線程申請後,將對應線程一列設定為當前線程編號,並將是否申請過一列設置為申請一次,這樣,別的線程就無法申請該頁。如果下載成功,則將內容存入內容列。如果不成功,內容列仍為空,作為是否再次下載的依據之一,如果反復不成功,則進程將於達到重試次數(對應該地址被申請的次數,用戶可設)後,申請下一個Url地址。主要的代碼如下(以VFP為例):
<建立表>
CREATE TABLE (ctablename) ( curl M , ctext M , ldowned I , threadNum I )
&&建立一個表ctablename.dbf,含有地址、文本內容、已經嘗試下載次數、
線程標志(初值為-1,線程標志是從0開始的整數)四個字段
<提取Url地址>
cfullname = (ctablename) + '.dbf'&&為表添加擴展名
USE (cfullname)
GO TOP
LOCATE FOR (EMPTY( ALLTRIM( ctext ) ) AND ldowned < 2 AND
( threadNum = thisNum OR threadNum = - 1) )
&&查找尚未下載成功且應下載的屬於本線程權限的Url地址,thisNum是當前線程的編號,
可以通過參數傳遞得到
gotUrl = curl
recNum = RECNO()
IF recNum <= RECCOUNT() THEN &&如果在列表中找到這樣的Url地址
UPDATE (cfullname) SET ldowned = ( ldowned + 1 ) , threadNum =
thisNum WHERE RECNO() = recNum &&更新表,將此記錄更新為已申請,即下載次數加1,
線程標志列設為本線程的編號。
<下載內容>
cfulltablename = (ctablename) + '.dbf'
USE (cfulltablename)
SET EXACT ON
LOCATE FOR curl = (csiteurl) && csiteurl是參數,為下載到的內容所對應的Url地址
recNumNow = RECNO()&&得到含有此地址的記錄號
UPDATE (cfulltablename) SET ctext = (ccontent) WHERE RECNO() =
recNumNow &&插入對應地址的對應內容
<插入新地址>
ctablename = (ctablename) + '.dbf'
USE (ctablename)
GO TOP
SET EXACT ON
LOCATE FOR curl = (cnewurl) &&查找有無此地址
IF RECNO() > RECCOUNT() THEN &&如果尚無此地址
SET CARRY OFF
INSERT INTO (ctablename) ( curl , ctext , ldowned , threadNum )
VALUES ( (cnewurl) , "" , 0 , -1 ) &&將主頁地址添加到列表
好了,這樣就解決了多線程中,線程沖突。當然,去重問題也可以在C#語言內解決,只根建立一個臨時文件(文本就可以),保存所有的Url地址,差對它們設置相應的屬性即可,但查找效率可能不及數據庫快。
3.線程結束是很難判斷的,因為它總是在查找新的鏈接。用者認為可以假設:線程重復N次以後還是沒有能申請到新的Url地址,那麼可以認為它已經下載完了所有鏈接。主要代碼如下:
string url = "";
int times = 0;
while ( url == "" )//如果沒有找到符合條件的記錄,則不斷地尋找符合條件的記錄
{
url = getUrl.GetAUrl( …… );//調用GetAUrl方法,試圖得到一個url值
if ( url == "" )//如果沒有找到
{
times ++;//嘗試次數自增
continue; //進行下一次嘗試
}
if ( times > N ) //如果已經嘗試夠了次數,則退出進程
{
downloadThread[i].Abort; //退出進程
}
else//如果沒有嘗試夠次數
{
Times = 0; //嘗試次數歸零處理
}
//進行下一步針對得到的Url的處理
}
4.這個問題相對簡單,因為在問題一中已經建議,將線程聲名為類級數組,這樣就很易於控制。只要用一個for循環即可結束。代碼如下:
for( int i = 0; i < n; i++ )//關閉指定數量n的線程數
{
downloadThread[i].Abort();//逐個關閉線程
}
好了,一個蜘蛛程序就這樣完成了,在C#面前,它的實現原來如此簡單。
這裡筆者還想提醒讀者:筆者只是提供了一個思路及一個可以實現的解決方案,但它並不是最佳的,即使這個方案本身,也有好多可以改進的地方,留給讀者思考。
最後說明一下我所使用的環境:
winXP sp2 Pro
VFP 9.0
Visual Studio 2003 .net中文企業版