这两天准备做一些网站编程的工作,于是对HtmlParse小研究了一下,目的是快速入手,而不是深入研究,做了一下整理,和大家共同讨论一下。
一,数据组织分析:
HtmlParser主要靠Node、AbstractNode和Tag来表达Html,因为Remark和Text相对简单,此处就将其忽略了。
* Node是形成树结构表示HTML的基础,所有的数据表示都是接口Node的实现,Node定义了与页面树结构所表达的页面Page对象,定义了获取父、子、兄弟节点的方法,定义了节点到对应html文本的方法,定义了该节点对应的起止位置,定义了过滤方法,定义了Visitor访问机制。
* AbstractNode是Node的一种具体的类实现,起到构成树形结构的作用,除了同具体Node相关的accetp方法,toString,toHtml,toPlainTextString方法以外,AbstractNode实现了大多基本的方法,使得它的子类,不用理会具体的树操作。
* Tag是具体分析的主要内容。Tag分成composite的Tag和不能包含其他Tag的简单Tag两类,其中前者的基类是CompositeTag,其子类包含BodyTag,Div,FrameSetTag,OptionTag,等27个子类;而简单Tag有BaseHrefTag、DoctypeTag,FrameTag,ImageTag,InputTag,JspTag,MetaTag,ProcessingInstructionTag这八类。
Node分成三类:
* RemarkNode:代表Html中的注释
* TagNode:标签节点,是种类最多的节点类型,上述Tag的具体节点类都是TagNode的实现。
* TextNode:文本节点
二,Visitor方式访问Html:
1,整体解析过程
* 用一个URL或页面String做一个Parser
* 用这个Parser做一个Visitor
* 使用Parser.visitAllNodeWith(Visitor)来遍历节点
* 获取Visitor遍历后得到的数据
2,Visit过程
* 做解析之前做的事情:visitor.beginParsing();
* 每次取到一个节点Node,让该Node接受accept该Visitor
* 做解析后做的事情:visitor.finishedParsing();
3,获取节点的过程:逐步遍历Html,分析出Node。此部分较为复杂,且对于我们应用来说无需很多了解,暂跳过。
4,节点访问
节点访问采用Visitor模式,Node的accept方法和具体Visitor的visit方法是关键。
首先三类Node来accept的方式各不相同:
* 对于所有TagNode都使用一个accept方法,即TagNode的accept方法。首先判断是否是标签结尾,如果是就visitor.visitEndTag (this);否则visitor.visitTag (this);
* 如果是TextNode,那就visitor.visitStringNode (this);就可以了。
* 如果是RemarkNode,那就visitor.visitRemarkNode (this);就可以了。
实际上NodeVisitor里边这四种visit方法都是空的,因为在不同的Visitor中对于这三类节点的处理是不同的;对于需要处理的节点,只要重载对应的visit方法就行了,如果不处理那就不理会就可以了;另外,如果用户用自己的Visitor,那么还可以灵活的处理不同类型的节点了。
系统为我们实现了下面我要介绍的8种Visitor,实际上可以看作是系统给我们演示了如何做各种各样的Visitor来访问Html,因为实际上我们要真正来用HtmlParser的话,还需要特定的Visitor,而通过简单的这些系统提供的Visitor组合是难以做成什么事情的。
三,系统Visitor功能简介:
* ObjectFindingVisitor:用来找出所有指定类型的节点,采用getTags()来获取结果。
* StringBean:用来从一个指定的URL获取移除了<SCRIPT></SCRIPT>和<PRE></PRE>之间代码的Html代码,也可以用做Visitor,用来移除这两种标签内部的代码,采用StringBean.getStrings()来获取结果。
* HtmlPage:提取Title,body中的节点和页面中的TableTag节点。
* LinkFindingVisitor:找出节点中包含某个链接的总个数。
* StringFindingVisitor:找出遍历的TextNode中含有指定字符串的个数。
* TagFindingVisitor:找出指定Tag的所有节点,可以指定多种类型。
* TextExtractingVisitor:从网页中把所有标签去掉来提取文本,这个提取文本的Visitor有时是很实用的,只是注意在提取文本时将标签的属性也去掉了,也就是说只剩下标签之间的文本,例如<a>中的链接也去掉了。
* UrlModifyingVisitor:用来修改网页中的链接。
四,Filter
如果说visitor是遍历提取信息,当然这个信息可以包括某些节点或者从节点分析出来的更有效的信息,这都取决于我们的Visitor做成什么样子,那么Filter则目标很明确,就是用来提取节点的。所以说要想用HtmlParser,首先要熟悉上面讲到的数据组织。
系统定义了17种具体的Filter,包括依据节点父子关系的Filter,连接Filter组合的Filter,依据网页内容匹配情况的filter,等等。我们也可以implement Filter来做自己的Filter来提取节点。
Filter的调用是同Visitor独立的,因为也无需先filter出一些NodeList,再用Visitor来访问。调用Filter的方法是:
NodeList nodeList = myParser.parse(someFilter);
解析之后,我们可以采用:
Node[] nodes = nodeList.toNodeArray();
来获取节点数组,也可以直接访问:
Node node = nodeList.elementAt(i)来获取Node。
另外,在Filter后得到NodeList以后,我们仍然可以使用NodeList的 extractAllNodesThatMatch(someFilter)来进一步过滤,同时又可以用NodeList的 isitAllNodesWith(someVisitor)来做进一步的访问。
这样,我们可以看到HtmlParser 为我们提供了非常方便的Html解析方式,针对不同的应用可以采用visitor来遍历Html节点提取数据,也可以用Filter来过滤节点,提取出我们所关注的节点,再对节点进行处理。通过这样的组合,一定能够找出我们所需要的信息。
package test.baidu; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import org.htmlparser.Node; import org.htmlparser.NodeFilter; import org.htmlparser.Parser; import org.htmlparser.filters.NodeClassFilter; import org.htmlparser.filters.OrFilter; import org.htmlparser.filters.TagNameFilter; import org.htmlparser.nodes.TextNode; import org.htmlparser.tags.LinkTag; import org.htmlparser.util.NodeList; import org.htmlparser.util.ParserException; import org.htmlparser.visitors.HtmlPage; import org.htmlparser.visitors.TextExtractingVisitor; /** * 演示了Html Parse的应用. * */ public class ParseHtmlTest { public static void main(String[] args) throws Exception { String aFile = "F:\\百度一下,你就知道.htm"; String content = readTextFile(aFile, "GBK"); test1(content); System.out.println("===================================="); test2(content); System.out.println("===================================="); test3(content); System.out.println("===================================="); test4(content); System.out.println("===================================="); test5(aFile); System.out.println("===================================="); //访问外部资源,相对慢 test5("http://www.baidu.com"); System.out.println("===================================="); } /** * 读取文件的方式来分析内容. * filePath也可以是一个Url. * * @param resource 文件/Url */ public static void test5(String resource) throws Exception { Parser myParser = new Parser(resource); //设置编码 myParser.setEncoding("GBK"); HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getTitle(); System.out.println("test5 html title = " + textInPage); } /** * 按页面方式处理.对一个标准的Html页面,推荐使用此种方式. */ public static void test4(String content) throws Exception { Parser myParser; myParser = Parser.createParser(content, "GBK"); HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getTitle(); System.out.println("test4 html title = " + textInPage); } /** * 利用Visitor模式解析html页面. * * 小优点:翻译了<>等符号 * 缺点:好多空格,无法提取link * */ public static void test3(String content) throws Exception { Parser myParser; myParser = Parser.createParser(content, "GBK"); TextExtractingVisitor visitor = new TextExtractingVisitor(); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getExtractedText(); System.out.println("test3 = " + textInPage); } /** * 得到普通文本和链接的内容. * * 使用了过滤条件. */ public static void test2(String content) throws ParserException { Parser myParser; NodeList nodeList = null; myParser = Parser.createParser(content, "GBK"); NodeFilter textFilter = new NodeClassFilter(TextNode.class); NodeFilter linkFilter = new NodeClassFilter(LinkTag.class); //暂时不处理 meta //NodeFilter metaFilter = new NodeClassFilter(MetaTag.class); OrFilter lastFilter = new OrFilter(); lastFilter.setPredicates(new NodeFilter[] { textFilter, linkFilter }); nodeList = myParser.parse(lastFilter); Node[] nodes = nodeList.toNodeArray(); for (int i = 0; i < nodes.length; i++) { Node anode = (Node) nodes[i]; String line = ""; if (anode instanceof TextNode) { TextNode textnode = (TextNode) anode; //line = textnode.toPlainTextString().trim(); line = textnode.getText(); } else if (anode instanceof LinkTag) { LinkTag linknode = (LinkTag) anode; line = linknode.getLink(); //@todo 过滤jsp标签:可以自己实现这个函数 //line = StringFunc.replace(line, "<%.*%>", ""); } if (isTrimEmpty(line)) continue; System.out.println("test2 = " + line); } } /** * 解析普通文本节点. * * @param content * @throws ParserException */ public static void test1(String content) throws ParserException { Parser myParser; Node[] nodes = null; myParser = Parser.createParser(content, null); NodeFilter nodeFilter = new TagNameFilter(TextNode.class.getName()); nodes = myParser.extractAllNodesThatMatch(nodeFilter).toNodeArray(); //exception could be thrown here for (int i = 0; i < nodes.length; i++) { TextNode textnode = (TextNode) nodes[i]; String line = textnode.toPlainTextString().trim(); if (line.equals("")) continue; System.out.println("test1 = " + line); } } /** * 读取一个文件到字符串里. * * @param sFileName 文件名 * @param sEncode String * @return 文件内容 */ public static String readTextFile(String sFileName, String sEncode) { StringBuffer sbStr = new StringBuffer(); try { File ff = new File(sFileName); InputStreamReader read = new InputStreamReader(new FileInputStream(ff), sEncode); BufferedReader ins = new BufferedReader(read); String dataLine = ""; while (null != (dataLine = ins.readLine())) { sbStr.append(dataLine); sbStr.append("\r\n"); } ins.close(); } catch (Exception e) { System.out.println("read Text File Error"); e.printStackTrace(); } return sbStr.toString(); } /** * 去掉左右空格后字符串是否为空 * @param astr String * @return boolean */ public static boolean isTrimEmpty(String astr) { if ((null == astr) || (astr.length() == 0)) { return true; } if (isBlank(astr.trim())) { return true; } return false; } /** * 字符串是否为空:null或者长度为0. * @param astr 源字符串. * @return boolean */ public static boolean isBlank(String astr) { if ((null == astr) || (astr.length() == 0)) { return true; } else { return false; } } }