用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;

用C#实现网络爬虫(一)的更多相关文章
- Python初学者之网络爬虫(二)
声明:本文内容和涉及到的代码仅限于个人学习,任何人不得作为商业用途.转载请附上此文章地址 本篇文章Python初学者之网络爬虫的继续,最新代码已提交到https://github.com/octans ...
- 网络爬虫:使用Scrapy框架编写一个抓取书籍信息的爬虫服务
上周学习了BeautifulSoup的基础知识并用它完成了一个网络爬虫( 使用Beautiful Soup编写一个爬虫 系列随笔汇总 ), BeautifulSoup是一个非常流行的Python网 ...
- 网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(3): 抓取amazon.com价格
通过上一篇随笔的处理,我们已经拿到了书的书名和ISBN码.(网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(2): 抓取allitebooks.com书籍信息 ...
- 网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(2): 抓取allitebooks.com书籍信息及ISBN码
这一篇首先从allitebooks.com里抓取书籍列表的书籍信息和每本书对应的ISBN码. 一.分析需求和网站结构 allitebooks.com这个网站的结构很简单,分页+书籍列表+书籍详情页. ...
- 网络爬虫: 从allitebooks.com抓取书籍信息并从amazon.com抓取价格(1): 基础知识Beautiful Soup
开始学习网络数据挖掘方面的知识,首先从Beautiful Soup入手(Beautiful Soup是一个Python库,功能是从HTML和XML中解析数据),打算以三篇博文纪录学习Beautiful ...
- Atitit.数据检索与网络爬虫与数据采集的原理概论
Atitit.数据检索与网络爬虫与数据采集的原理概论 1. 信息检索1 1.1. <信息检索导论>((美)曼宁...)[简介_书评_在线阅读] - dangdang.html1 1.2. ...
- Java 网络爬虫获取页面源代码
原博文:http://www.cnblogs.com/xudong-bupt/archive/2013/03/20/2971893.html 1.网络爬虫是一个自动提取网页的程序,它为搜索引擎从万维网 ...
- [Search Engine] 搜索引擎技术之网络爬虫
随着互联网的大力发展,互联网称为信息的主要载体,而如何在互联网中搜集信息是互联网领域面临的一大挑战.网络爬虫技术是什么?其实网络爬虫技术就是指的网络数据的抓取,因为在网络中抓取数据是具有关联性的抓取, ...
- [Python] 网络爬虫和正则表达式学习总结
以前在学校做科研都是直接利用网上共享的一些数据,就像我们经常说的dataset.beachmark等等.但是,对于实际的工业需求来说,爬取网络的数据是必须的并且是首要的.最近在国内一家互联网公司实习, ...
- 【Python网络爬虫一】爬虫原理和URL基本构成
1.爬虫定义 网络爬虫,即Web Spider,是一个很形象的名字.把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛.网络蜘蛛是通过网页的链接地址来寻找网页的.从网站某一个页面(通常 ...
随机推荐
- java解析页面包jsoup
http://www.open-open.com/jsoup/parsing-a-document.htm jsoup: Java HTML Parser jsoup is a Java librar ...
- Xamarin开发教程如何使用Xamarin开发Android应用
Xamarin开发教程如何使用Xamarin开发Android应用 如何使用Xamarin开发Android应用 在了解了Xamarin和Andriod系统之后,下面我们需要了解一下如何使用这些工具和 ...
- 删除表中多余的重复记录(多个字段),只留有rowid最小的记录
假如表Users,其中ID为自增长. ID,Name,Sex 1 张三,男 2 张三,男 3 李四,女 4 李四,女 5 王五,男 --查找出最小行号ID的重复记录 select Name,Sex,C ...
- Android中为图标加上数字--用于未读短信数提醒,待更新应用数提醒等
本文属于原创,转载请著名出处:http://flysnow.iteye.com/blog/906770 写道 在我们开发一些如短消息.应用商店等应用时,会考虑在短消息的图标上加上未读短信的数量,在应用 ...
- Android 使用加速度传感器实现摇一摇功能及优化
如有转载,请声明出处: 时之沙: http://blog.csdn.net/t12x3456 目前很多应用已经实现了摇一摇功能,这里通过讲解该功能的原理及实现回顾一下加速度传感器的使用: 1.首先获得 ...
- Java的容器小结
1. 各个类与接口的关系:
- iOS 10 个实用小技巧(总有你不知道的和你会用到的)
在开发过程中我们总会遇到各种各样的小问题,有些小问题并不是十分容易解决.在此我就总结一下,我在开发中遇到的各种小问题,以及我的解决方法.比较普遍的我就不再提了,这里主要讲一些你可能不知道的(当然,也有 ...
- git 创建远程仓库
在远程服务器上$ cd /server/path/ $ git init --bare myproject.git 在本地 1> $ cd /client/path/ 运行 git init 2 ...
- 【原创】贴片电容的测量方法。。。这是我从自己QQ空间转过来的,本人实操!
电容不工作一般分为3种情况,漏电.击穿.无电容.一般检测用万用表检测阻值一般调在10K-20K为测量标准,特别是贴片电容.把万用表的笔尖点在贴片电容的两侧,如下图测量: l1.jpg l2.jpg l ...
- python手机号码运营商归属测试
#手机号码测试: def number_test(): while True: number = input('Please enter your phone number:') CN_mobile ...