蜘蛛/爬蟲程序的多線程控制(C#)

2010-08-28 10:52:01來源:西部e網(wǎng)作者:

爬蟲/蜘蛛程序的制作

問題是對某一網(wǎng)站或所有網(wǎng)站進行抓取,即下載所有網(wǎng)頁。怎么實現(xiàn)呢?

先將問題最小化(轉化的思想,轉化為小規(guī)模,可以解決的問題):如果只有一個網(wǎng)頁,怎么下載?問題變地很簡單,只要用WebClient/WebRequest(甚至OpenFileDialog都可以)打開Url地址,將數(shù)據(jù)流存入本地存儲器的文件(以相應的擴展名作為擴展名)即可。示例代碼如下:

string BoardStream;//下載內容存入此變量

Uri url = new Uri( “http://www.163.com” );//將下載地址轉換為Uri類型

HttpWebRequest requestPage = ( HttpWebRequest )WebRequest.Create( url );

WebResponse response = requestMainPage.GetResponse();

Stream stream = response.GetResponseStream();//獲取頁面流

if( response.ContentType.ToLower().StartsWith( "text/" ) )//如果獲得成功(即為文本格式)

{

    StreamReader reader = new StreamReader( stream , System.Text.Encoding.UTF8 );//讀取獲得內容流

BoardStream = reader.ReadToEnd();//將內容流轉換為文本并存入變量BoardStream,即為所需要的數(shù)據(jù)流

}

StreamWriter saveAPage = new StreamWriter( “C:\a.html” , false , System.Text.Encoding.GetEncoding( "gb2312" ) );//實例化寫入類,保存路徑假設為C:\a.html

saveAPage.Write(Rich.Text);//創(chuàng)建寫入任務

saveAPage.Flush();//寫入文件(即清理緩存流)

saveAPage.Close();//關閉寫入類的對象

好了,這樣便完成了一個網(wǎng)頁的下載。最簡化問題解決!

 

好了,下面的問題是,如何獲得更多的網(wǎng)頁?可以分兩步:

1.    得到更多的地址

2.    下載地址指向的鏈接內容(和上面下載一頁的方法一樣)

循環(huán)進行上面兩步即可以完成蜘蛛的全部功能了 '

要得到更多的地址,最好的辦法是模擬人使用網(wǎng)頁的辦法。我們平時怎么瀏覽整個網(wǎng)站?無非是從主頁依次點開各層鏈接而已。好了,思路出來了:

分析已經(jīng)下載的主頁文本,提取其中所有的Url地址信息,再依次下載得到的Url地址指向的鏈接即可。

現(xiàn)在網(wǎng)絡上有不少Web2.0的網(wǎng)站了,這對解析Url地址有不小的負面作用。在Web2.0出現(xiàn)前,所有的鏈接都是在HREF后面出現(xiàn)的,而現(xiàn)在卻沒有了這樣的關鍵字,地址可能出現(xiàn)于任何的關鍵字之后。怎么辦呢?

經(jīng)過大量分析,筆者發(fā)現(xiàn):其實現(xiàn)在所有的鏈接還有一個共性,即都包裹在雙引號(””)當中,這便對解析提供了極大的方便。筆者將鏈接分為兩類:

1.    完整鏈接,即:”http://www.163.com”類,其前面有明顯的標志http://,這樣的內容很好提取,只要用String的靜態(tài)方法IndexOf()找出http://的位置以及從此位置算起第一個“””出現(xiàn)的位置(即鏈接結束的位置),再用SubString()方法將地址提取出來即可。

2.    非完整鏈接,其形式一般為”\index.htm”,提取方法和完整鏈接的方法相同,只是判斷它是不是鏈接上有一定難度(因為屬性等其它信息也可能以“”\”開頭,這時就很難判斷了。筆者采取的方法是試下載,即下載一下試試,如果超時剛不是,不超時剛是。注意:要在它的前面加上根地址,如“http://www.163.com/index.htm”。

 在《爬蟲/蜘蛛程序的制作(C#語言)》一文中,已經(jīng)介紹了爬蟲程序實現(xiàn)的基本方法,可以說,已經(jīng)實現(xiàn)了爬蟲的功能。只是它存在一個效率問題,下載速度可能很慢。這是兩方面的原因造成的:

1.分析和下載不能同步進行。在《爬蟲/蜘蛛程序的制作(C#語言)》中已經(jīng)介紹了爬蟲程序的兩個步驟:分析和下載。在單線程的程序中,兩者是無法同時進行的。也就是說,分析時會造成網(wǎng)絡空閑,分析的時間越長,下載的效率越低。反之也是一樣,下載時無法同時進行分析,只有停下下載后才能進行下一步的分析。問題浮出水面,我想大家都會想到:把分析和下載用不同的線程進行,問題不就解決了嗎?

2.只是單線程下載。相信大家都有用過網(wǎng)際快車等下載資源的經(jīng)歷,它里面是可以設置線程數(shù)的(近年版本默認是10,曾經(jīng)默認是5)。它會將文件分成與線程數(shù)相同的部分,然后每個線程下載自己的那一部分,這樣下載效率就有可能提高。相信大家都有加多線程數(shù),提升下載效率的經(jīng)歷。但細心的用戶會發(fā)現(xiàn),在帶寬一定的情況下,并不是線程越多,速度越快,而是在某一點達到峰值。爬蟲作為特殊的下載工具,不具備多線程的能力何以有效率可談?爬蟲在信息時代的目的,難道不是快速獲取信息嗎?所以,爬蟲需要有多線程(可控數(shù)量)同時下載網(wǎng)頁。

好了,認識、分析完問題,就是解決問題了:

多線程在C#中并不難實現(xiàn)。它有一個命名空間:System.Threading,提供了多線程的支持。

要開啟一個新線程,需要以下的初始化:

ThreadStart startDownload = new ThreadStart( DownLoad ); //線程起始設置:即每個線程都執(zhí)行DownLoad(),注意:DownLoad()必須為不帶有參數(shù)的方法Thread downloadThread = new Thread( startDownload ); //實例化要開啟的新類downloadThread.Start();//開啟線程
 


由于線程起始時啟動的方法不能帶有參數(shù),這就為多線程共享資源添加了麻煩。不過我們可以用類級變量(當然也可以使用其它方法,筆者認為此方法最簡單易用)來解決這個問題。知道開啟多線程下載的方法后,大家可能會產生幾個疑問:

1.如何控制線程的數(shù)量?

2.如何防止多線程下載同一網(wǎng)頁?

3.如何判斷線程結束?

4.如何控制線程結束?

下面就這幾個問題提出解決方法:

1.線程數(shù)量我們可以通過for循環(huán)來實現(xiàn),就如同當年初學編程的打點程序一樣。

比如已知用戶指定了n(它是一個int型變量)個線程吧,可以用如下方法開啟五個線程。

Thread[] downloadThread;//聲名下載線程,這是C#的優(yōu)勢,即數(shù)組初始化時,不需要指定其長度,可以在使用時才指定。這個聲名應為類級,這樣也就為其它方法控件它們提供了可能ThreadStart startDownload = new ThreadStart( DownLoad );//線程起始設置:即每個線程都執(zhí)行DownLoad()downloadThread = new Thread[ n ];//為線程申請資源,確定線程總數(shù)for( int i = 0; i < n; i++ )//開啟指定數(shù)量的線程數(shù){downloadThread[i] = new Thread( startDownload );//指定線程起始設置downloadThread[i].Start();//逐個開啟線程}
 


好了,實現(xiàn)控制開啟線程數(shù)是不是很簡單。

2.下面出現(xiàn)的一個問題:所有的線程都調用DonwLoad()方法,這樣如何避免它們同時下載同一個網(wǎng)頁呢?

這個問題也好解決,只要建立一下Url地址表,表中的每個地址只允許被一個線程申請即可。具體實現(xiàn):

可以利用數(shù)據(jù)庫,建立一個表,表中有四列,其中一列專門用于存儲Url地址,另外兩列分別存放地址對應的線程以及該地址被申請的次數(shù),最后一列存放下載的內容。(當然,對應線程一列不是必要的)。當有線程申請后,將對應線程一列設定為當前線程編號,并將是否申請過一列設置為申請一次,這樣,別的線程就無法申請該頁。如果下載成功,則將內容存入內容列。如果不成功,內容列仍為空,作為是否再次下載的依據(jù)之一,如果反復不成功,則進程將于達到重試次數(shù)(對應該地址被申請的次數(shù),用戶可設)后,申請下一個Url地址。主要的代碼如下(以VFP為例):

<建立表>Create TABLE (ctablename) ( curl M , ctext M , ldowned I , threadNum I ) &&建立一個表ctablename.dbf,含有地址、文本內容、已經(jīng)嘗試下載次數(shù)、線程標志(初值為-1,線程標志是從0開始的整數(shù))四個字段<提取Url地址>cfullname = (ctablename) + '.dbf'&&為表添加擴展名USE (cfullname)  GO TOPLOCATE FOR (EMPTY( ALLTRIM( ctext ) ) AND ldowned < 2 AND ( threadNum = thisNum or threadNum = - 1) )  &&查找尚未下載成功且應下載的屬于本線程權限的Url地址,thisNum是當前線程的編號,可以通過參數(shù)傳遞得到gotUrl = curl recNum = RECNO()IF recNum <= RECCOUNT() THEN  &&如果在列表中找到這樣的Url地址Update (cfullname) SET ldowned = ( ldowned + 1 ) , threadNum = thisNum Where RECNO() = recNum &&更新表,將此記錄更新為已申請,即下載次數(shù)加1,線程標志列設為本線程的編號。<下載內容>cfulltablename = (ctablename) + '.dbf'USE (cfulltablename)SET EXACT ON LOCATE FOR curl = (csiteurl) && csiteurl是參數(shù),為下載到的內容所對應的Url地址recNumNow = RECNO()&&得到含有此地址的記錄號Update (cfulltablename) SET ctext = (ccontent) Where RECNO() = recNumNow &&插入對應地址的對應內容<插入新地址>ctablename = (ctablename) + '.dbf'USE (ctablename)GO TOP SET EXACT ONLOCATE FOR curl = (cnewurl) &&查找有無此地址IF RECNO() > RECCOUNT() THEN &&如果尚無此地址SET CARRY OFFInsert INTO (ctablename) ( curl , ctext , ldowned , threadNum ) VALUES ( (cnewurl) , "" , 0 , -1 )  &&將主頁地址添加到列表
 


好了,這樣就解決了多線程中,線程沖突。當然,去重問題也可以在C#語言內解決,只根建立一個臨時文件(文本就可以),保存所有的Url地址,差對它們設置相應的屬性即可,但查找效率可能不及數(shù)據(jù)庫快。

 


3.線程結束是很難判斷的,因為它總是在查找新的鏈接。用者認為可以假設:線程重復N次以后還是沒有能申請到新的Url地址,那么可以認為它已經(jīng)下載完了所有鏈接。主要代碼如下:

string url = "";int times = 0;while ( url == "" )//如果沒有找到符合條件的記錄,則不斷地尋找符合條件的記錄{url = getUrl.GetAUrl( …… );//調用GetAUrl方法,試圖得到一個url值if ( url == "" )//如果沒有找到{times ++;//嘗試次數(shù)自增continue; //進行下一次嘗試}if ( times > N ) //如果已經(jīng)嘗試夠了次數(shù),則退出進程{downloadThread[i].Abort; //退出進程}else//如果沒有嘗試夠次數(shù){Times = 0; //嘗試次數(shù)歸零處理}//進行下一步針對得到的Url的處理}
 


4.這個問題相對簡單,因為在問題一中已經(jīng)建議,將線程聲名為類級數(shù)組,這樣就很易于控制。只要用一個for循環(huán)即可結束。代碼如下:

for( int i = 0; i < n; i++ )//關閉指定數(shù)量n的線程數(shù){downloadThread[i].Abort();//逐個關閉線程}
 


好了,一個蜘蛛程序就這樣完成了,在C#面前,它的實現(xiàn)原來如此簡單。

這里筆者還想提醒讀者:筆者只是提供了一個思路及一個可以實現(xiàn)的解決方案,但它并不是最佳的,即使這個方案本身,也有好多可以改進的地方,留給讀者思考。

最后說明一下我所使用的環(huán)境:

winXP sp2 Pro

VFP 9.0

Visual Studio 2003 .net中文企業(yè)版

下一篇:蜘蛛/爬蟲程序的多線程控制

在《爬蟲/蜘蛛程序的制作(C#語言)》一文中,已經(jīng)介紹了爬蟲程序實現(xiàn)的基本方法,可以說,已經(jīng)實現(xiàn)了爬蟲的功能。只是它存在一個效率問題,下載速度可能很慢。這是兩方面的原因造成的:

1.分析和下載不能同步進行。在《爬蟲/蜘蛛程序的制作(C#語言)》中已經(jīng)介紹了爬蟲程序的兩個步驟:分析和下載。在單線程的程序中,兩者是無法同時進行的。也就是說,分析時會造成網(wǎng)絡空閑,分析的時間越長,下載的效率越低。反之也是一樣,下載時無法同時進行分析,只有停下下載后才能進行下一步的分析。問題浮出水面,我想大家都會想到:把分析和下載用不同的線程進行,問題不就解決了嗎?

2.只是單線程下載。相信大家都有用過網(wǎng)際快車等下載資源的經(jīng)歷,它里面是可以設置線程數(shù)的(近年版本默認是10,曾經(jīng)默認是5)。它會將文件分成與線程數(shù)相同的部分,然后每個線程下載自己的那一部分,這樣下載效率就有可能提高。相信大家都有加多線程數(shù),提升下載效率的經(jīng)歷。但細心的用戶會發(fā)現(xiàn),在帶寬一定的情況下,并不是線程越多,速度越快,而是在某一點達到峰值。爬蟲作為特殊的下載工具,不具備多線程的能力何以有效率可談?爬蟲在信息時代的目的,難道不是快速獲取信息嗎?所以,爬蟲需要有多線程(可控數(shù)量)同時下載網(wǎng)頁。

好了,認識、分析完問題,就是解決問題了:

多線程在C#中并不難實現(xiàn)。它有一個命名空間:System.Threading,提供了多線程的支持。

要開啟一個新線程,需要以下的初始化:

ThreadStart startDownload = new ThreadStart( DownLoad );

//線程起始設置:即每個線程都執(zhí)行DownLoad(),注意:DownLoad()必須為不帶有參數(shù)的方法

Thread downloadThread = new Thread( startDownload ); //實例化要開啟的新類

downloadThread.Start();//開啟線程

由于線程起始時啟動的方法不能帶有參數(shù),這就為多線程共享資源添加了麻煩。不過我們可以用類級變量(當然也可以使用其它方法,筆者認為此方法最簡單易用)來解決這個問題。知道開啟多線程下載的方法后,大家可能會產生幾個疑問:

1.如何控制線程的數(shù)量?

2.如何防止多線程下載同一網(wǎng)頁?

3.如何判斷線程結束?

4.如何控制線程結束?

下面就這幾個問題提出解決方法:

1.線程數(shù)量我們可以通過for循環(huán)來實現(xiàn),就如同當年初學編程的打點程序一樣。

比如已知用戶指定了n(它是一個int型變量)個線程吧,可以用如下方法開啟五個線程。

Thread[] downloadThread;

//聲名下載線程,這是C#的優(yōu)勢,即數(shù)組初始化時,不需要指定其長度,可以在使用時才指定。

這個聲名應為類級,這樣也就為其它方法控件它們提供了可能

ThreadStart startDownload = new ThreadStart( DownLoad );

//線程起始設置:即每個線程都執(zhí)行DownLoad()

downloadThread = new Thread[ n ];//為線程申請資源,確定線程總數(shù)

for( int i = 0; i < n; i++ )//開啟指定數(shù)量的線程數(shù)

{

downloadThread[i] = new Thread( startDownload );//指定線程起始設置

downloadThread[i].Start();//逐個開啟線程

}

好了,實現(xiàn)控制開啟線程數(shù)是不是很簡單?

2.下面出現(xiàn)的一個問題:所有的線程都調用DonwLoad()方法,這樣如何避免它們同時下載同一個網(wǎng)頁呢?

這個問題也好解決,只要建立一下Url地址表,表中的每個地址只允許被一個線程申請即可。具體實現(xiàn):

可以利用數(shù)據(jù)庫,建立一個表,表中有四列,其中一列專門用于存儲Url地址,另外兩列分別存放地址對應的線程以及該地址被申請的次數(shù),最后一列存放下載的內容。(當然,對應線程一列不是必要的)。當有線程申請后,將對應線程一列設定為當前線程編號,并將是否申請過一列設置為申請一次,這樣,別的線程就無法申請該頁。如果下載成功,則將內容存入內容列。如果不成功,內容列仍為空,作為是否再次下載的依據(jù)之一,如果反復不成功,則進程將于達到重試次數(shù)(對應該地址被申請的次數(shù),用戶可設)后,申請下一個Url地址。主要的代碼如下(以VFP為例):

<建立表>

CREATE TABLE (ctablename) ( curl M , ctext M , ldowned I , threadNum I )

&&建立一個表ctablename.dbf,含有地址、文本內容、已經(jīng)嘗試下載次數(shù)、

線程標志(初值為-1,線程標志是從0開始的整數(shù))四個字段

<提取Url地址>

cfullname = (ctablename) + '.dbf'&&為表添加擴展名

USE (cfullname) 

GO TOP

LOCATE FOR (EMPTY( ALLTRIM( ctext ) ) AND ldowned < 2 AND

( threadNum = thisNum OR threadNum = - 1) ) 

&&查找尚未下載成功且應下載的屬于本線程權限的Url地址,thisNum是當前線程的編號,

可以通過參數(shù)傳遞得到

gotUrl = curl

recNum = RECNO()

IF recNum <= RECCOUNT() THEN  &&如果在列表中找到這樣的Url地址

UPDATE (cfullname) SET ldowned = ( ldowned + 1 ) , threadNum =

thisNum WHERE RECNO() = recNum &&更新表,將此記錄更新為已申請,即下載次數(shù)加1,

線程標志列設為本線程的編號。

<下載內容>

cfulltablename = (ctablename) + '.dbf'

USE (cfulltablename)

SET EXACT ON

LOCATE FOR curl = (csiteurl) && csiteurl是參數(shù),為下載到的內容所對應的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地址,差對它們設置相應的屬性即可,但查找效率可能不及數(shù)據(jù)庫快。

關鍵詞:C#

贊助商鏈接: