用C#实现网络爬虫(一)

来源:互联网 发布:德军总部新巨人 知乎 编辑:程序博客网 时间:2024/06/04 20:17

转自:http://www.cnblogs.com/Jiajun/archive/2012/06/16/2552103.html

网络爬虫在信息检索与处理中有很大的作用,是收集网络信息的重要工具。

接下来就介绍一下爬虫的简单实现。

爬虫的工作流程如下

爬虫自指定的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) //判断是否还有未下载的URL10              {11                  _workingSignals.FinishWorking(index); //设置工作实例的状态为Finished12                  return;13              }14              _reqsBusy[index] = true;15              _workingSignals.StartWorking(index); //设置工作状态为Working16              depth = _urlsUnload.First().Value; //取出第一个未下载的URL17              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 Html13     {14         get15         {16             return _sb;17         }18     }19 20     public byte[] Data21     {22         get23         {24             return _data;25         }26     }27 28     public int BufferSize29     {30         get31         {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     try13     {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()) //检查是否所有工作实例都为Finished10     {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;
复制代码

 


1 0
原创粉丝点击