创建智能网络蜘蛛
——如何使用Java网络对象和HTML对象(翻译)
作者:Mark O. Pendergast
原文:http://www.javaworld.com/javaworld/jw-11-2004/jw-1101-spider.html
<!----><o:p></o:p>
摘要
你是否想过创建自己的符合特定标准的网站数据库呢?网络蜘蛛,有时也称为网络爬虫,是一些根据网络链接从一个网站到另外一个网站,检查内容和记录位置的程序。商业搜索站点使用网络蜘蛛丰富它们的数据库,研究人员可以使用蜘蛛获得相关的信息。创建自己的蜘蛛搜索的内容、主机和网页特征,比如文字密度和内置的多媒体内容。这篇文章将告诉你如何使用Java的HTML和网络类来创建你自己的功能强大的网络蜘蛛。
<o:p></o:p>
这篇文章将介绍如何在标准Java网络对象的基础上创建一个智能的网络蜘蛛。蜘蛛的核心是一个基于关键字/短语标准和网页特征进行深入网络搜索的递归程序。搜索过程在图形上类似于JTree结构。我主要介绍的问题,例如处理相关的URL,防止循环引用和监视内存/堆栈使用。另外,我将介绍再访问和分解远程网页中如何正确是用Java网络对象。
<o:p></o:p>
l 蜘蛛示例程序
示例程序包括用户界面类SpiderControl、网络搜索类Spider,两个用作创建JTree显示结果的类UrlTreeNode和UrlNodeRenderer,和两个帮助验证用户界面中数字输入的类IntegerVerifier和VerifierListener。文章末尾的资源中有完整代码和文档的琏接。
SpiderControl界面由三个属性页组成,一个用来设置搜索参数,另一个显示结果搜索树(JTree),第三个显示错误和状态信息,如图1
<!----><v:shapetype o:spt="75" coordsize="21600,21600" filled="f" stroked="f" id="_x0000_t75" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:extrusionok="f" o:connecttype="rect" gradientshapeok="t"></v:path><o:lock v:ext="edit" aspectratio="t"></o:lock></v:shapetype>
图1 搜索参数属性页
搜索参数包括访问网站的最大数量,搜索的最大深度(链接到链接到链接),关键字/短语列表,搜索的顶级主机,起始网站或者门户。一旦用户输入了搜索参数,并按下开始按钮,网络搜索将开始,第二个属性页将显示搜索的进度。
图2 搜索树
一个Spider类的实例以独立进程的方式执行网络搜索。独立进程的使用是为了SpiderControl模块可以不断更新搜索树显示和处理停止搜索按钮。当Spider运行时,它不断在第二个属性页中为JTree增加节点(UrlTreeNode)。包含关键字和短语的搜索树节点以蓝色显示(UrlNodeRenderer)。
当搜索完成以后,用户可以查看站点的统计,还可以用外部浏览器(默认是位于Program Files目录的Internet Explorer)查看站点。统计包括关键字出现次数,总字符数,总图片数和总链接数。
l Spider类
Spider类负责搜索给出起点(入口)的网络,一系列的关键字和主机,和搜索深度和大小的限制。Spider继承了Thread,所以可以以独立线程运行。这允许SpiderControl模块不断更新搜索树显示和处理停止搜索按钮。
构造方法接受包含对一个空的JTree和一个空的JtextArea引用的搜索参数。JTree被用作创建一个搜索过程中的分类站点记录。这样为用户提供了可见的反馈,帮助跟踪Spdier循环搜索的位置。JtextArea显示错误和过程信息。
构造器将参数存放在类变量中,使用UrlNodeRenderer类初始化显示节点的JTree。直到SpiderControl调用run()方法搜索才开始。
run()方法以独立的线程开始执行。它首先判断入口站点是否是一个Web引用(以http,ftp或者www开始)或是一个本地文件引用。它接着确认入口站点是否具有正确的符号,重置运行统计,接着调用searchWeb()开始搜索:
public void run()
{
DefaultTreeModel treeModel = (DefaultTreeModel)searchTree.getModel(); // get our model
DefaultMutableTreeNode root = (DefaultMutableTreeNode)treeModel.getRoot();
String urllc = startSite.toLowerCase();
if(!urllc.startsWith("http://") && !urllc.startsWith("ftp://") &&
!urllc.startsWith("www."))
{
startSite = "file:///"+startSite; // Note you must have 3 slashes !
}
else // Http missing ?
if(urllc.startsWith("www."))
{
startSite = "http://"+startSite; // Tack on http://
}
startSite = startSite.replace('\\', '/'); // Fix bad slashes
sitesFound = 0;
sitesSearched = 0;
updateStats();
searchWeb(root,startSite); // Search the Web
messageArea.append("Done!\n\n");
}
searchWeb()是一个接受搜索树父节点和搜索Web地址参数的递归方法。searchWeb()首先检查给出的站点是否已被访问和未被执行的搜索深度和站点。SearchWeb()接着允许SpiderControl运行(更新界面和检查停止搜索按钮是否按下)。如果所有正常,searchWeb()继续,否则返回。
在searchWeb()开始读和解析站点以前,它首先检验基于站点创建的URL对象是否具有正确的类型和主机。URL协议被检查来确认它是一个HTML地址或者一个文件地址(不必搜索mailto:和其他协议)。接着检查文件扩展名(如果当前有)来确认它是一个HTML文件(不必解析pdf或者gif文件)。一旦这些工作完成,通过isDomainOk()方法检查根据用户指定的列表检查主机:
...URL url = new URL(urlstr); // Create the URL object from a string.
<o:p></o:p>
String protocol = url.getProtocol(); // Ask the URL for its protocol
if(!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("file"))
{
messageArea.append(" Skipping : "+urlstr+" not a http site\n\n");
return;
}
<o:p></o:p>
String path = url.getPath(); // Ask the URL for its path
int lastdot = path.lastIndexOf("."); // Check for file extension
if(lastdot > 0)
{
String extension = path.substring(lastdot); // Just the file extension
if(!extension.equalsIgnoreCase(".html") && !extension.equalsIgnoreCase(".htm"))
return; // Skip everything but html files
}
<o:p></o:p>
if(!isDomainOk(url))
{
messageArea.append(" Skipping : "+urlstr+" not in domain list\n\n");
return;
}
<o:p></o:p>
这里,searchWeb()公平的确定它是否有值得搜索的URL,接着它为搜索树创建一个新节点,添加到树中,打开一个输入流解析文件。下面的章节涉及很多关于解析HTML文件,处理相关URL和控制递归的细节。
l 解析HTML文件
这里有两个为了查找A HREF来解析HTML文件方法——一个麻烦的方法和一个简单的方法。
如果你选择麻烦的方法,你将使用Java的StreamTokenizer类创建你自己的解析规则。使用这些技术,你必须为StreamTokenizer对象指定单词和空格,接着去掉<和>符号来查找标签,属性,在标签之间分割文字。太多的工作要做。
简单的方法是使用内置的ParserDelegator类,一个HTMLEditorKit.Parser抽象类的子类。这些类在Java文档中没有完善的文档。使用ParserDelegator有三个步骤:首先为你的URL创建一个InputStreamReader对象,接着创建一个ParserCallback对象的实例,最后创建一个ParserDelegator对象的实例并调用它的public方法parse():
UrlTreeNode newnode = new UrlTreeNode(url); // Create the data node
InputStream in = url.openStream(); // Ask the URL object to create an input stream
InputStreamReader isr = new InputStreamReader(in); // Convert the stream to a reader
DefaultMutableTreeNode treenode = addNode(parentnode, newnode);
SpiderParserCallback cb = new SpiderParserCallback(treenode); // Create a callback object
ParserDelegator pd = new ParserDelegator(); // Create the delegator
pd.parse(isr,cb,true); // Parse the stream
isr.close(); // Close the stream
parse()接受一个InputStreamReader,一个ParseCallback对象实例和一个指定CharSet标签是否忽略的标志。parse()方法接着读和解码HTML文件,每次完成解码一个标签或者HTML元素后调用ParserCallback对象的方法。
在示例代码中,我实现了ParserCallback作为Spider的一个内部类,这样就允许ParseCallback访问Spider的方法和属性。基于ParserCallback的类可以覆盖下面的方法:
n handleStartTag():当遇到起始HTML标签时调用,比如>A <
n handleEndTag():当遇到结束HTML标签时调用,比如>/A<
n handleSimpleTag():当遇到没有匹配结束标签时调用
n handleText():当遇到标签之间的文字时调用
在示例代码中,我覆盖了handleSimpleTag()以便我的代码可以处理HTML的BASE和IMG标签。BASE标签告诉当处理相关的URL引用时使用什么URL。如果没有BASE标签出现,那么当前URL就用来处理相关的引用。HandleSimpleTag()接受三个参数,一个HTML.Tag对象,一个包含所有标签属性的MutableAttributeSet,和在文件中的相应位置。我的代码检查标签来判断它是否是一个BASE对象实例,如果是则HREF属性被提取出来并保存在页面的数据节点中。这个属性以后在处理链接站点的URL地址中被用到。每次遇到IMG标签,页面图片数就被更新。
我覆盖了handleStartTag以便程序可以处理HTML的A和TITLE标签。方法检查t参数是否是一个事实上的A标签,如果是则HREF属性将被提取出来。
fixHref()被用作清理大量的引用(改变反斜线为斜线,添加缺少的结束斜线),链接的URL通过使用基础URL和引用创建URL对象来处理。接着递归调用searchWeb()来处理链接。如果方法遇到TITLE标签,它就清除存储最后遇到文字的变量以便标题的结束标记具有正确的值(有时网页的title标签之间没有标题)。
我覆盖了handleEndTag()以便HTML的TITLE结束标记可以被处理。这个结束标记指出前面的文字(存在lastText中)是页面的标题文字。这个文字接着存在页面的数据节点中。因为添加标题信息到数据节点中将改变树中数据节点的显示,nodeChanged()方法必须被调用以便树可以更新。
我覆盖了handleText()方法以便HTML页面的文字可以根据被搜索的任意关键字或者短语来检查。HandleText()接受一个包含一个子符数组和该字符在文件中位置作为参数。HandleText()首先将字符数组转换成一个String对象,在这个过程中全部转换为大写。接着在搜索列表中的每个关键字/短语根据String对象的indexof()方法来检查。如果indexof()返回一个非负结果,则关键字/短语在页面的文字中显示。如果关键字/短语被显示,匹配被记录在匹配列表的节点中,统计数据被更新:
public class SpiderParserCallback extends HTMLEditorKit.ParserCallback {
/**
* Inner class used to html handle parser callbacks
*/
public class SpiderParserCallback extends HTMLEditorKit.ParserCallback {
/** URL node being parsed */
private UrlTreeNode node;
/** Tree node */
private DefaultMutableTreeNode treenode;
/** Contents of last text element */
private String lastText = "";
/**
* Creates a new instance of SpiderParserCallback
* @param atreenode search tree node that is being parsed
*/
public SpiderParserCallback(DefaultMutableTreeNode atreenode) {
treenode = atreenode;
node = (UrlTreeNode)treenode.getUserObject();
}
/**
* Handle HTML tags that don't have a start and end tag
* @param t HTML tag
* @param a HTML attributes
* @param pos Position within file
*/
public void handleSimpleTag(HTML.Tag t,
MutableAttributeSet a,
int pos)
{
if(t.equals(HTML