网络爬虫在信息检索与处理中有很大的作用,是收集网络信息的重要工具。
接下来就介绍一下爬虫的简单实现。
爬虫的工作流程如下
爬虫自指定的 URL 地址开始下载网络资源,直到该地址和所有子地址的指定资源都下载完
毕为止。http://mmm.qqq23.com
下面开始逐步分析爬虫的实现。
1. 待下载集合与已下载集合
为了保存需要下载的 URL,同时防止重复下载,我们需要分别用了两个集合来存放将要下
载的 URL 和已经下载的 URL。
因为在保存 URL 的同时需要保存与 URL 相关的一些其他信息,如深度,所以这里我采用了
Dictionary 来存放这些 URL。
具体类型是 Dictionary 其中 string 是 Url 字符串,int 是该 Url 相对于基
URL 的深度。
每次开始时都检查未下载的集合,如果已经为空,说明已经下载完毕;如果还有 URL,那
么就取出第一个 URL 加入到已下载的集合中,并且下载这个 URL 的资源。
2. HTTP 请求和响应
C#已经有封装好的 HTTP 请求和响应的类 HttpWebRequest 和 HttpWebResponse,所
以实现起来方便不少。
为了提高下载的效率,http://www.qqq100.com 我们可以用多个请求并发的方式同时下
载多个 URL 的资源,一种简单的做法是采用异步请求的方法。
控制并发的数量可以用如下方法实现
if (_stop) //判断是否中止下载
{
}
for (int i = 0; i < _reqCount; i++)
{
if (!_reqsBusy[i]) //判断此编号的工作实例是否空闲
{
RequestResource(i); //让此工作实例请求资源
return;
1 private void DispatchWork()
2 {
3
4
5
6
7
8
9
10
11
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
4
5
6
int depth;
string url = "";
try
{
if (_urlsUnload.Count <= 0)
_workingSignals.FinishWorking(index);
_reqsBusy[index] = true;
_workingSignals.StartWorking(index);
depth = _urlsUnload.First().Value;
url = _urlsUnload.First().Key;
_urlsLoaded.Add(url, depth);
_urlsUnload.Remove(url);
{
}
{
}
return;
lock (_locker)
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//回调方法的参数
27
edResource), rs); //异步请求
28
andle, //注册超时处理方法
29
30
31
32
33
atus);
34
35 }
}
catch (WebException we)
{
}
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.Method = _method; //请求方法
req.Accept = _accept; //接受的内容
req.UserAgent = _userAgent; //用户代理
RequestState rs = new RequestState(req, url, depth, index);
var result = req.BeginGetResponse(new AsyncCallback(Receiv
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitH
TimeoutCallback, rs, _maxTime, true);
MessageBox.Show("RequestResource " + we.Message + url + we.St
第 26 行的请求的额外信息在异步请求的回调方法作为参数传入,之后还会提到。
第 27 行开始异步请求,这里需要传入一个回调方法作为响应请求时的处理,同时传入回调
方法的参数。
第 28 行给该异步请求注册一个超时处理方法 TimeoutCallback,最大等待时间是_maxT
ime,且只处理一次超时,并传入请求的额外信息作为回调方法的参数。
RequestState 的定义是
1 class RequestState
private const int BUFFER_SIZE = 131072; //接收数据包的空间大小
private byte[] _data = new byte[BUFFER_SIZE]; //接收数据包的 buffer
private StringBuilder _sb = new StringBuilder(); //存放所有接收到的
public HttpWebRequest Req { get; private set; } //请求
public string Url { get; private set; } //请求的 URL
public int Depth { get; private set; } //此次请求的相对深度
public int Index { get; private set; } //工作实例的编号
public Stream ResStream { get; set; } //接收数据流
public StringBuilder Html
{
public RequestState(HttpWebRequest req, string url, int depth,
}
2 {
3
4
5
字符
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int index)
37
38
39
40
41
42
43 }
}
}
{
}
{
}
{
}
{
}
get
return _sb;
public byte[] Data
{
get
return _data;
public int BufferSize
{
get
return BUFFER_SIZE;
Req = req;
Url = url;
Depth = depth;
Index = index;
TimeoutCallback 的定义是
if (timedOut) //判断是否是超时
{
RequestState rs = state as RequestState;
if (rs != null)
1 private void TimeoutCallback(object state, bool timedOut)
2 {
3
4
5
6
7
8
9
10
11
12
13 }
_reqsBusy[rs.Index] = false; //重置工作状态
DispatchWork(); //分配新任务
rs.Req.Abort(); //撤销请求
{
}
}
接下来就是要处理请求的响应了
RequestState rs = (RequestState)ar.AsyncState; //得到请求时传入的
HttpWebResponse res = (HttpWebResponse)req.EndGetResponse
HttpWebRequest req = rs.Req;
string url = rs.Url;
try
{
1 private void ReceivedResource(IAsyncResult ar)
2 {
3
参数
4
5
6
7
8
(ar); //获取响应
9
10
11
12
13
14
15
判断是否成功获取响应
16
17
18
if (_stop) //判断是否中止下载
{
res.Close();
req.Abort();
return;
{
}
if (res != null && res.StatusCode == HttpStatusCode.OK) //
Stream resStream = res.GetResponseStream(); //得到资源流
rs.ResStream = resStream;
var result = resStream.BeginRead(rs.Data, 0, rs.Buffer
new AsyncCallback(ReceivedData), rs);
else //响应失败
res.Close();
rs.Req.Abort();
{
}
19
Size, //异步请求读取数据
20
21
22
23
24
25
26
27
28
29
30
31
32
tatus);
33
}
34 }
}
}
catch (WebException we)
{
_reqsBusy[rs.Index] = false; //重置工作状态
DispatchWork(); //分配新任务
MessageBox.Show("ReceivedResource " + we.Message + url + we.S
第 19 行这里采用了异步的方法来读数据流是因为我们之前采用了异步的方式请求,不然的
话不能够正常的接收数据。
该异步读取的方式是按包来读取的,所以一旦接收到一个包就会调用传入的回调方法 Rece
ivedData,然后在该方法中处理收到的数据。
该方法同时传入了接收数据的空间 rs.Data 和空间的大小 rs.BufferSize。
接下来是接收数据和处理
RequestState rs = (RequestState)ar.AsyncState; //获取参数
HttpWebRequest req = rs.Req;
Stream resStream = rs.ResStream;
string url = rs.Url;
int depth = rs.Depth;
string html = null;
int index = rs.Index;
int read = 0;
1 private void ReceivedData(IAsyncResult ar)
2 {
3
4
5
6
7
8
9
10
11
12
13
14
15
16
read = resStream.EndRead(ar); //获得数据读取结果
if (_stop)//判断是否中止下载
try
{
{
rs.ResStream.Close();
req.Abort();
}
{
}
return;
if (read > 0)
17
18
19
20
21
22
23
/利用获得的数据创建内存流
24
25
26
27
Size, //再次异步请求读取数据
28
29
30
31
32
33
34
35
36
37
38
39
40
41
tatus);
42
}
43 }
return;
MemoryStream ms = new MemoryStream(rs.Data, 0, read); /
StreamReader reader = new StreamReader(ms, _encoding);
string str = reader.ReadToEnd(); //读取所有字符
rs.Html.Append(str); // 添加到之前的末尾
var result = resStream.BeginRead(rs.Data, 0, rs.Buffer
new AsyncCallback(ReceivedData), rs);
html = rs.Html.ToString();
SaveContents(html, url); //保存到本地
string[] links = GetLinks(html); //获取页面中的链接
AddUrls(links, depth + 1); //过滤链接并添加到未下载集合中
_reqsBusy[index] = false; //重置工作状态
DispatchWork(); //分配新任务
}
catch (WebException we)
{
MessageBox.Show("ReceivedData Web " + we.Message + url + we.S
第 14 行获得了读取的数据大小 read,如果 read>0 说明数据可能还没有读完,所以在 2
7 行继续请求读下一个数据包;
如果 read<=0 说明所有数据已经接收完毕,这时 rs.Html 中存放了完整的 HTML 数据,
就可以进行下一步的处理了。