爬虫自指定的URL地址开始下载网络资源,直到该地址和所有子地址的指定资源都下载完毕为止。
下面开始逐步分析爬虫的实现。
- 待下载集合与已下载集合
为了保存需要下载的URL,同时防止重复下载,我们需要分别用了两个集合来存放将要下载的URL和已经下载的URL。
因为在保存URL的同时需要保存与URL相关的一些其他信息,如深度,所以这里我采用了Dictionary来存放这些URL。
具体类型是Dictionary<string, int> 其中string是Url字符串,int是该Url相对于基URL的深度。
每次开始时都检查未下载的集合,如果已经为空,说明已经下载完毕;如果还有URL,那么就取出第一个URL加入到已下载的集合中,并且下载这个URL的资源。
- HTTP请求和响应
C#已经有封装好的HTTP请求和响应的类HttpWebRequest和HttpWebResponse,所以实现起来方便不少。
为了提高下载的效率,我们可以用多个请求并发的方式同时下载多个URL的资源,一种简单的做法是采用异步请求的方法。
控制并发的数量可以用如下方法实现
private void DispatchWork()
{
if (_stop) //判断是否中止下载
{
return;
}
for (int i = 0; i < _reqCount; i++)
{
if (!_reqsBusy[i]) //判断此编号的工作实例是否空闲
{
RequestResource(i); //让此工作实例请求资源
}
}
}
由于没有显式开新线程,所以用一个工作实例来表示一个逻辑工作线程
private bool[] _reqsBusy = null; //每个元素代表一个工作实例是否正在工作
private int _reqCount = 4; //工作实例的数量
每次一个工作实例完成工作,相应的_reqsBusy就设为false,并调用DispatchWork,那么DispatchWork就能给空闲的实例分配新任务了。
接下来是发送请求 每次一个工作实例完成工作,相应的_reqsBusy就设为false,并调用DispatchWork,那么DispatchWork就能给空闲的实例分配新任务了。
接下来是发送请求
private void RequestResource(int index)
{
int depth;
string url = "";
try
{
lock (_locker)
{
if (_urlsUnload.Count <= 0) //判断是否还有未下载的URL
{
_workingSignals.FinishWorking(index); //设置工作实例的状态为Finished
return;
}
_reqsBusy[index] = true;
_workingSignals.StartWorking(index); //设置工作状态为Working
depth = _urlsUnload.First().Value; //取出第一个未下载的URL
url = _urlsUnload.First().Key;
_urlsLoaded.Add(url, depth); //把该URL加入到已下载里
_urlsUnload.Remove(url); //把该URL从未下载中移除
}
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(ReceivedResource), rs); //异步请求
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, //注册超时处理方法
TimeoutCallback, rs, _maxTime, true);
}
catch (WebException we)
{
MessageBox.Show("RequestResource " + we.Message + url + we.Status);
}
}
private void RequestResource(int index)
{
int depth;
string url = "";
try
{
lock (_locker)
{
if (_urlsUnload.Count <= 0) //判断是否还有未下载的URL
{
_workingSignals.FinishWorking(index); //设置工作实例的状态为Finished
return;
}
_reqsBusy[index] = true;
_workingSignals.StartWorking(index); //设置工作状态为Working
depth = _urlsUnload.First().Value; //取出第一个未下载的URL
url = _urlsUnload.First().Key;
_urlsLoaded.Add(url, depth); //把该URL加入到已下载里
_urlsUnload.Remove(url); //把该URL从未下载中移除
}
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(ReceivedResource), rs); //异步请求
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, //注册超时处理方法
TimeoutCallback, rs, _maxTime, true);
}
catch (WebException we)
{
MessageBox.Show("RequestResource " + we.Message + url + we.Status);
}
}
第7行为了保证多个任务并发时的同步,加上了互斥锁。_locker是一个Object类型的成员变量。
第9行判断未下载集合是否为空,如果为空就把当前工作实例状态设为Finished;如果非空则设为Working并取出一个URL开始下载。当所有工作实例都为Finished的时候,说明下载已经完成。由于每次下载完一个URL后都调用DispatchWork,所以可能激活其他的Finished工作实例重新开始工作。
第26行的请求的额外信息在异步请求的回调方法作为参数传入,之后还会提到。
第27行开始异步请求,这里需要传入一个回调方法作为响应请求时的处理,同时传入回调方法的参数。
第28行给该异步请求注册一个超时处理方法TimeoutCallback,最大等待时间是_maxTime,且只处理一次超时,并传入请求的额外信息作为回调方法的参数。
RequestState的定义是
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
{
get
{
return _sb;
}
}
public byte[] Data
{
get
{
return _data;
}
}
public int BufferSize
{
get
{
return BUFFER_SIZE;
}
}
public RequestState(HttpWebRequest req, string url, int depth, int index)
{
Req = req;
Url = url;
Depth = depth;
Index = index;
}
}