HTMLParser 是一个用来解析 HTML 文档的开放源码项目,它具有小巧、快速、使用简单的特点以及拥有强大的功能。对该项目还不了解的朋友可以参照 2004 年三月份我发表的文章--《从HTML中攫取你所需的信息》,这篇文章介绍如何通过 HTMLParser 来提取 HTML 文档中的文本数据以及提取出文档中的所有链接或者是图片等信息。
现在该项目的最新版本是 Integration Build 1.6,与之前版本的差别在于代码结构的调整、当然也有一些功能的提升以及 BugFix,同时对字符集的处理也更加自动了。比较遗憾的该项目并没有详尽的使用文档,你只能借助于它的 API 文档、一两个简单例子以及源码来熟悉它。
如果是 HTML 文档,那么用 HTMLParser 已经差不多可以满足你至少 90% 的需求。一个 HTML 文档中可能出现的标签差不多在 HTMLParser 中都有对应的类,甚至包括一些动态的脚本标签,例如 <%...%> 这种 JSP 和 ASP 用到的标签都有相应的 JspTag 对应。HTMLParser 的强大功能还体现在你可以修改每个标签的属性或者它所包含的文本内容并生成新的 HTML 文档,比如你可以文档中的链接地址偷偷的改成你自己的地址等等。关于 HTMLParser 的强大功能,其实上一篇文章已经介绍很多,这里不再累赘,我们今天要讲的是另外一个用途--处理自定义标签。
首先我们先解释一下什么叫自定义标签,我把所有不是 HTML 脚本语言中定义的标签称之为自定义标签,比如可以是 <scriptlet>、<book> 等等,这是我们自己创造出来的标签。你可能会很奇怪,因为这些标签一旦用在 HTML 文档中是没有任何效果的,那么我们换另外一个例子,假如你要解析的不是 HTML 文档,而是一个 WML(Wireless Markup Lauguage)文档呢?WML 文档中的 card,anchor 等标签 HTMLParser 是没有现成的标签类来处理的。还有就是你同样可以用 HTMLParser 来处理 XML 文档,而 XML 文档中所有的标签都是你自己定义的。
为了使我们的例子更具有代表意义,接下来我们将给出一段代码用来解析出 WML 文档中的所有链接,了解 WML 文档的人都知道,WML 文档中除了与 HTML 文档相同的链接写法外,还多了一种标签叫 <anchor>,例如在一个 WML 文档我们可以用下面两种方式来表示一个链接。

<a href="http://www.javayou.com?cat_id=1">Java自由人</a>
或者:
<anchor>
Java自由人
<go href="http://www.javayou.com" method="get">
<postfield name="cat_id" value="1"/>
</go>
</anchor>

(更多的时候使用 anchor 的链接用来提交一个表单。) 如果我们还是使用 LinkTag 来遍历整个 WML 文档的话,那 Anchor 中的链接将会被我们所忽略掉。
下面我们先给出一个简单的例子,然后再叙述其中的道理。这个例子包含两个文件,一个是WML 的测试脚本文件 test.wml,另外一个是 Java 程序文件 HyperLinkTrace.java,内容如下:

1. test.wml


<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card title="Java自由人登录">
<p>    
用户名:<input type="text" name="username" size="15"/>
         密码:<input type="text" name="password" size="15"/>
<br/>
<anchor>现在登录
    <go href="/wap/user.do" method="get">
            <postfield name="name" value="$(username)"/>
            <postfield name="password" value="$(password)"/>
            <postfield name="eventSubmit_Login" value="WML"/>
    </go>
</anchor><br/>
<a href="/wap/index.vm">返回首页</a>
</p>
</card>
</wml>



test.wml 中的粗体部分是我们需要提取出来的链接。

2. HyperLinkTrace.java
//HyperLinkTrace.java

package demo.htmlparser;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.net.URL;
import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.PrototypicalNodeFactory;
import org.htmlparser.tags.CompositeTag;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
/**
* 用来遍历WML文档中的所有超链接
* @author Winter Lau
*/

public class HyperLinkTrace {
public static void main(String[] args) throws Exception {
    //初始化HTMLParser
    Parser parser = new Parser();
    parser.setEncoding("8859_1");
    parser.setInputHTML(getWmlContent());
    
    //注册新的结点解析器
    PrototypicalNodeFactory factory = new PrototypicalNodeFactory ();
    factory.registerTag(new WmlGoTag ());
    parser.setNodeFactory(factory);
    //遍历符合条件的所有节点
    NodeList nlist = parser.extractAllNodesThatMatch(lnkFilter);
    for(int i=0;i<nlist.size();i++){
     CompositeTag node = (CompositeTag)nlist.elementAt(i);
     if(node instanceof LinkTag){
        LinkTag link = (LinkTag)node;
        System.out.println("LINK: \t" + link.getLink());
     }
     else if(node instanceof WmlGoTag){
        WmlGoTag go = (WmlGoTag)node;
        System.out.println("GO: \t" + go.getLink());
     }
    }
}
/**
    * 获取测试的WML脚本内容
    * @return
    * @throws Exception
    */

static String getWmlContent() throws Exception{
    URL url = ParserTester.class.getResource("/demo/htmlparser/test.wml");
    File f = new File(url.toURI());
    BufferedReader in = new BufferedReader(new FileReader(f));
    StringBuffer wml = new StringBuffer();
    do{
     String line = in.readLine();
     if(line==null)
        break;
     if(wml.length()>0)
        wml.append("\r\n");
     wml.append(line);    
    }while(true);
    return wml.toString();    
}
/**
    * 解析出所有的链接,包括行为<a>与<go>
    */

static NodeFilter lnkFilter = new NodeFilter() {
    public boolean accept(Node node) {
     if(node instanceof WmlGoTag)
        return true;
     if(node instanceof LinkTag)
        return true;
     return false;
    }
};

/**
    * WML文档的GO标签解析器
    * @author Winter Lau
    */

static class WmlGoTag extends CompositeTag {
         private static final String[] mIds = new String[] {"GO"};
         private static final String[] mEndTagEnders = new String[] {"ANCHOR"};
         public String[] getIds (){
                 return (mIds);
         }
         public String[] getEnders (){
                 return (mIds);
         }
         public String[] getEndTagEnders (){
                 return (mEndTagEnders);
         }
        
         public String getLink(){
            return super.getAttribute("href");
         }
        
         public String getMethod(){
            return super.getAttribute("method");
         }
}
}


上面这段代码比较长,可以分成下面几部分来看:
1. getWmlContent方法: 该方法用来获取在同一个包中的test.wml脚本文件的内容并返回字符串。
2. 静态属性lnkFilter:这是一个NodeFilter的匿名类所构造的实例。该实例用来传递给HTMLParser告知需要提取哪些节点。在这个例子中我们仅需要提取链接标签以及我们自定义的一个GO标签。
3. 嵌套类WmlGoTag:这也是最为重要的一部分,这个类用来告诉HTMLParser如何去解析<go>这样一个节点。我们先看看下面这个HTMLParser的节点类层次图:

HTMLParser使用详解(5)- 扩展 HTMLParser 对自定义标签的处理能力_休闲
如上图所示,HTMLParser将一个文档分成三种节点分别是:Remark(注释);Text(文本);Tag(标签)。而标签又分成两种分别 是简单标签(Tag)和复合标签(CompositeTag),像<img><br/>这种标签称为简单标签,因为标签不会再包 含其它内容。而像<a href="xxxx">Home</a>这种类型的标签,因为标签会嵌套文本或者其他标签的称为复合标签,也就是对应着 CompositeTag这个类。简单标签的实现类很简单,只需要扩展Tag类并覆盖getIds方法以返回标签的识别文本,例如<img> 标签应该返回包含"img"字符串的数组,具体的代码可以参考HTMLParser自带的ImageTag标签类的实现。
从上图可清楚看出,复合标签事实上是对简单标签的扩展,HTMLParser在处理一个复合标签时需要知道该标签的起始标识以及结束标识,也就是我 们在前面给出的源码中的两个方法getIds和getEnders,一般来讲,标签出现都是成对的,因此这两个方法一般返回相同的值。另外一个方法 getEndTagEnders,这个方法用来返回父一级的标签名称,例如<tr>的父一级标签应该是<table>。这个方法 的必要性在于HTML对格式的要求很不严格,在很多的HTML文档中的一些标签经常是有开始标识,但是没有结束标识,由于浏览器的超强适应能力使这种情况 出现的很频繁,因此HTMLParser利用这个方法来辅助判断一个标签是否已经结束。由于WML文档的格式要求非常严格,因此上例源码中的 getEndTagEnders方法事实上可有可无。
4. 入口方法main:该方法初始化HTMLParser并注册新的节点解析器,解析文档并打印运行结果。
最后我们编译并运行这个例子,便可以得到下面的运行结果:

GO:  /wap/user.do
LINK: /wap/index.vm

HTMLParser本身就是一个开放源码的项目,它对于HTML文档中出现的标签定义已经应有尽有,我们尽可以参考这些标签解析类的源码来学习如何实现一个标签的解析类,从而扩展出更丰富多彩的应用程序。