XML正迅快速的成为数据存储和交换的标准格式流行开来了。现在可用的完整的Java XML分析器非常庞大而且功能强大--但是实现这些强大的功能的同时也要消耗等量的资源。举例来说,流行的Apache Xerces-J分析器超过1.7 MB,而最新的完整的Sun JAXP(用于XML处理的Java应用编程接口)实现程序包超过了3MB。因此使用一个功能强大的XML分析器可能过于浪费。如果配置环境是一个Java小程序或者是一个J2ME应用程序,网络带宽或者系统存储器的制约可能根本不能够使用完整的XML分析器。本文将告诉你如何构建一个轻量级的XML DOM分析程序。
开始编写SimpleDOMParser
SimpleDOMParser是一个使用Java写的高度简化和超轻量级的XML DOM分析器。 你可以将配置整个分析器配置为一个小于4KB的.jar文件。源程序还不到400行呢。
显然,使用这么小的代码,SimpleDOMParser将不支持XML域名空间,不能够理解多字符集编码或者以DTD文件或者schema验证文件;但是SimpleDOMParser能做的就是把符合语法规则的XML标记解析为一个类似于DOM的元素树,让你执行从XML格式化文本提取的数据的公共任务。
为什么使用DOM作为模型而不是SAX呢?这是因为DOM提供一个比SAX更加易用的程序接口。与SAX不同的是,当你把一个XML文件作为一个DOM树来处理的时候,这个文件内的所有的信息都是可以利用的。虽然SAX分析模型能够提供比DOM模型更加优异的性能和利用更少的存储空间,但是大部分开发者在使用SAX的时候都会发现他们自己正在创建一个完整的或者部分的DOM树。使用SAX,一个应用程序每次只能处理一条标记。如果其它的标记内容在处理的过程中必须被用到,那你就必须在处理的整个过程保持一种全局状态。而保持全局状态正是DOM模型目的的精髓。但是许多小型的XML应用程序不需要完整的DOM模型。因此,SimpleDOMParser提供到标记名、层次和内容的访问,但是不涉及完整的W3C DOM的许多用不上的功能。
简化DOM模型
一个DOM树是由分析XML文件产生的结点组成。结点是一个XML实体的非存储表现。标准W3C DOM模型有几种类型的结点。 举例来说,一个文本结点表示在XML文件中的一段文本,一个元素结点表示XML文件而一个属性结点表示一个元素内部的属性名和值。
DOM是一个树,因为除了根或文件结点以外的每个结点都有一个父结点。举例来说,属性结点总是和一个元素结点相关联,而用来封装元素的起始标记和结束标记中的文本是映射到一个文本结点的。文本结点是元素结点的一个子节点。所以,即使很简单的XML文件的表现也可能会需要很多种节点类型。举例来说,图1表示下面XML文件的一个W3C DOM树形表示。
<parser>SimpleDOMParser</parser> |
正如你在图1中所看见的,DOM模型使用一个document类型节点来封装整个XML文件,所以DOM使用三种不同的节点。通过把所有的DOM节点类型抽象成为一个单独的类型SimpleElement来尽可能的简化DOM模型。一个SimpleElement获得一个XML元素的关键的信息,比如标识名、元素属性和任何封装的文本或者XML。此外,SimpleDOMParser不使用任何特殊的节点类型表示最高等级的文档。结果是大大地简化了DOM树,使之只包含SimpleElement节点。图2表示了简化的DOM树。
代码段1给出了SimpleElement类的完整的源程序。
public class SimpleElement { private String tagName; private String text; private HashMap attributes; private LinkedList childElements; public SimpleElement(String tagName) { this.tagName = tagName; attributes = new HashMap(); childElements = new LinkedList(); } public String getTagName() { return tagName; } public void setTagName(String tagName) { this.tagName = tagName; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getAttribute(String name) { return (String)attributes.get(name); } public void setAttribute(String name, String value) { attributes.put(name, value); } public void addChildElement(SimpleElement element) { childElements.add(element); } public Object[] getChildElements() { return childElements.toArray(); } } |
定义XML语法分析基本元素
为了把一个XML文件处理成为上面提到的简化的DOM树模型,我们必须定义一些基本的语法分析规则。使用这些规则,语法分析程序就能容易地从输入的XML文件中提取标记或者文本块。
第一个是peek,从输入的XML文件中返回下一个字符,而实际上则不必从下层流中获得这个字符。通过保持输入流的完整性,高级函数比如readTag和readText(后面将介绍)可以更加容易地根据它们接下来期待的字符获取需要的内容。
private int peek() throws IOException { reader.mark(1); int result = reader.read(); reader.reset(); return result; } |
下一个方法是skipWhitespce,作用是跳过输入的XML流中的空格、制表符或者回车符。
private void skipWhitespace() throws IOException { while (Character.isWhitespace((char) peek())) { reader.read(); } } |
在创建了如上所述的这两个方法后,我们就可以写一个函数从输入文件中检索XML标记。
private String readTag() throws IOException { skipWhitespace(); StringBuffer sb = new StringBuffer(); int next = peek(); if (next != '<') { throw new IOException ("Expected > but got " + (char) next); } sb.append((char)reader.read()); while (peek() != '>') { sb.append((char)reader.read()); } sb.append((char)reader.read()); return sb.toString(); } |
和peek方法联合使用,readTag函数只获得一个标记的内容,而让别的函数去处理其他的内容。 最后的一个方法是readText函数,用来读取XML标记之间的文本。
private String readText() throws IOException { int[] cdata_start = {'<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['}; int[] cdata_end = {']', ']', '>'}; StringBuffer sb = new StringBuffer(); int[] next = new int[cdata_start.length]; peek(next); if (compareIntArrays(next, cdata_start) == true) { // CDATA reader.skip(next.length); int[] buffer = new int[cdata_end.length]; while (true) { peek(buffer); if (compareIntArrays (buffer, cdata_end) == true) { reader.skip(buffer.length); break; } else { sb.append((char)reader.read()); } } } else { while (peek() != '<') { sb.append((char)reader.read()); } } return sb.toString(); } |
这次使用的peek方法是前面那个从基本的XML文档返回一个字符串序列的peek方法的变体。这个peek变体让语法分析程序判断它将分析的文本是否被装入一个CDATA块。 compareIntArrays函数是一个执行两个整数数组的深度比较的简单程序。
XML语法分析策略和SimpleDOMParser实现
与一个正常的文本文档不同的是,一个符号语法规则的XML文档有一些独特的特点,可以促进语法分析工作:
在一个XML文档中所有的标记都必须匹配。每个起始标记都必须有一个匹配的结束标记,除了当标记本身就是两个起始和结束标记的时候,比如<parser/>就是<parser></parser>的简易格式。标记和属性名称是大小写敏感的。
在一个XML文档中所有的标记都必须正确地嵌套。XML标记不可以交叉嵌套。 例如:一个包含<t1><t2>... </t1></t2>的文档就是错误的,因为结束标记</t1>出现在了结束标记</t2>之前。
着眼于这些规则,SimpleDOMParser语法分析策略就应该遵循如下伪代码所示的模式:
While Not EOF(Input XML Document) Tag = Next tag from the document LastOpenTag = Top tag in Stack If Tag is an open tag Add Tag as the child of LastOpenTag Push Tag in Stack Else // 结束标记 If Tag is the matching close tag of LastOpenTag Pop Stack If Stack is empty Parse is complete End If Else // 无效标记嵌套 Report error End If End If End While |
这算法的关键就是标记堆栈,它可以保存从输入文件中获得的但是没有与它们结束标记匹配的起始标记。堆栈的顶部总是最后的一个起始标记。
除第一个标记以外,每个新的起始标记将是上一个起始标记的子标记。所以语法分析程序把新的标记添加为上一个起始标记的子标记,然后把它推到堆栈的顶部,它就成了最新的起始标记。 另一方面,如果输入标记是一个结束标记,它必须匹配最后一个起始标记。 基于正确嵌套规则,一个不匹配结束标记会出现XML语法错误。 当结束标记匹配最后一个起始标记,语法分析程序从堆栈中弹出最后一个起始标记,因为对于这个标记的分析是完整的。这个处理继续下去,直到堆栈为空为止。此时,你就完成了整个文档的语法分析过程。代码段2给出了SimpleDOMParser.parse方法的整个源代码。
SimpleDOMParser.java package simpledomparser; import java.io.Reader; import java.io.IOException; import java.io.EOFException; import java.util.Stack; public class SimpleDOMParser { private static final int[] cdata_start = {'<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['}; private static final int[] cdata_end = {']', ']', '>'}; private Reader reader; private Stack elements; private SimpleElement currentElement; public SimpleDOMParser() { elements = new Stack(); currentElement = null; } public SimpleElement parse(Reader reader) throws IOException { this.reader = reader; skipPrologs(); while (true) { int index; String tagName; String currentTag = readTag().trim(); if (currentTag.startsWith("</")) { //结束标记 tagName = currentTag.substring(2, currentTag.length()-1); //没有起始标记 if (currentElement == null) { throw new IOException("Got close tag '" + tagName + "' without open tag."); } //结束标记与起始标记不匹配 if (!tagName.equals(currentElement.getTagName())) { throw new IOException("Expected close tag for '" + currentElement.getTagName() + "' but got '" + tagName + "'."); } if (elements.empty()) { //文本处理结束 return currentElement; } else { //弹出前面的起始标记 currentElement = (SimpleElement)elements.pop(); } } else { // 起始标记或者带有起始标记和结束标记的标记 index = currentTag.indexOf(" "); if (index < 0) { // 不带属性的标记 if (currentTag.endsWith("/>")) { tagName = currentTag.substring(1, currentTag.length()-2); currentTag = "/>"; } else { // 起始标记 tagName = currentTag.substring(1, currentTag.length()-1); currentTag = ""; } } else { // 带有属性的标记 tagName = currentTag.substring(1, index); currentTag = currentTag.substring(index+1); } //创建元素 SimpleElement element = new SimpleElement(tagName); //分析属性 boolean isTagClosed = false; while (currentTag.length() > 0) { currentTag = currentTag.trim(); if (currentTag.equals("/>")) { //结束标记 isTagClosed = true; break; } else if (currentTag.equals(">")) { //起始标记 break; } index = currentTag.indexOf("="); if (index < 0) { throw new IOException("Invalid attribute for tag '" + tagName + "'."); } // 取得属性名 String attributeName = currentTag.substring(0, index); currentTag = currentTag.substring(index+1); // 取得属性值 String attributeValue; boolean isQuoted = true; if (currentTag.startsWith("\"")) { index = currentTag.indexOf('"', 1); } else if (currentTag.startsWith("'")) { index = currentTag.indexOf('\'', 1); } else { isQuoted = false; index = currentTag.indexOf(' '); if (index < 0) { index = currentTag.indexOf('>'); if (index < 0) { index = currentTag.indexOf('/'); } } } if (index < 0) { throw new IOException("Invalid attribute for tag '" + tagName + "'."); } if (isQuoted) { attributeValue = currentTag.substring(1, index); } else { attributeValue = currentTag.substring(0, index); } // 添加属性到新的元素中 element.setAttribute(attributeName, attributeValue); currentTag = currentTag.substring(index+1); } // 读取起始标记和结束标记之间的文本 if (!isTagClosed) { element.setText(readText()); } // 添加一个新的元素作为目前元素的子元素 if (currentElement != null) { currentElement.addChildElement(element); } if (!isTagClosed) { if (currentElement != null) { elements.push(currentElement); } currentElement = element; } else if (currentElement == null) { // 在文档中只有一个标记 return element; } } } } private int peek() throws IOException { reader.mark(1); int result = reader.read(); reader.reset(); return result; } private void peek(int[] buffer) throws IOException { reader.mark(buffer.length); for (int i=0; i<buffer.length; i++) { buffer[i] = reader.read(); } reader.reset(); } private void skipWhitespace() throws IOException { while (Character.isWhitespace((char)peek())) { reader.read(); } } private void skipProlog() throws IOException { //跳过"<?" or "<!" reader.skip(2); while (true) { int next = peek(); if (next == '>') { reader.read(); break; } else if (next == '<') { skipProlog(); } else { reader.read(); } } } private void skipPrologs() throws IOException { while (true) { skipWhitespace(); int[] next = new int[2]; peek(next); if (next[0] != '<') { throw new IOException("Expected '<' but got '" + (char)next[0] + "'."); } if ((next[1] == '?') || (next[1] == '!')) { skipProlog(); } else { break; } } } private String readTag() throws IOException { skipWhitespace(); StringBuffer sb = new StringBuffer(); int next = peek(); if (next != '<') { throw new IOException("Expected < but got " + (char)next); } sb.append((char)reader.read()); while (peek() != '>') { sb.append((char)reader.read()); } sb.append((char)reader.read()); return sb.toString(); } private String readText() throws IOException { StringBuffer sb = new StringBuffer(); int[] next = new int[cdata_start.length]; peek(next); if (compareIntArrays(next, cdata_start) == true) { // CDATA reader.skip(next.length); int[] buffer = new int[cdata_end.length]; while (true) { peek(buffer); if (compareIntArrays(buffer, cdata_end) == true) { reader.skip(buffer.length); break; } else { sb.append((char)reader.read()); } } } else { while (peek() != '<') { sb.append((char)reader.read()); } } return sb.toString(); } private boolean compareIntArrays(int[] a1, int[] a2) { if (a1.length != a2.length) { return false; } for (int i=0; i<a1.length; i++) { if (a1[i] != a2[i]) { return false; } } return true; } } |
为了简单起见,SimpleDOMParser不允许在XML文档中使用注解,并且完全忽视XML声明和DOCTYPE。它使用下面的程序来跳过XML声明和DOCTYPE元素。这个程序递归调用它本身,就像处理一个内部DTD一样处理DOCTYPE。
private void skipProlog() throws IOException { //跳过"<?"或者 "<!" reader.skip(2); while (true) { int next = peek(); if (next == '>') { reader.read(); break; } else if (next == '<') { skipProlog(); } else { reader.read(); } } } |
虽然本文中介绍的SimpleDOMParser只有有限的功能,但是对于许多简单的应用程序来说,它仍然是非常有用的。比如说,一个Java applet可以使用它以XML格式传送数据到一个后端服务器应用程序中。因为它是极其轻便的,所以SimpleDOMParser在资源非常有限的环境中更是很有吸引力的。此外,SimpleDOMParser的实现还很简单。虽然目前的实现只能保存元素,而不能保存声明或者DOCTYPE,但是你可以修改它来处理你想要处理的XML文本,而这一切都是非常容易的。