GJM:用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请求和响应的类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) //判断是否还有未下载的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;

GJM:用C#实现网络爬虫(一) [转载]的更多相关文章
- GJM:用C#实现网络爬虫(二) [转载]
上一篇<用C#实现网络爬虫(一)>我们实现了网络通信的部分,接下来继续讨论爬虫的实现 3. 保存页面文件 这一部分可简单可复杂,如果只要简单地把HTML代码全部保存下来的话,直接存文件就行 ...
- SHELL网络爬虫实例剖析--转载
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://nolinux.blog.51cto.com/4824967/1552472 前天 ...
- Python初学者之网络爬虫(二)
声明:本文内容和涉及到的代码仅限于个人学习,任何人不得作为商业用途.转载请附上此文章地址 本篇文章Python初学者之网络爬虫的继续,最新代码已提交到https://github.com/octans ...
- Atitit.数据检索与网络爬虫与数据采集的原理概论
Atitit.数据检索与网络爬虫与数据采集的原理概论 1. 信息检索1 1.1. <信息检索导论>((美)曼宁...)[简介_书评_在线阅读] - dangdang.html1 1.2. ...
- 网络爬虫(java)
陆陆续续做了有一个月,期间因为各种技术问题被多次暂停,最关键的一次主要是因为存储容器使用的普通二叉树,在节点权重相同的情况下导致树高增高,在进行遍历的时候效率大大降低,甚至在使用递归的时候导致栈 ...
- 关于网络爬虫项目的项目建议(NABCD)
Need 我们小组的研究课题是编写一个更快捷,更安全的爬虫软件,编写时会应用到学长的部分代码并在其基础上完善创新. 初步阅读了学长们的博客上面的几个版本的测试情况和源代码,发现学长们在实现基础功能的条 ...
- Atitit 网络爬虫与数据采集器的原理与实践attilax著 v2
Atitit 网络爬虫与数据采集器的原理与实践attilax著 v2 1. 数据采集1 1.1. http lib1 1.2. HTML Parsers,1 1.3. 第8章 web爬取199 1 2 ...
- 爬虫学习之基于Scrapy的网络爬虫
###概述 在上一篇文章<爬虫学习之一个简单的网络爬虫>中我们对爬虫的概念有了一个初步的认识,并且通过Python的一些第三方库很方便的提取了我们想要的内容,但是通常面对工作当作复杂的需求 ...
- python网络爬虫学习笔记
python网络爬虫学习笔记 By 钟桓 9月 4 2014 更新日期:9月 4 2014 文章文件夹 1. 介绍: 2. 从简单语句中開始: 3. 传送数据给server 4. HTTP头-描写叙述 ...
随机推荐
- Linux常用命令02
显示当前目录 pwd (print working directory) 显示当前目录 创建目录 mkdir (make directory) 创建目录(注意不是创建文 ...
- Ubuntu14中supervisor的安装及配置
supervisor是一款很好用的进程管理工具,其命令也很简单,其安装过程如下: Ubuntu14: 首先保证本地的Python环境是OK的,并且已经安装supervisor包,如果没有安装可以用ea ...
- ligerUI Tree 实例 代码
http://www.oschina.net/code/snippet_1762525_47819#68813
- python 反射的使用
反射这个功能在很多编程语言中都有,在Python中自然也不例外.其实编程语言中的很多功能都能用简单的代码来验证. 在code代码之前,先简单的了解下反射的几个属性. hasattr(obj,name_ ...
- JAVA--网络编程(UDP)
上午给大家简单介绍了一下TCP网络通信的知识,现在就为大家补充完整网络编程的知识,关于UDP的通信知识. UDP是一种不可靠的网络协议,那么还有什么使用价值或必要呢?其实不然,在有些情况下UDP协议可 ...
- 编译原理LL1文法分析表算法实现
import hjzgg.first.First; import hjzgg.follow.Follow; import hjzgg.tablenode.TableNode; import hjzgg ...
- poj 2195 Going Home
/* 做网络流的题建图真的是太重要了! 本题是将人所在的位置和房子所在的位置建立边的联系,其中man到house这一条边的流量为 1, 费用为两者的距离 而方向边的流量为 0, 费用为正向边的相反数( ...
- Microsoft Naive Bayes 算法——三国人物身份划分
Microsoft朴素贝叶斯是SSAS中最简单的算法,通常用作理解数据基本分组的起点.这类处理的一般特征就是分类.这个算法之所以称为“朴素”,是因为所有属性的重要性是一样的,没有谁比谁更高.贝叶斯之名 ...
- oc连接signalr,各种填坑
在网上搜了signalr的oc客户端,基本上都指向同一个东西https://github.com/DyKnow/SignalR-ObjC 但是这个也有日子没更新了,用cocoapods安装下来是编译不 ...
- Android基于mAppWidget实现手绘地图(六)–如何展示地图对象
为了展示选中的点,你需要完成以下步骤: 1.创建或者获得一个已经存在的图层 2.创建代表选中点的地图对象 3.把地图对象添加到图层 创建新图层 使用以下代码片段创建图层 int COFFEE_SHOP ...