当前位置:首页 > 计算机相关 > .net专区 > 正文内容

用C#实现网络爬虫

piikee11年前 (2012-12-29).net专区627
网络爬虫在信息检索与处理中有很大的作用,是收集网络信息的重要工具。
接下来就介绍一下爬虫的简单实现。
爬虫的工作流程如下
210
爬虫自指定的URL地址开始下载网络资源,直到该地址和所有子地址的指定资源都下载完毕为止。
下面开始逐步分析爬虫的实现。
1. 待下载集合与已下载集合
为了保存需要下载的URL,同时防止重复下载,我们需要分别用了两个集合来存放将要下载的URL和已经下载的URL。
因为在保存URL的同时需要保存与URL相关的一些其他信息,如深度,所以这里我采用了Dictionary来存放这些URL。
具体类型是Dictionary<string, int> 其中string是Url字符串,int是该Url相对于基URL的深度。
每次开始时都检查未下载的集合,如果已经为空,说明已经下载完毕;如果还有URL,那么就取出第一个URL加入到已下载的集合中,并且下载这个URL的资源。
2. HTTP请求和响应
C#已经有封装好的HTTP请求和响应的类HttpWebRequest和HttpWebResponse,所以实现起来方便不少。
为了提高下载的效率,  我们可以用多个请求并发的方式同时下载多个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)
10             {
11                 _workingSignals.FinishWorking(index);
12                 return;
13             }
14             _reqsBusy[index] = true;
15             _workingSignals.StartWorking(index);
16             depth = _urlsUnload.First().Value;
17             url = _urlsUnload.First().Key;
18             _urlsLoaded.Add(url, depth);
19             _urlsUnload.Remove(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 }
第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数据,就可以进行下一步的处理了。

扫描二维码推送至手机访问。

版权声明:本文由萍客小居发布,如需转载请注明出处。

本文链接:https://www.piikee.net/992.html

分享给朋友:

相关文章

让SQL Server2005允许远程连接

在尝试从远程计算机连接到 Microsoft SQL Server 2005 实例时,可能会接收到错误消息。在使用任何程序连接到 SQL Server 时都可能会发生此问题。例如,在使用 SQLCMD 实用工具连接到 SQL Server...

SQL 2005中设置两个主键的方法

SQL2005中,可视化见表,选中一行设置主键,然后再选一行设置主键,则刚才设置的主键就自动取消了。怎么设置两个主键呢。其实Windows中很多东西都是融会贯通的,想一想多选的Ctrl键不就解决了~ 按住Ctrl,用鼠标选两个行,然后右键,...

C#窗体也有DOCK属性

在一个主窗体里面,放入一个PANEL和一个窗体。然后,里面那个窗体最大化时,会覆盖了PANEL或者被PANEL覆盖。要让里面的窗体最大化时刚好把PANEL剩下的空间填满可以这样做: 把form2.Dock = DockStyle.Fill;...

AjaxControlToolkit中各个控件功能说明

1.Accordion控件1.1【功能概述】Accordion可以让你设计多个panel 并且一次只显示一个Panel .在页面上的显示效果就像是使用了多个CollapsiblePanels只不过每一次只展开其中一个CollapsibleP...

C#的窗体(WinForm)退出杂谈

1、Winform点击退出按钮,需要确认再退出:private void MainForm_FormClosing(object sender, FormClosingEventArgs e){if (MessageBox.Show("是否...

C#下winform和JS的互相调用和传参(webbrowser)

不多说,直接上代码,winform下button1调用js函数,从html页面获取值,然后JS调用WINFORM的函数,传送获取到的值到winform并通过messagebox的方法show出来。一步到位, winform调用JS函数 和J...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。