经常要用的Xml和Html解决,实际上这个领域也有非常好的解决方案。
相对来说现在各种开源的Xml解析功能比较丰富,机制也比较灵活,但是由于他功能比较完善,干的事情比较多,所以性能方面也慢一点;另外,由于Xml天生是有严格格式的,所以问题不大,但是Html文件的内容是良莠不齐,有的网站经常缺少关闭标签,有的开始是大写,关闭是小写等等,没有严格遵守规范的时候,连Dom结构也解不正确,对于数据抓取程序来说,这就会严重影响正确性。
另外,一个重要的问题是数据遍历,一般来说在数据遍历方面,开源框架没有在性能做过充分优化,因此,如果要进行高速检索,就需要进行程序扩展。为此,本人编写一套XmlParser和HtmlParser,在数据校验方面做了删减,不支持进行数据校验,在容错性方面做了扩充,在Html解决时,即使格式不正确,在大多数情况下也可以返回正确的结果。最坏的情况也,也可以解决出Dom,但是Dom结构不一定正确,而不会出现崩溃或解析异常的问题。
还有一个是简体中文标签的支持能力,比如: <中文 属性1="1" 属性2="b" />
OK,费话少说,看看调用代码。
XmlStringParser parser = new XmlStringParser(); XmlDocument xmlDocument = parser.parse("<aa a=\"1\"><!--aa --><a a=\"aa\"></a></aa>");上面就已经把xml解析好了。
HtmlStringParser parser = new HtmlStringParser(); HtmlDocument xmlDocument = parser.parse("<aa a=\"1\"><!--aa --><a a=\"aa\"></a></aa>");上面就已经把html解析好了。
由于Xml及Html都是用得统一的接口,所以,会了Xml解析,Html也是一样样的。
解析出的Node,都实现了下面的接口,因此遍历方面也是非常方便的。
public interface Node<T extends Node<T>> extends ForEachProcessor<T> { /** * 获取结点头标签相关内容 * * @return StringBuffer */ void getHeader(StringBuffer sb); /** * 返回子节点 * * @param name * @return */ List<T> getSubNodes(String name); /** * 添加内容节点 * * @param content */ void addContent(String content); /** * 设置结点名称 * * @param name */ void setNodeName(String name); /** * 获取结尾标签 * * @return StringBuffer */ void getFooter(StringBuffer sb); /** * 获取根结点 * * @return T */ T getRoot(); /** * 设置父亲节点 * * @param parent */ void setParent(T parent); /** * 返回节点名称 * * @return */ String getNodeName(); /** * 返回父亲节点 * * @return */ T getParent(); /** * 返回中间内容 * * @return */ StringBuffer getBody(); /** * 写出数据 * * @param stream * @throws IOException */ void write(OutputStream stream) throws IOException; /** * 返回节点类型 * * @return */ NodeType getNodeType(); /** * 返回属性 * * @param attributeName * @return */ String getAttribute(String attributeName); /** * 删除属性 * * @param attributeName */ void removeAttrivute(String attributeName); /** * 设置属性值 * * @param attributeName * @param value */ void setAttribute(String attributeName, String value); /** * 添加节点 * * @param node * 要增加的节点 * @return 如果增加成功,则返回node节点,否则返回null */ T addNode(T node); /** * 删除节点 * * @param node * @return 删除的节点,如果当前节点中不包含node节点,则返回null */ T removeNode(T node); /** * 删除指定节点 * * @param nodeName * @return */ List<T> removeNode(String nodeName); /** * 获取内容 * * @return */ String getContent(); /** * 变成StreamBuffer * * @return */ StringBuffer toStringBuffer(); /** * 设置内容 * * @param content */ void setContent(String content); /** * 返回属属性 * * @return */ Map<String, String> getAttributes(); /** * 返回子节点 * * @return */ List<T> getSubNodes(); /** * 是否单节点 * * @return */ boolean isSingleNode(); /** * 是否大小写敏感 * * @return */ boolean isCaseSensitive(); /** * 根据大小写相关返回名字 * * @param name * @return */ String getCaseSensitiveName(String name); /** * 返回纯文本内容 * * @return */ String getPureText(); }为了避免接口太过庞大,因此把格式化的处理放在独立的结构中进行处理。
public interface NodeFormater<E extends Node<E>, T extends Document<E>> { /** * 格式化文档 * * @param doc * @return String */ String format(T doc); void setEncode(String encode); /** * 格式化文档、 并在指定的输出流中输出 * * @param doc * @param out * @return void * @throws IOException */ String format(E node); void format(T doc, OutputStream out) throws IOException; void format(E node, OutputStream out) throws IOException; }要格式化输入的话,下面的代码就可以了:
HtmlDocument doc= new XmlStringParser().parse("<html 中='文'><head><title>aaa</title></head></html>"); HtmlFormater f = new HtmlFormater(); System.out.println(f.format(doc));输出结果如下:
<html 中="文"> <head> <title> aaa </title> </head> </html>上面已经演示了解析和格式化以及遍历,下面看看检索。
首先构建60*60*60,三层的Dom结构,也就是现在有216000个Dom节点
XmlNode node = new XmlNode("root"); for (int i = 0; i < 60; i++) { XmlNode a = node.addNode(new XmlNode("a" + i)); for (int j = 0; j < 60; j++) { XmlNode b = a.addNode(new XmlNode("b" + j)); for (int k = 0; k < 60; k++) { b.addNode(new XmlNode("c" + k)); } } }然后对其进行节点查找,用两种方法进行10000次节点过滤:
public void testSpeed() { long t21 = System.currentTimeMillis(); QuickNameFilter quick = new QuickNameFilter(node); long t22 = System.currentTimeMillis(); System.out.println("quick初始化用时" + (t22 - t21)); long t1 = System.currentTimeMillis(); String nodeName = null; for (int x = 0; x < 10000; x++) { nodeName = quick.findNode("b6").toString(); } long t2 = System.currentTimeMillis(); System.out.println("QuickNameFilter用时" + (t2 - t1)); } public void testSpeed1() { long t21 = System.currentTimeMillis(); FastNameFilter fast = new FastNameFilter(node); long t22 = System.currentTimeMillis(); System.out.println("fast初始化用时" + (t22 - t21)); long t1 = System.currentTimeMillis(); String nodeName = null; for (int x = 0; x < 10000; x++) { nodeName = fast.findNode("b6").toString(); } long t2 = System.currentTimeMillis(); System.out.println("FastNameFilter用时" + (t2 - t1)); }
下面看看时间耗费情况:
quick初始化用时385 QuickNameFilter用时376 fast初始化用时122 FastNameFilter用时330可以看到fast的初始化时间及查找用时,都是最快的;而quick的初始化时间和查找用时相比要慢一些。但是请注意,这都是在216000个节点中查找10000次所耗费的时间。
那么再用传统的方式试一下---一般的开源方式也差不多在这个量级。
public void testSpeed2() { long t11 = System.currentTimeMillis(); NameFilter filter = new NameFilter(node); long t12 = System.currentTimeMillis(); System.out.println("Name初始化用时" + (t12 - t11)); long t1 = System.currentTimeMillis(); String nodeName = null; for (int x = 0; x < 10; x++) { nodeName = filter.findNode("b6").toString(); } long t2 = System.currentTimeMillis(); System.out.println("NameFilter用时" + (t2 - t1)); }运行结果:
Name初始化用时12 NameFilter用时83但是,请注意,他的查询次数是10次,如果变成10000次,就是83000ms,也就是83秒之多。与Fast过滤方式相差了680倍之多。
小结:我们实现的Xml及HtmlParser确实是有自己独特的优点(学习成本低,Html和Xml解析方法一致,格式化输出,紧凑输出,容错性,查询效率高等等),也有不足(不支持DTD,XSD校验),在不需要校验的场景,需要容错性好及过滤性能高的场景下,是非常有优势的。