一、简介

为了简化编写处理 XML 的 Java 程序,已经建立了多种编程接口。这些接口或者由公司定义,或者由标准体或用户组定义,以满足 XML 程序员的需要:

  • Document Object Model (DOM,文档对象模型),Level 2
  • Simple API for XML (SAX), Version 2.0
  • JDOM, Jason Hunter 和 Brett McLaughlin 创立的一种简单 Java API
  • Java API for XML Processing (JAXP)

这四种接口中前三个(DOM、SAX 和 JDOM)定义了如何访问与表示 XML 文档的内容。JAXP 包含创建解析器对象的类。要创建 DOM 或 SAX 解析器,你需要使用 JAXP。如果使用 JDOM,JDOM 库将在幕后使用 JAXP 为你创建一个解析器。总之:

  • 使用 DOM、SAX 或 JDOM 处理 XML 文档的内容。
  • 如果使用 DOM 或 SAX,则使用 JAXP 创建解析器。
  • 如果使用 JDOM,则 JDOM 库为你创建解析器。

本文只讨论 DOM 和 SAX,不讨论 JDOM。

二、XML 解析器

XML 解析器是读取 XML 文档并分析其结构的一段代码。一般而言使用解析器需要以下步骤:

  1. 创建一个解析器对象
  2. 使解析器指向你的 XML 文档
  3. 处理结果

解析器有不同的分类方法:

  • 验证和非验证解析器
  • 支持一种或多种 XML Schema 语言的解析器
  • 支持 Document Object Model (DOM) 的解析器
  • 支持 Simple API for XML (SAX) 的解析器

有三种不同类型的 XML 文档:

  • 结构良好的文档:这类文档符合 XML 基本规则(属性必须放在引号中、标签必须正确嵌套等等)。
  • 有效文档:这些结构良好的文档同时还符合文档类型定义(DTD)或 XML Schema 所定义的规则。
  • 无效文档:所有其他文档。

“验证解析器”在解析时验证 XML 文档,而“非验证解析器”不验证文档。换句话说,如果一个 XML 文档是结构良好的,那么非验证解析器并不关心文档是否符合 DTD 或模式中定义的规则,甚至不关心该文档是否有这样的规则(多数验证解析器都默认关闭验证功能)。XML 解析器读取 DTD 或者模式、建立规则引擎保证 XML 文档中的每个元素和属性都遵循这些规则,需要做大量的工作。如果你确信一个 XML 文档是有效的(比如,可能由一个数据库查询生成),那么就可以完全跳过验证。根据文档规则复杂程度的不同,这样可以节约相当可观的时间和内存。

为了解决 DTD 的局限性,万维网联盟(W3C)创建了 XML Schema 语言。XML Schema 允许以精确得多的方式定义有效的文档应该是什么样子。XML Schema 语言非常丰富,XML Schema 文档可能非常复杂。因此,支持 XML Schema 验证的 XML 解析器往往非常大。W3C XML Schema 语言并不是唯一的模式语言。一些解析器支持其他的 XML 模式语言,比如 RELAX NG 或者 Schematron。

文档对象模型(DOM)是正式的 W3C 推荐标准。它定义了一个接口,使程序能够访问和更新 XML 文档的结构。如果一个 XML 解析器声称支持 DOM,就意味着它实现了该标准中定义的接口。当使用 DOM 解析器解析一个 XML 文档时,得到 一棵结构树,它表示 XML 文档的内容。所有的文本、元素和属性(以及其他的东西)都在这个树结构中。DOM 还提供各种不同的功能,可用于分析和操作树的内容和结构。

Simple API for XML (SAX) API 是处理 XML 文档内容的一种替代方法。它的设计目标是更少的内存占用,但是把更多的工作交给了程序员。SAX 和 DOM 是互补的,有各自的适用环境。作为一个“事实上的”标准,SAX 吸收了 Internet 上许多用户的想法。当使用 SAX 解析器解析一个 XML 文档时,解析器在读取文档的过程中会生成一系列的事件。至于如何处理这些事件则取决于程序员。

一般说来以下情况应使用 DOM 解析器:需要详细了解文档的结构、需要改变文档的结构(也许你需要对元素排序、增加新的元素等等)、 需要多次引用解析的信息。

在以下情况中应使用 SAX 解析器:内存少(就是说你的“机器”没有太多内存)、只需要 XML 文档中少量元素或属性、解析的信息只使用一次。

三、文档对象模型(DOM)

DOM 是处理 XML 文档结构的一种接口。作为一个 W3C 项目,DOM 的设计目标是提供一组对象和方法,使程序员的工作更轻松。(从技术上讲,DOM 先于 XML;早期的 DOM 研究是对 HTML 文档而言的。)理想情况下,你应该能够编写一个应用程序使用一种 DOM 兼容的解析器处理 XML 文档,然后切换到另外一种 DOM 兼容的解析器而无需改变代码。

当使用 DOM 解析器解析一个 XML 文档时,你得到一个层次化的数据结构(DOM 树),它表示解析器在 XML 文档中发现的所有内容。然后你可以使用 DOM 函数操纵这棵树。你可以搜索树中的内容、移动分支、增加新的分支或者删除树的一部分。

Node 是 DOM 的基本数据类型,DOM 树中的所有事物都是这种或那种类型的 Node。DOM Level 1 还定义了 Node 接口的几种子接口:

  • Element:表示源文档中的一个 XML 元素。
  • Attr:表示 XML 元素的一个属性。
  • Text:一个元素的内容。这意味着带有文本的元素包含文本节点孩子,元素的文本 不是 元素本身的一个属性。
  • Document:表示整个 XML 文档。解析的每个 XML 文档中有且只有一个 Document 对象。给定一个 Document 对象就可以找到 DOM 树的根,从这个根可以使用 DOM 函数读和操纵树。

其他的节点类型包括:Comment 表示 XML 文件中的注释、ProcessingInstruction 表示处理指令、CDATASection 表示 CDATA 节。

注意:“元素”和“标签”这两个词有不同的含义。“元素”是指起始元素、结束元素以及两者之间的一切内容,包括属性、文本、注释以及子元素。“标签”是一对<尖括号>和两者之间的内容,包括元素名和所有属性。比如 <p class="blue"> 是一个标签,</p> 也是;而 <p class="blue">The quick brown fox</p> 是一个元素。

处理 DOM 经常要用到以下方法:

  • Document.getDocumentElement():返回 DOM 树的根。(该函数是 Document 接口的一个方法,没有定义其他的 Node 子类型。)
  • Node.getFirstChild() 和 Node.getLastChild():返回给定 Node 的第一个和最后一个孩子。
  • Node.getNextSibling() 和 Node.getPreviousSibling():返回给定 Node 的下一个和上一个兄弟。
  • Element.getAttribute(String attrName):对于给定的 Element,返回名为 attrName 的属性的值。如果需要 "id" 属性的值,可以使用 Element.getAttribute("id")。如果该属性不存在,该方法返回一个空字符串 ("")。

3.1 准备一个 XML 文档

为后面的例程准备一个 XML 文档 test.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
 <诗 type="现代诗">
 <作者>
 <姓名>顾城</姓名>
 <生年>1956</生年>
 <卒年>1993</卒年>
 </作者>
 <标题>一代人</标题>
 <正文>
 <line>黑夜给了我黑色的眼睛</line>
 <line>我却用它寻找光明</line>
 </正文>
 </诗>

注意:XML 文档推荐 utf-8 编码。个人推荐所有涉及到编程的文本文件都使用 utf-8 编码,例如 java、html、css、js、jsp 等。在 Windows 系统下,如果手工编辑文本文件,则需要借助 UltraEdit 这样的工具,将文件保存为“utf-8 无 BOM”格式。用 Windows 记事本是不行的,记事本保存的 utf-8 格式是带 BOM 的。在 Windows 下用 javac 手工编译 utf-8 编码的 Java 源文件,需要使用 -encoding utf-8 参数。如果使用 Eclipse 则在菜单“Window - Preference”中的“General - Content types”中 “Text”下文件都改成 utf-8 编码即可。

3.2 第一个 DOM 例程

第一个例程 Dom1.java,这段简单的 Java 代码完成四件事:扫描命令行得到 XML 文件名、创建一个解析器对象、告诉解析器解析命令行中给定的 XML 文件、遍历 DOM 结果树向标准输出打印各种节点的内容。

import org.w3c.dom.Attr;
 import org.w3c.dom.Document;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;

 public class Dom1 {
 public static void main(String argv[]) {
 if (argv.length == 0 || (argv.length == 1 && argv[0].equals("-help"))) {
 System.out.println("命令行使用样例:java Dom1 test.xml");
 System.out.println("\n该程序解析一个XML文档,并在终端窗口显示该文档的DOM树。");
 System.exit(1);
 }

 Dom1 dom1 = new Dom1();
 dom1.parseAndPrint(argv[0]);
 }

 public void parseAndPrint(String uri) {
 Document doc = null;

 try {
 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
 // 有没有下面这句,忽略空格,似乎关系不大。因为使用了DOS回车(?)
 // 如果忽略空格,理论上XML文档的所有内容应该会写在一行上(?)
 dbf.setIgnoringElementContentWhitespace(true);
 DocumentBuilder db = dbf.newDocumentBuilder();
 doc = db.parse(uri);
 if (doc != null)
 printDomTree(doc);
 } catch (Exception ex) {
 System.err.println(ex.toString());
 }
 }

 public void printDomTree(Node node) {
 int type = node.getNodeType();
 switch (type) {
 // 打印文档元素
 case Node.DOCUMENT_NODE: { // 根节点
 System.out.println("使用DOM解析的XML文档内容如下:");
 printDomTree(((Document) node).getDocumentElement());
 break;
 }
 case Node.ELEMENT_NODE: { // 普通元素节点
 System.out.print("<");
 System.out.print(node.getNodeName());

 NamedNodeMap attrs = node.getAttributes();
 for (int i = 0; i < attrs.getLength(); i++)
 printDomTree(attrs.item(i));

 System.out.print(">");

 if (node.hasChildNodes()) {
 NodeList children = node.getChildNodes();
 for (int i = 0; i < children.getLength(); i++)
 printDomTree(children.item(i));
 }

 System.out.print("</");
 System.out.print(node.getNodeName());
 System.out.print('>');

 break;
 }
 case Node.ATTRIBUTE_NODE: { // 属性节点
 System.out.print(" " + node.getNodeName() + "="" + ((Attr) node).getValue() + """);
 break;
 }

 case Node.TEXT_NODE: { // 文本节点
 System.out.print(node.getNodue());
 break;
 }
 }
 }

 }

四、Simple API for XML (SAX)

SAX 是一种 推式 API:你创建一个 SAX 解析器,解析器在发现 XML 文档中的内容时告诉你(把事件推给你)。具体来讲,SAX 是通过这样解决上述问题的:

  • SAX 不建立 XML 文档的内存树。SAX 解析器在发现 XML 文档中的事物时发出事件。如何(或者是否)保存那些数据取决于你。
  • SAX 解析器不创建任何对象。如果需要你可以建立对象,但这是你的事情,与解析器无关。
  • SAX 立即发出事件,不需要等待解析器读完文档。

SAX API 定义了许多事件。你的任务是编写 Java 代码对这些事件作出响应。你很可能要在应用程序中使用 SAX 帮助器类。如果使用帮助器类,你就只需为关心的少量事件编写事件处理程序,让帮助器类完成其他的工作。如果不处理某个事件,解析器将丢弃该事件,因此不必担心内存使用、不必要的对象以及其他使用 DOM 解析器所担心的问题。

SAX 事件是“无状态的”。换句话说,观察单个的 SAX 事件不能说明其中的内容。如果你需要了解 <lastName> 元素内 <author> 元素中出现的一段文本,连续记录解析器所发现的元素是你的工作。所有的 SAX 事件只能告诉你,“这里是一些文本”。指出该文本属于哪个元素要由你自己完成。

在基于 SAX 的应用程序中,多数时候你都要处理五种基本的事件:

  • startDocument() 告诉你解析器发现了文档的开始。该事件没有传递任何信息,只是告诉你解析器开始扫描文档。
  • endDocument() 告诉你解析器发现了文档尾。
  • startElement(...) 告诉你解析器发现了一个起始标签。该事件告诉你元素的名称、该元素所有属性的名称和值,还会告诉你一些名称空间的信息。
  • characters(...) 告诉你解析器发现了一些文本。你得到一个字符数组、该数组的偏移量和一个长度变量,有这三个变量你就可以访问解析器所发现的文本。
  • endElement(...) 告诉你解析器发现了一个结束标签。该事件告诉你元素的名称,以及相关的名称空间信息。

4.1 第一个 SAX 例程

这个应用程序 Sax1.java 和你的第一个 DOM 应用程序 Dom1.java 类似。因此运行结果将和前面的运行结果相同。代码需要完成三项工作,基本上与 Dom1 的四项任务相同:扫描命令行得到 XML 文件名称(或者 URI)、创建解析器对象、告诉解析器对象解析命令行中指定的文件,并要求它把生成的所有 SAX 事件发送给你的代码。

import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;

 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
 import org.xml.sax.SAXParseException;
 import org.xml.sax.helpers.DefaultHandler;

 public class Sax1 extends DefaultHandler {
 public static void main(String argv[]) {
 if (argv.length == 0 || (argv.length == 1 && argv[0].equals("-help"))) {
 System.out.println("命令行使用样例:java Sax1 test1.xml");
 System.out.println("\n该程序解析一个XML文档,并在终端窗口显示该文档的SAX树。");
 System.exit(1);
 }
 Sax1 sax1 = new Sax1();
 sax1.parseURI(argv[0]);
 }

 public void parseURI(String uri) {
 try {
 SAXParserFactory spf = SAXParserFactory.newInstance();
 SAXParser sp = spf.newSAXParser();
 sp.parse(uri, this);
 } catch (Exception e) {
 System.err.println(e);
 }
 }

 public void startDocument() {
 System.out.println("使用SAX解析的XML文档内容如下:");
 }

 public void startElement(String namespaceURI, String localName, String rawName, Attributes attrs) {
 System.out.print("<");
 System.out.print(rawName);
 if (attrs != null) {
 int len = attrs.getLength();
 for (int i = 0; i < len; i++) {
 System.out.print(" ");
 System.out.print(attrs.getQName(i));
 System.out.print("="");
 System.out.print(attrs.getValue(i));
 System.out.print(""");
 }
 }
 System.out.print(">");
 }

 public void characters(char ch[], int start, int length) {
 System.out.print(new String(ch, start, length));
 }

 public void endElement(String namespaceURI, String localName, String rawName) {
 System.out.print("</");
 System.out.print(rawName);
 System.out.print(">");
 }

 public void endDocument() {

 }

 public void warning(SAXParseException ex) {
 System.err.println("[Warning] " + ex.getLineNumber() + "-" + ex.getColumnNumber() + ex.getMessage());
 }

 public void error(SAXParseException ex) {
 System.err.println("[Error] " + ex.getLineNumber() + "-" + ex.getColumnNumber() + ex.getMessage());
 }

 public void fatalError(SAXParseException ex) throws SAXException {
 System.err.println("[Fatal Error] " + ex.getLineNumber() + "-" + ex.getColumnNumber() + ex.getMessage());
 throw ex;
 }

 }