蜘蛛(Spider)是一种半自动的程序就象现实当中的蜘蛛在它的Web(蜘蛛网)上旅行一样蜘蛛程序也按照类似的方式在Web链接织成的网上旅行蜘蛛程序之所以是半自动的是因为它总是需要一个初始链接(出发点)但此后的运行情况就要由它自己决定了蜘蛛程序会扫描起始页面包含的链接然后访问这些链接指向的页面再分析和追蹤那些页面包含的链接从理论上看最终蜘蛛程序会访问到Internet上的每一个页面因为Internet上几乎每一个页面总是被其他或多或少的页面引用
本文介绍如何用C#语言构造一个蜘蛛程序它能够把整个网站的内容下载到某个指定的目录程序的运行界面如图一你可以方便地利用本文提供的几个核心类构造出自己的蜘蛛程序
)thisstylewidth=; border= twffan=done>
图
C#特别适合于构造蜘蛛程序这是因为它已经内置了HTTP访问和多线程的能力而这两种能力对于蜘蛛程序来说都是非常关键的下面是构造一个蜘蛛程序要解决的关键问题
⑴ HTML分析需要某种HTML解析器来分析蜘蛛程序遇到的每一个页面
⑵ 页面处理需要处理每一个下载得到的页面下载得到的内容可能要保存到磁盘或者进一步分析处理
⑶ 多线程只有拥有多线程能力蜘蛛程序才能真正做到高效
⑷ 确定何时完成不要小看这个问题确定任务是否已经完成并不简单尤其是在多线程环境下
一HTML解析
C#语言本身不包含解析HTML的能力但支持XML解析不过XML有着严格的语法为XML设计的解析器对HTML来说根本没用因为HTML的语法要宽松得多为此我们需要自己设计一个HTML解析器本文提供的解析器是高度独立的你可以方便地将它用于其它用C#处理HTML的场合
本文提供的HTML解析器由ParseHTML类实现使用非常方便首先创建该类的一个实例然后将它的Source属性设置为要解析的HTML文档
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
ParseHTML parse = new ParseHTML();parseSource = Hello World;
接下来就可以利用循环来检查HTML文档包含的所有文本和标记通常检查过程可以从一个测试Eof方法的while循环开始
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
while(!parseEof()){char ch = parseParse();
Parse方法将返回HTML文档包含的字符它返回的内容只包含那些非HTML标记的字符如果遇到了HTML标记Parse方法将返回值表示现在遇到了一个HTML标记遇到一个标记之后我们可以用GetTag()方法来处理它
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
if(ch==){HTMLTag tag = parseGetTag();}
一般地蜘蛛程序最重要的任务之一就是找出各个HREF属性这可以借助C#的索引功能完成例如下面的代码将提取出HREF属性的值(如果存在的话)
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
Attribute href = tag[HREF];string link = hrefValue;
获得Attribute对象之后通过AttributeValue可以得到该属性的值
二处理HTML页面
下面来看看如何处理HTML页面首先要做的当然是下载HTML页面这可以通过C#提供的HttpWebRequest类实现
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
HttpWebRequest request = (HttpWebRequest)WebRequestCreate(m_uri);response = requestGetResponse();stream = responseGetResponseStream();
接下来我们就从request创建一个stream流在执行其他处理之前我们要先确定该文件是二进制文件还是文本文件不同的文件类型处理方式也不同下面的代码确定该文件是否为二进制文件
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
if( !responseContentTypeToLower()StartsWith(text/) ){SaveBinaryFile(response);return null;}string buffer = line;
如果该文件不是文本文件我们将它作为二进制文件读入如果是文本文件首先从stream创建一个StreamReader然后将文本文件的内容一行一行加入缓沖区
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
reader = new StreamReader(stream);while( (line = readerReadLine())!=null ){buffer+=line+\r\n;}
装入整个文件之后接着就要把它保存为文本文件
SaveTextFile(buffer);
下面来看看这两类不同文件的存储方式
二进制文件的内容类型声明不以text/开头
蜘蛛程序直接把二进制文件保存到磁盘不必进行额外的处理这是因为二进制文件不包含HTML因此也不会再有需要蜘蛛程序处理的HTML链接下面是写入二进制文件的步骤
首先准备一个缓沖区临时地保存二进制文件的内容 byte []buffer = new byte[];
接下来要确定文件保存到本地的路径和名称如果要把一个网站的内容下载到本地的c:\test文件夹二进制文件的网上路径和名称是_//gif与此同时我们还要确保c:\test目录下已经创建了images子目录这部分任务由convertFilename方法完成
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
string filename = convertFilename( responseResponseUri );
convertFilename方法分离HTTP地址创建相应的目录结构确定了输出文件的名字和路径之后就可以打开读取Web页面的输入流写入本地文件的输出流
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
Stream outStream = FileCreate( filename );Stream inStream = responseGetResponseStream();
接下来就可以读取Web文件的内容并写入到本地文件这可以通过一个循环方便地完成
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
int l;do{l = inStreamRead(bufferbufferLength);if(l>)outStreamWrite(bufferl);} while(l>);
写入整个文件之后关闭输入流输出流
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
outStreamClose();inStreamClose();
比较而言下载文本文件更容易一些文本文件的内容类型总是以text/开头假设文件已被下载并保存到了一个字符串这个字符串可以用来分析网页包含的链接当然也可以保存为磁盘上的文件下面代码的任务就是保存文本文件
borderColorDark=#ffffff cellPadding=
width=
align=center borderColorLight=#
border=
>e
e
>
string filename = convertFilename( m_uri );StreamWriter outStream = new StreamWriter( filename );outStreamWrite(buffer);outStreamClose();
在这里我们首先打开一个文件输出流然后将缓沖区的内容写入流最后关闭文件
三多线程
多线程使得计算机看起来就象能够同时执行一个以上的操作不过除非计算机包含多个处理器否则所谓的同时执行多个操作仅仅是一种模拟出来的效果靠计算机在多个线程之间快速切换达到同时执行多个操作的效果一般而言只有在两种情况下多线程才能事实上提高程序运行的速度第一种情况是计算机拥有多个处理器第二种情况是程序经常要等待某个外部事件
对于蜘蛛程序来说第二种情况正是它的典型特征之一它每发出一个URL请求总是要等待文件下载完毕然后再请求下一个URL如果蜘蛛程序能够同时请求多个URL显然能够有效地减少总下载时间
为此我们用DocumentWorker类封装所有下载一个URL的操作每当一个DocumentWorker的实例被创建它就进入循环等待下一个要处理的URL下面是DocumentWorker的主循环
borderColorDark=#ffffff cellPadding= width= align=center borderColorLight=# border=>ee>
while(!m_spiderQuit ){m_uri = m_spiderObtainWork();m_spiderSpiderDoneWorkerBegin();string page = GetPage();if(page!=null)ProcessPage(page);m_spiderSpiderDoneWorkerEnd();}
这个循环将一直运行直至Quit标记被设置成了true(当用户点击Cancel按钮时Quit标记就被设置成true)在循环之内我们调用ObtainWork获取一个URLObtainWork将一直等待直到有一个URL可用这要由其他线程解析文档并寻找链接才能获得Done类利用WorkerBegin和WorkerEnd方法来确定何时整个下载操作已经完成
从图一可以看出蜘蛛程序允许用户自己确定要使用的线程数量在实践中线程的最佳数量受许多因素影响如果你的机器性能较高或者有两个处理器可以设置较多的线程数量
反之如果网络带宽机器性能有限设置太多的线程数量其实不一定能够提高性能
四任务完成了吗?
利用多个线程同时下载文件有效地提高了性能但也带来了线程管理方面的问题其中最复杂的一个问题是蜘蛛程序何时才算完成了工作?在这里我们要借助一个专用的类Done来判断
首先有必要说明一下完成工作的具体含义只有当系统中不存在等待下载的URL而且所有工作线程都已经结束其处理工作时蜘蛛程序的工作才算完成也就是说完成工作意味着已经没有等待下载和正在下载的URL
Done类提供了一个WaitDone方法它的功能是一直等待直到Done对象检测到蜘蛛程序已完成工作下面是WaitDone方法的代码
borderColorDark=#ffffff cellPadding= width= align=center borderColorLight=# border=>ee>
public void WaitDone(){MonitorEnter(this);while ( m_activeThreads> ){MonitorWait(this);}MonitorExit(this);}
WaitDone方法将一直等待直到不再有活动的线程但必须注意的是下载开始的最初阶段也没有任何活动的线程所以很容易造成蜘蛛程序一开始就立即停止的现象为解决这个问题我们还需要另一个方法WaitBegin来等待蜘蛛程序进入正式的工作阶段一般的调用次序是先调用WaitBegin再接着调用WaitDoneWaitDone将等待蜘蛛程序完成工作下面是WaitBegin的代码
borderColorDark=#ffffff cellPadding= width= align=center borderColorLight=# border=>ee>
public void WaitBegin(){MonitorEnter(this);while ( !m_started ){MonitorWait(this);}MonitorExit(this);}
WaitBegin方法将一直等待直到m_started标记被设置m_started标记是由WorkerBegin方法设置的工作线程在开始处理各个URL之时会调用WorkerBegin处理结束时调用WorkerEndWorkerBegin和WorkerEnd这两个方法帮助Done对象确定当前的工作状态下面是WorkerBegin方法的代码
borderColorDark=#ffffff cellPadding= width= align=center borderColorLight=# border=>ee>
public void WorkerBegin(){MonitorEnter(this);m_activeThreads++;m_started = true;MonitorPulse(this);MonitorExit(this);}
WorkerBegin方法首先增加当前活动线程的数量接着设置m_started标记最后调用Pulse方法以通知(可能存在的)等待工作线程启动的线程如前所述可能等待Done对象的方法是WaitBegin方法每处理完一个URLWorkerEnd方法会被调用
borderColorDark=#ffffff cellPadding= width= align=center borderColorLight=# border=>ee>
public void WorkerEnd(){MonitorEnter(this);m_activeThreads;MonitorPulse(this);MonitorExit(this);}
WorkerEnd方法减小m_activeThreads活动线程计数器调用Pulse释放可能在等待Done对象的线程如前所述可能在等待Done对象的方法是WaitDone方法