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 :于 2016-11-16 转载自 http://www.cnblogs.com/Jiajun/archive/2012/06/17/2552458.html   如影响作者版权问题 请联系我 [email protected]

时间: 2024-07-29 03:50:30

GJM:用C#实现网络爬虫(二)的相关文章

零基础自学Python 3开发网络爬虫(二): 用到的数据结构简介以及爬虫Ver1.0 alpha

上一回, 我学会了 用伪代码写出爬虫的主要框架; 用Python的urllib.request库抓取指定url的页面; 用Python的urllib.parse库对普通字符串转符合url的字符串. 这一回, 开始用Python将伪代码中的所有部分实现. 由于文章的标题就是"零基础", 因此会先把用到的两种数据结构队列和集合介绍一下. 而对于"正则表达式"部分, 限于篇幅不能介绍, 但给出我比较喜欢的几个参考资料. Python的队列 在爬虫程序中, 用到了广度优先搜

Python3网络爬虫——二、Urllib库的基本使用

一.什么是Urllib Urllib库是Python自带的一个http请求库,包含以下几个模块: urllib.request 请求模块 urllib.error   异常处理模块 urllib.parse   url解析模块 urllib.robotparser  robots.txt解析模块 其中前三个模块比较常用,第四个仅作了解. 二.Urllib方法介绍 将结合Urllib的官方文档进行说明.首先是urllib.request模块: urllib.request.urlopen(url,

Python3网络爬虫(二):利用urllib.urlopen向有道翻译发送数据获得翻译结果

一.urlopen的url参数 Agent url不仅可以是一个字符串,例如:http://www.baidu.com.url也可以是一个Request对象,这就需要我们先定义一个Request对象,然后将这个Request对象作为urlopen的参数使用,方法如下: # -*- coding: UTF-8 -*- from urllib import request if __name__ == "__main__": req = request.Request("http

【Python网络爬虫二】使用urllib2抓去网页内容

在Python中通过导入urllib2组件,来完成网页的抓取工作.在python3.x中被改为urllib.request. 爬取具体的过程类似于使用程序模拟IE浏览器的功能,把URL作为HTTP请求的内容发送到服务器端, 然后读取服务器端的响应资源. 实现过程: 1 import urllib2 2 3 response=urllib2.urlopen('http://gs.ccnu.edu.cn/') 4 html=response.read() 5 print html 将返回的html信

Python 3开发网络爬虫(四): 登录

原文出处: Jecvay Notes (@Jecvay) <零基础自学用Python 3开发网络爬虫(一)> <零基础自学用Python 3开发网络爬虫(二)> <零基础自学用Python 3开发网络爬虫(三)> 今天的工作很有意思, 我们用 Python 来登录网站, 用Cookies记录登录信息, 然后就可以抓取登录之后才能看到的信息. 今天我们拿知乎网来做示范. 为什么是知乎? 这个很难解释, 但是肯定的是知乎这么大这么成功的网站完全不用我来帮他打广告. 知乎网的

Python3网络爬虫(七):使用Beautiful Soup爬取小说

转载请注明作者和出处:http://blog.csdn.net/c406495762 运行平台: Windows Python版本: Python3.x IDE: Sublime text3 一.Beautiful Soup简介 简单来说,Beautiful Soup是python的一个库,最主要的功能是从网页抓取数据.官方解释如下: Beautiful Soup提供一些简单的.python式的函数用来处理导航.搜索.修改分析树等功能.它是一个工具箱,通过解析文档为用户提供需要抓取的数据,因为简

GJM:用C#实现网络爬虫(二) [转载]

上一篇<用C#实现网络爬虫(一)>我们实现了网络通信的部分,接下来继续讨论爬虫的实现 3. 保存页面文件 这一部分可简单可复杂,如果只要简单地把HTML代码全部保存下来的话,直接存文件就行了. 1 private void SaveContents(string html, string url) 2 { 3 if (string.IsNullOrEmpty(html)) //判断html字符串是否有效 4 { 5 return; 6 } 7 string path = string.Form

python网络爬虫入门(二)——用python简单实现调用谷歌翻译

最近在看国外的文档,有些生词不认识.就用谷歌翻译来理解,用着用着闲来无事就按F12查看了下页面的源代码.发现可以用python简单的实现下谷歌翻译的页面功能.于是先上网搜下有没有类似的文章博客,发现几篇不错的,于是参考其他代码与自己的思路,简单的实现了下翻译的功能,代码如下: import re import urllib,urllib2 #----------模拟浏览器的行为,向谷歌翻译发送数据,然后抓取翻译结果,这就是大概的思路------- def Gtranslate(text): #t

Golang 网络爬虫框架gocolly/colly 二 jQuery selector

Golang 网络爬虫框架gocolly/colly 二 jQuery selector colly框架依赖goquery库,goquery将jQuery的语法和特性引入到了go语言中.如果要灵活自如地采集数据,首先要了解jQuery选择器.以下内容是翻译jQuery官方网站 <Category: Selectors>章节内容: Category: Selectors Borrowing from CSS 1–3, and then adding its own, jQuery offers