用C#實現網絡爬蟲(一)


網絡爬蟲在信息檢索與處理中有很大的作用,是收集網絡信息的重要工具。

接下來就介紹一下爬蟲的簡單實現。

爬蟲的工作流程如下

爬蟲自指定的URL地址開始下載網絡資源,直到該地址和所有子地址的指定資源都下載完畢為止。

下面開始逐步分析爬蟲的實現。

 

1. 待下載集合與已下載集合

為了保存需要下載的URL,同時防止重復下載,我們需要分別用了兩個集合來存放將要下載的URL和已經下載的URL。

因為在保存URL的同時需要保存與URL相關的一些其他信息,如深度,所以這里我采用了Dictionary來存放這些URL。

具體類型是Dictionary<string, int> 其中string是Url字符串,int是該Url相對於基URL的深度。

每次開始時都檢查未下載的集合,如果已經為空,說明已經下載完畢;如果還有URL,那么就取出第一個URL加入到已下載的集合中,並且下載這個URL的資源。

 

2. HTTP請求和響應

C#已經有封裝好的HTTP請求和響應的類HttpWebRequestHttpWebResponse,所以實現起來方便不少。

為了提高下載的效率,我們可以用多個請求並發的方式同時下載多個URL的資源,一種簡單的做法是采用異步請求的方法。

控制並發的數量可以用如下方法實現

 1 private void DispatchWork()  2 {  3     if (_stop) //判斷是否中止下載
 4  {  5         return;  6  }  7     for (int i = 0; i < _reqCount; i++)  8  {  9         if (!_reqsBusy[i]) //判斷此編號的工作實例是否空閑
10  { 11             RequestResource(i); //讓此工作實例請求資源
12  } 13  } 14 }

 由於沒有顯式開新線程,所以用一個工作實例來表示一個邏輯工作線程

1 private bool[] _reqsBusy = null; //每個元素代表一個工作實例是否正在工作
2 private int _reqCount = 4; //工作實例的數量

 每次一個工作實例完成工作,相應的_reqsBusy就設為false,並調用DispatchWork,那么DispatchWork就能給空閑的實例分配新任務了。

 

 接下來是發送請求

 1 private void RequestResource(int index)
 2  {
 3      int depth;
 4      string url = "";
 5      try
 6      {
 7          lock (_locker)
 8          {
 9              if (_urlsUnload.Count <= 0) //判斷是否還有未下載的URL
10              {
11                  _workingSignals.FinishWorking(index); //設置工作實例的狀態為Finished
12                  return;
13              }
14              _reqsBusy[index] = true;
15              _workingSignals.StartWorking(index); //設置工作狀態為Working
16              depth = _urlsUnload.First().Value; //取出第一個未下載的URL
17              url = _urlsUnload.First().Key;
18              _urlsLoaded.Add(url, depth); //把該URL加入到已下載里
19              _urlsUnload.Remove(url); //把該URL從未下載中移除
20          }
21                  
22          HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
23          req.Method = _method; //請求方法
24          req.Accept = _accept; //接受的內容
25          req.UserAgent = _userAgent; //用戶代理
26          RequestState rs = new RequestState(req, url, depth, index); //回調方法的參數
27          var result = req.BeginGetResponse(new AsyncCallback(ReceivedResource), rs); //異步請求
28          ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, //注冊超時處理方法
29                  TimeoutCallback, rs, _maxTime, true);
30      }
31      catch (WebException we)
32      {
33          MessageBox.Show("RequestResource " + we.Message + url + we.Status);
34      }
35  }

第7行為了保證多個任務並發時的同步,加上了互斥鎖。_locker是一個Object類型的成員變量。

第9行判斷未下載集合是否為空,如果為空就把當前工作實例狀態設為Finished;如果非空則設為Working並取出一個URL開始下載。當所有工作實例都為Finished的時候,說明下載已經完成。由於每次下載完一個URL后都調用DispatchWork,所以可能激活其他的Finished工作實例重新開始工作。

第26行的請求的額外信息在異步請求的回調方法作為參數傳入,之后還會提到。

第27行開始異步請求,這里需要傳入一個回調方法作為響應請求時的處理,同時傳入回調方法的參數。

第28行給該異步請求注冊一個超時處理方法TimeoutCallback,最大等待時間是_maxTime,且只處理一次超時,並傳入請求的額外信息作為回調方法的參數。

 

RequestState的定義是

 1 class RequestState  2 {  3     private const int BUFFER_SIZE = 131072; //接收數據包的空間大小
 4     private byte[] _data = new byte[BUFFER_SIZE]; //接收數據包的buffer
 5     private StringBuilder _sb = new StringBuilder(); //存放所有接收到的字符
 6 
 7     public HttpWebRequest Req { get; private set; } //請求
 8     public string Url { get; private set; } //請求的URL
 9     public int Depth { get; private set; } //此次請求的相對深度
10     public int Index { get; private set; } //工作實例的編號
11     public Stream ResStream { get; set; } //接收數據流
12     public StringBuilder Html 13  { 14         get
15  { 16             return _sb; 17  } 18  } 19 
20     public byte[] Data 21  { 22         get
23  { 24             return _data; 25  } 26  } 27 
28     public int BufferSize 29  { 30         get
31  { 32             return BUFFER_SIZE; 33  } 34  } 35 
36     public RequestState(HttpWebRequest req, string url, int depth, int index) 37  { 38         Req = req; 39         Url = url; 40         Depth = depth; 41         Index = index; 42  } 43 } 

  

TimeoutCallback的定義是

 1 private void TimeoutCallback(object state, bool timedOut)  2 {  3     if (timedOut) //判斷是否是超時
 4  {  5         RequestState rs = state as RequestState;  6         if (rs != null)  7  {  8             rs.Req.Abort(); //撤銷請求
 9  } 10         _reqsBusy[rs.Index] = false; //重置工作狀態
11         DispatchWork(); //分配新任務
12  } 13 }

 

接下來就是要處理請求的響應了

 1 private void ReceivedResource(IAsyncResult ar)  2 {  3     RequestState rs = (RequestState)ar.AsyncState; //得到請求時傳入的參數
 4     HttpWebRequest req = rs.Req;  5     string url = rs.Url;  6     try
 7  {  8         HttpWebResponse res = (HttpWebResponse)req.EndGetResponse(ar); //獲取響應
 9         if (_stop) //判斷是否中止下載
10  { 11  res.Close(); 12  req.Abort(); 13             return; 14  } 15         if (res != null && res.StatusCode == HttpStatusCode.OK) //判斷是否成功獲取響應
16  { 17             Stream resStream = res.GetResponseStream(); //得到資源流
18             rs.ResStream = resStream; 19             var result = resStream.BeginRead(rs.Data, 0, rs.BufferSize, //異步請求讀取數據
20                 new AsyncCallback(ReceivedData), rs); 21  } 22         else //響應失敗
23  { 24  res.Close(); 25  rs.Req.Abort(); 26             _reqsBusy[rs.Index] = false; //重置工作狀態
27             DispatchWork(); //分配新任務
28  } 29  } 30     catch (WebException we) 31  { 32         MessageBox.Show("ReceivedResource " + we.Message + url + we.Status); 33  } 34

第19行這里采用了異步的方法來讀數據流是因為我們之前采用了異步的方式請求,不然的話不能夠正常的接收數據。

該異步讀取的方式是按包來讀取的,所以一旦接收到一個包就會調用傳入的回調方法ReceivedData,然后在該方法中處理收到的數據。

該方法同時傳入了接收數據的空間rs.Data和空間的大小rs.BufferSize

 

接下來是接收數據和處理

 1 private void ReceivedData(IAsyncResult ar)  2 {  3     RequestState rs = (RequestState)ar.AsyncState; //獲取參數
 4     HttpWebRequest req = rs.Req;  5     Stream resStream = rs.ResStream;  6     string url = rs.Url;  7     int depth = rs.Depth;  8     string html = null;  9     int index = rs.Index; 10     int read = 0; 11 
12     try
13  { 14         read = resStream.EndRead(ar); //獲得數據讀取結果
15         if (_stop)//判斷是否中止下載
16  { 17  rs.ResStream.Close(); 18  req.Abort(); 19             return; 20  } 21         if (read > 0) 22  { 23             MemoryStream ms = new MemoryStream(rs.Data, 0, read); //利用獲得的數據創建內存流
24             StreamReader reader = new StreamReader(ms, _encoding); 25             string str = reader.ReadToEnd(); //讀取所有字符
26             rs.Html.Append(str); // 添加到之前的末尾
27             var result = resStream.BeginRead(rs.Data, 0, rs.BufferSize, //再次異步請求讀取數據
28                 new AsyncCallback(ReceivedData), rs); 29             return; 30  } 31         html = rs.Html.ToString(); 32         SaveContents(html, url); //保存到本地
33         string[] links = GetLinks(html); //獲取頁面中的鏈接
34         AddUrls(links, depth + 1); //過濾鏈接並添加到未下載集合中
35 
36         _reqsBusy[index] = false; //重置工作狀態
37         DispatchWork(); //分配新任務
38  } 39     catch (WebException we) 40  { 41         MessageBox.Show("ReceivedData Web " + we.Message + url + we.Status); 42  } 43 } 

第14行獲得了讀取的數據大小read,如果read>0說明數據可能還沒有讀完,所以在27行繼續請求讀下一個數據包;

如果read<=0說明所有數據已經接收完畢,這時rs.Html中存放了完整的HTML數據,就可以進行下一步的處理了。

第26行把這一次得到的字符串拼接在之前保存的字符串的后面,最后就能得到完整的HTML字符串。

 

然后說一下判斷所有任務完成的處理

 1 private void StartDownload()
 2 {
 3     _checkTimer = new Timer(new TimerCallback(CheckFinish), null, 0, 300);
 4     DispatchWork();
 5 }
 6 
 7 private void CheckFinish(object param)
 8 {
 9     if (_workingSignals.IsFinished()) //檢查是否所有工作實例都為Finished
10     {
11         _checkTimer.Dispose(); //停止定時器
12         _checkTimer = null;
13         if (DownloadFinish != null && _ui != null) //判斷是否注冊了完成事件
14         {
15             _ui.Dispatcher.Invoke(DownloadFinish, _index); //調用事件
16         }
17     }
18 }

第3行創建了一個定時器,每過300ms調用一次CheckFinish來判斷是否完成任務。
第15行提供了一個完成任務時的事件,可以給客戶程序注冊。_index里存放了當前下載URL的個數。

該事件的定義是

1 public delegate void DownloadFinishHandler(int count);
2 
3 /// <summary>
4 /// 全部鏈接下載分析完畢后觸發
5 /// </summary>
6 public event DownloadFinishHandler DownloadFinish = null;

 

下接后篇《用C#實現網絡爬蟲(二)》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM