网络爬虫在信息检索与处理中有很大的作用,是收集网络信息的重要工具。
接下来就介绍一下爬虫的简单实现。
爬虫的工作流程如下

爬虫自指定的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 2 3 4 5 6 7 8 9 10 11 12 13 14 | private void DispatchWork()
{
if (_stop)
{
return ;
}
for ( int i = 0; i < _reqCount; i++)
{
if (!_reqsBusy[i])
{
RequestResource(i);
}
}
}
|
由于没有显式开新线程,所以用一个工作实例来表示一个逻辑工作线程
1 private bool[] _reqsBusy = null; //每个元素代表一个工作实例是否正在工作
2 private int _reqCount = 4; //工作实例的数量
每次一个工作实例完成工作,相应的_reqsBusy就设为false,并调用DispatchWork,那么DispatchWork就能给空闲的实例分配新任务了。
接下来是发送请求
1 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 | private void RequestResource( int index)
{
int depth;
string url = "" ;
try
{
lock (_locker)
{
if (_urlsUnload.Count <= 0)
{
_workingSignals.FinishWorking(index);
return ;
}
_reqsBusy[index] = true ;
_workingSignals.StartWorking(index);
depth = _urlsUnload.First().Value;
url = _urlsUnload.First().Key;
_urlsLoaded.Add(url, depth);
_urlsUnload.Remove(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的定义是
1 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 37 38 39 40 41 42 43 | class RequestState
{
private const int BUFFER_SIZE = 131072;
private byte [] _data = new byte [BUFFER_SIZE];
private StringBuilder _sb = new StringBuilder();
public HttpWebRequest Req { get ; private set ; }
public string Url { get ; private set ; }
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;
}
}
|
TimeoutCallback的定义是
1 2 3 4 5 6 7 8 9 10 11 12 13 | private void TimeoutCallback( object state, bool timedOut)
{
if (timedOut)
{
RequestState rs = state as RequestState;
if (rs != null )
{
rs.Req.Abort();
}
_reqsBusy[rs.Index] = false ;
DispatchWork();
}
}
|
接下来就是要处理请求的响应了
1 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 | private void ReceivedResource(IAsyncResult ar)
{
RequestState rs = (RequestState)ar.AsyncState;
HttpWebRequest req = rs.Req;
string url = rs.Url;
try
{
HttpWebResponse res = (HttpWebResponse)req.EndGetResponse(ar);
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.BufferSize,
new AsyncCallback(ReceivedData), rs);
}
else
{
res.Close();
rs.Req.Abort();
_reqsBusy[rs.Index] = false ;
DispatchWork();
}
}
catch (WebException we)
{
MessageBox.Show( "ReceivedResource " + we.Message + url + we.Status);
}
}
|
第19行这里采用了异步的方法来读数据流是因为我们之前采用了异步的方式请求,不然的话不能够正常的接收数据。
该异步读取的方式是按包来读取的,所以一旦接收到一个包就会调用传入的回调方法ReceivedData,然后在该方法中处理收到的数据。
该方法同时传入了接收数据的空间rs.Data和空间的大小rs.BufferSize。
接下来是接收数据和处理
1 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 37 38 39 40 41 42 43 | private void ReceivedData(IAsyncResult ar)
{
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;
try
{
read = resStream.EndRead(ar);
if (_stop)
{
rs.ResStream.Close();
req.Abort();
return ;
}
if (read > 0)
{
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.BufferSize,
new AsyncCallback(ReceivedData), rs);
return ;
}
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.Status);
}
}
|
第14行获得了读取的数据大小read,如果read>0说明数据可能还没有读完,所以在27行继续请求读下一个数据包;
如果read<=0说明所有数据已经接收完毕,这时rs.Html中存放了完整的HTML数据,就可以进行下一步的处理了。
第26行把这一次得到的字符串拼接在之前保存的字符串的后面,最后就能得到完整的HTML字符串。
然后说一下判断所有任务完成的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private void StartDownload()
{
_checkTimer = new Timer( new TimerCallback(CheckFinish), null , 0, 300);
DispatchWork();
}
private void CheckFinish( object param)
{
if (_workingSignals.IsFinished())
{
_checkTimer.Dispose();
_checkTimer = null ;
if (DownloadFinish != null && _ui != null )
{
_ui.Dispatcher.Invoke(DownloadFinish, _index);
}
}
}
|
第3行创建了一个定时器,每过300ms调用一次CheckFinish来判断是否完成任务。
第15行提供了一个完成任务时的事件,可以给客户程序注册。_index里存放了当前下载URL的个数。
该事件的定义是
1 2 3 4 5 6 7 8 9 | public delegate void DownloadFinishHandler( int count);
/// <summary>
/// 全部链接下载分析完毕后触发
/// </summary>
public event DownloadFinishHandler DownloadFinish = null ;
GJM :于 2016-11-16 转载自 http:
|