创建智能网络蜘蛛
——如何使用Java网络对象和HTML对象(翻译)
作者:Mark O. Pendergast
原文:http://www.javaworld.com/javaworld/jw-11-2004/jw-1101-spider.html
摘要
你是否想过创建自己的符合特定标准的网站数据库呢?网络蜘蛛,有时也称为网络爬虫,是一些根据网络链接从一个网站到另外一个网站,检查内容和记录位置的程序。商业搜索站点使用网络蜘蛛丰富它们的数据库,研究人员可以使用蜘蛛获得相关的信息。创建自己的蜘蛛搜索的内容、主机和网页特征,比如文字密度和内置的多媒体内容。这篇文章将告诉你如何使用Java的HTML和网络类来创建你自己的功能强大的网络蜘蛛。
这篇文章将介绍如何在标准Java网络对象的基础上创建一个智能的网络蜘蛛。蜘蛛的核心是一个基于关键字/短语标准和网页特征进行深入网络搜索的递归程序。搜索过程在图形上类似于JTree结构。我主要介绍的问题,例如处理相关的URL,防止循环引用和监视内存/堆栈使用。另外,我将介绍再访问和分解远程网页中如何正确是用Java网络对象。
l 蜘蛛示例程序
示例程序包括用户界面类SpiderControl、网络搜索类Spider,两个用作创建JTree显示结果的类UrlTreeNode和UrlNodeRenderer,和两个帮助验证用户界面中数字输入的类IntegerVerifier和VerifierListener。文章末尾的资源中有完整代码和文档的琏接。
SpiderControl界面由三个属性页组成,一个用来设置搜索参数,另一个显示结果搜索树(JTree),第三个显示错误和状态信息,如图1
图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!nn");
}
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.
String protocol = url.getProtocol(); // Ask the URL for its protocol
if(!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("file"))
{
messageArea.append(" Skipping : "+urlstr+" not a http sitenn");
return;
}
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
}
if(!isDomainOk(url))
{
messageArea.append(" Skipping : "+urlstr+" not in domain listnn");
return;
}
这里,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.Tag.IMG))
{
node.addImages(1);
return;
}
if(t.equals(HTML.Tag.BASE))
{
Object value = a.getAttribute(HTML.Attribute.HREF);
if(value != null)
node.setBase(fixHref(value.toString()));
}
}
/**
* Take care of start tags
* @param t HTML tag
* @param a HTML attributes
* @param pos Position within file
*/
public void handleStartTag(HTML.Tag t,
MutableAttributeSet a,
int pos)
{
if(t.equals(HTML.Tag.TITLE))
{
lastText="";
return;
}
if(t.equals(HTML.Tag.A))
{
Object value = a.getAttribute(HTML.Attribute.HREF);
if(value != null)
{
node.addLinks(1);
String href = value.toString();
href = fixHref(href);
try{
URL referencedURL = new URL(node.getBase(),href);
searchWeb(treenode, referencedURL.getProtocol()+"://"+referencedURL.getHost()+referencedURL.getPath());
}
catch (MalformedURLException e)
{
messageArea.append(" Bad URL encountered : "+href+"nn");
return;
}
}
}
}
/**
* Take care of start tags
* @param t HTML tag
* @param pos Position within file
*/
public void handleEndTag(HTML.Tag t,
int pos)
{
if(t.equals(HTML.Tag.TITLE) && lastText != null)
{
node.setTitle(lastText.trim());
DefaultTreeModel tm = (DefaultTreeModel)searchTree.getModel();
tm.nodeChanged(treenode);
}
}
/**
* Take care of text between tags, check against keyword list for matches, if
* match found, set the node match status to true
* @param data Text between tags
* @param pos position of text within Webpage
*/
public void handleText(char[] data, int pos)
{
lastText = new String(data);
node.addChars(lastText.length());
String text = lastText.toUpperCase();
for(int i = 0; i < keywordList.length; i++)
{
if(text.indexOf(keywordList[i]) >= 0)
{
if(!node.isMatch())
{
sitesFound++;
updateStats();
}
node.setMatch(keywordList[i]);
return;
}
}
}
}
l 处理和补全URL
当遇到相关页面的链接,你必须在它们基础URL上创建完整的链接。基础URL可能通过BASE标签在页面中明确的定义,或者暗含在当前页面的链接中。Java的URL对象为你解决这个问题提供了构造器,提供了根据它的链接结构创建相似的。
URL(URL context, String spec)接受spec参数的链接和context参数的基础链接。如果spec是一个相关链接,构建器将使用context来创建一个完整引用的URL对象。URL它推荐URL遵循严格的(Unix)格式。使用反斜线,在Microsoft Windows中,而不是斜线,将是错误的引用。如果spec或者context指向一个目录(包含index.html或default.html),而不是一个HTML文件,它必须有一个结束斜线。fixHref()方法检查这些引用并且修正它们:
public static String fixHref(String href)
{
String newhref = href.replace('\', '/'); // Fix sloppy Web references
int lastdot = newhref.lastIndexOf('.');
int lastslash = newhref.lastIndexOf('/');
if(lastslash > lastdot)
{
if(newhref.charAt(newhref.length()-1) != '/')
newhref = newhref+"/"; // Add missing /
}
return newhref;
}
l 控制递归
searchWeb()开始是为了搜索用户指定的起始Web地址而被调用的。它接着在遇到HTML链接时调用自身。这形成了深度优先搜索的基础,也带来了两种问题。首先非常危险的内存/堆栈溢出问题将因为太多的递归调用而产生。如果出现环形的引用,这个问题就将发生,也就是说,一个页面链接另外一个链接回来的连接,这是WWW中常见的事情。为了预防这种现象,searchWeb()检查搜索树(通过urlHasBeenVisited()方法)来确定是否引用的页面已经存在。如果已经存在,这个链接将被忽略。如果你选择实现一个没有搜索树的蜘蛛,你仍然必须维护一个以访问站点的列表(在Vector或数组中)以便你可以判断是否你正在重复访问站点。
递归的第二个问题来自深度优先的搜索和WWW的结构。根据选择的入口,深度优先的搜索在初始页面的初始链接在完成处理以前造成大量的递归调用。这就造成了两种不需要的结果:首先内存/堆栈溢出可能发生,第二被搜索过的页面可能很久才被从初始入口众多的结果中删除。为了控制这些,我为蜘蛛添加了最大搜索深度设置。用户可以选择可以达到的深度等级(链接到链接到链接),当遇到每个链接时,当前深度通过调用depthLimitExceeded()方法进行检查。如果达到限制,链接就被忽略。测试仅仅检查JTree中节点的级别。
示例程序也增加了站点限制,用户来指定,可以在特定数目的URL被检查以后停止搜索,这样确保程序可以最后停止!站点限制通过一个简单的数字计数器sitesSearched来控制,这个数字每次调用searchWeb()后都被更新和检查。
l UrlTreeNode和UrlNodeRenderer
UrlTreeNode和UrlNodeRenderer是用来在SpiderControl用户界面中创建JTree中个性化的树节点的类。UrlTreeNode包含每个搜索过的站点钟的URL信息和统计数据。UrlTreeNode以作为用户对象属性的标准DefaultMutableTreeNode对象形式存储在JTree中。数据包括节点中跟踪关键字出现的能力,节点的URL,节点的基础URL,链接的数量,图片的数量和字符的个数,以及节点是否符合搜索规则。
UrlTreeNodeRenderer是DefaultTreeCellRenderer界面的实现。UrlTreeNodeRenderer使节点包含匹配关键字显示为蓝色。UrlTreeNodeRenderer也为JtreeNodes加入了个性化的图标。个性化的显示通过覆盖getTreeCellRendererComponent()方法(如下)实现。这个方法在树中创建了一个Component对象。大部分的Component属性通过子类来进行设置,UrlTreeNodeRenderer改变了文字的颜色(前景色)和图标:
public Component getTreeCellRendererComponent(
JTree tree,
Object value,
boolean sel,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
super.getTreeCellRendererComponent(
tree, value, sel,
expanded, leaf, row,
hasFocus);
UrlTreeNode node = (UrlTreeNode)(((DefaultMutableTreeNode)value).getUserObject());
if (node.isMatch()) // Set color
setForeground(Color.blue);
else
setForeground(Color.black);
if(icon != null) // Set a custom icon
{
setOpenIcon(icon);
setClosedIcon(icon);
setLeafIcon(icon);
}
return this;
}
l 总结
这篇文章向你展示了如何创建网络蜘蛛和控制它的用户界面。用户界面使用JTree来跟踪蜘蛛的进展和记录访问过的站点。当然,你也可以使用Vector来记录访问过的站点和使用一个简单的计数器来显示进展。其他增强可以包含通过数据库记录关键字和站点的接口,增加通过多个入口搜索的能力,用大量或者很少的文字内容来显现站点,以及为搜索引擎提供同义搜索的能力。
这篇文章中展示的Spider类使用递归调用搜索程序,当然,一个新蜘蛛的独立线程可以在遇到每个链接时开始。这样的好处是允许链接远程URL并发执行,提高速度。然而记住那些叫做DefaultMutableTreeNode的JTree对象,不是线程安全的,所以程序员必须自己实现同步。
资源:
该文章的源代码和Java文档:
[url=http://www.javaworld.com/javaworld/jw-11-2004/spider/jw-1101-spider.zip]http://www.javaworld.com/javaworld/jw-11-2004/spider/jw-1101-spider.zip