# 使用 SAX 解析 XML 文件

> 原文： [https://docs.oracle.com/javase/tutorial/jaxp/sax/parsing.html](https://docs.oracle.com/javase/tutorial/jaxp/sax/parsing.html)

在实际应用程序中，您将需要使用 SAX 解析器来处理 XML 数据并对其执行一些有用的操作。本节介绍一个示例 JAXP 程序 `SAXLocalNameCount` ，该程序在 XML 文档中仅使用元素的 `localName` 组件计算元素的数量。为简单起见，将忽略命名空间名称。此示例还说明了如何使用 SAX `ErrorHandler` 。

* * *

**注 -** 从 [JAXP 下载区](http://jaxp.java.net/downloads.html)下载并安装了 JAXP API 的源代码后，本示例的示例程序可在 _install-dir 目录中找到 _ `/ jaxp-1_4_2-` _ 释放日期 _ `/样品/ sax` 。与其交互的 XML 文件可在 _install-dir_ `/ jaxp-1_4_2-` _ 发布日期 _ `/样本/数据`中找到。

* * *

## 创建骨架

`SAXLocalNameCount` 程序在名为 `SAXLocalNameCount.java` 的文件中创建。

```
public class SAXLocalNameCount {
    static public void main(String[] args) {
        // ...
    }
}

```

因为你将独立运行它，你需要一个 `main（）`方法。并且您需要命令行参数，以便您可以告诉应用程序要处理哪个文件。

## 导入类

应用程序将使用的类的 import 语句如下。

```
package sax;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

import java.util.*;
import java.io.*;

public class SAXLocalNameCount {
    // ...
}

```

`javax.xml.parsers` 包中包含 `SAXParserFactory` 类，用于创建使用的解析器实例。如果它不能产生与指定的选项配置匹配的解析器，它会抛出 `ParserConfigurationException` 。 （稍后，您将看到有关配置选项的更多信息）。 `javax.xml.parsers` 包还包含 `SAXParser` 类，它是工厂返回进行解析的内容。 `org.xml.sax` 包定义了用于 SAX 解析器的所有接口。 `org.xml.sax.helpers` 包中包含 `DefaultHandler` ，它定义了将处理解析器生成的 SAX 事件的类。 `java.util` 和 `java.io` 中的类需要提供哈希表和输出。

## 设置 I / O.

第一项业务是处理命令行参数，在此阶段仅用于获取要处理的文件的名称。 `main` 方法中的以下代码告诉应用程序您想要处理的文件 `SAXLocalNameCountMethod` 。

```
static public void main(String[] args) throws Exception {
    String filename = null;

    for (int i = 0; i < args.length; i++) {
        filename = args[i];
        if (i != args.length - 1) {
            usage();
        }
    }

    if (filename == null) {
        usage();
    } 
}

```

此代码设置在遇到问题时抛出`异常`的主要方法，并定义告诉应用程序要处理的 XML 文件的名称所需的命令行选项。当我们开始查看验证时，本课程稍后将讨论此部分代码中的其他命令行参数。

运行应用程序时提供的`文件名`字符串将通过内部方法 `convertToFileURL（）`转换​​为 `java.io.File` URL。这是通过 `SAXLocalNameCountMethod` 中的以下代码完成的。

```
public class SAXLocalNameCount {
    private static String convertToFileURL(String filename) {
        String path = new File(filename).getAbsolutePath();
        if (File.separatorChar != '/') {
            path = path.replace(File.separatorChar, '/');
        }

        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        return "file:" + path;
    }

    // ...
}

```

如果在程序运行时指定了错误的命令行参数，则调用 `SAXLocalNameCount` 应用程序的`用法（）`方法，以在屏幕上打印出正确的选项。

```
private static void usage() {
    System.err.println("Usage: SAXLocalNameCount <file.xml>");
    System.err.println("       -usage or -help = this message");
    System.exit(1);
}

```

进一步的`用法（）`选项将在本课程的后面部分进行检查。

## 实现 `ContentHandler` 接口

`SAXLocalNameCount` 中最重要的接口是 `ContentHandler` 。此接口需要 SAX 解析器为响应各种解析事件而调用的许多方法。主要的事件处理方法是： `startDocument` ， `endDocument` ， `startElement` 和 `endElement` 。

实现此接口的最简单方法是扩展 `org.xml.sax.helpers` 包中定义的 `DefaultHandler` 类。该类为所有 `ContentHandler` 事件提供无操作方法。示例程序扩展了该类。

```
public class SAXLocalNameCount extends DefaultHandler {
    // ...
} 

```

* * *

**注 -** `DefaultHandler` 也为 `DTDHandler` ， `EntityResolver` 和`中定义的其他主要事件定义无操作方法 ErrorHandler` 接口。您将在本课程后面了解有关这些方法的更多信息。

* * *

抛出 `SAXException` 的接口需要这些方法中的每一种。此处抛出的异常将发送回解析器，解析器将其发送到调用解析器的代码。

## 处理内容事件

本节显示处理 `ContentHandler` 事件的代码。

遇到开始标记或结束标记时，标记的名称将作为 String 传递给 `startElement` 或 `endElement` 方法（如果适用）。遇到开始标记时，它定义的任何属性也会在 `Attributes` 列表中传递。在元素中找到的字符将作为字符数组以及字符数（长度）和指向第一个字符的数组中的偏移量传递。

### 文件活动

以下代码处理启动文档和结束文档事件：

```
public class SAXLocalNameCount extends DefaultHandler {

    private Hashtable tags;

    public void startDocument() throws SAXException {
        tags = new Hashtable();
    }

    public void endDocument() throws SAXException {
        Enumeration e = tags.keys();
        while (e.hasMoreElements()) {
            String tag = (String)e.nextElement();
            int count = ((Integer)tags.get(tag)).intValue();
            System.out.println("Local Name \"" + tag + "\" occurs " 
                               + count + " times");
        }    
    }

    private static String convertToFileURL(String filename) {
        // ...
    }

    // ...
}

```

此代码定义了解析器遇到正在解析的文档的起点和终点时应用程序执行的操作。 `ContentHandler` 接口的 `startDocument（）`方法创建 `java.util.Hashtable` 实例，在[元素事件](#gclmm)中将填充 XML 解析器在文档中找到的元素。当解析器到达文档的末尾时，调用 `endDocument（）`方法，以获取哈希表中包含的元素的名称和计数，并在屏幕上打印出一条消息，告诉用户如何发现了每种元素的许多发生率。

这两个 `ContentHandler` 方法都抛出 `SAXException` s。您将在[设置错误处理](#gcnsr)中了解有关 SAX 异常的更多信息。

### 元素事件

如[文档事件](#gclmb)中所述，由 `startDocument` 方法创建的哈希表需要填充解析器在文档中找到的各种元素。以下代码处理 start-element 和 end-element 事件：

```
public void startDocument() throws SAXException {
    tags = new Hashtable();
}

public void startElement(String namespaceURI,
                         String localName,
                         String qName, 
                         Attributes atts)
    throws SAXException {

    String key = localName;
    Object value = tags.get(key);

    if (value == null) {
        tags.put(key, new Integer(1));
    } 
    else {
        int count = ((Integer)value).intValue();
        count++;
        tags.put(key, new Integer(count));
    }
}

public void endDocument() throws SAXException {
    // ...
}

```

此代码处理元素标记，包括在 start 标记中定义的任何属性，以获取名称空间通用资源标识符（URI），本地名称和该元素的限定名称。然后 `startElement（）`方法为每种类型的元素填充由 `startDocument（）`创建的哈希映射，其中包含本地名称及其计数。请注意，当调用 `startElement（）`方法时，如果未启用名称空间处理，则元素和属性的本地名称可能会变为空字符串。只要简单名称是空字符串，代码就会使用限定名称来处理该情况。

### 字符事件

JAXP SAX API 还允许您使用 `ContentHandler.characters（）`方法处理解析器提供给您的应用程序的字符。

* * *

**注 -** 字符事件未在 `SAXLocalNameCount` 示例中演示，但为了完整性，本节中包含简要说明。

* * *

解析器不需要一次返回任何特定数量的字符。解析器可以一次从单个字符返回任何内容，但仍然是符合标准的实现。因此，如果您的应用程序需要处理它看到的字符，那么让`字符（）`方法在 `java.lang.StringBuffer` 中累积字符是明智的，并且只在你确定已找到所有这些。

在元素结束时完成解析文本，因此您通常在该点执行字符处理。但是您可能还希望在元素启动时处理文本。这对于文档样式数据是必需的，文档样式数据可以包含与文本混合的 XML 元素。例如，请考虑以下文档片段：

`**&lt; para&gt;** 此段包含**&lt; bold&gt;** 重要**&lt; / bold&gt;** 的想法。 **&lt; / para&gt;**`

初始文本`本段包含`，由`&lt; bold&gt;的开头终止。` 元素。文本`重要`由结束标记终止，`&lt; / bold&gt;` ，以及最终文字，`的想法。` 由结束标记终止，`&lt; / para&gt;` 。

为了严格准确，字符处理程序应扫描&符号（&amp;）和左角括号字符（&lt;）并用字符串`&amp; amp;替换它们。` 或`&amp; lt;` ，视情况而定。这将在下一节中解释。

### 处理特殊字符

在 XML 中，实体是具有名称的 XML 结构（或纯文本）。按名称引用实体会导致将其插入到文档中以代替实体引用。要创建实体引用，请使用&符号和分号包围实体名称：

`&entityName;`

处理包含许多特殊字符的大块 XML 或 HTML 时，可以使用 CDATA 部分。 CDATA 部分的作用类似于`&lt; code&gt; ...&lt; / code&gt; HTML 中的`更是如此：CDATA 部分中的所有空格都很重要，其中的字符不会被解释为 XML。 CDATA 部分以`&lt;！[[CDATA [`并以`]结束&gt;开始。` 。

CDATA 部分的示例，取自示例 XML 文件 _install-dir_ `/ jaxp-1_4_2-` _ 发布日期 _ `/ samples / data / REC-xml-19980210.xml` 如下所示。

`&lt;p&gt;&lt;termdef id="dt-cdsection" term="CDATA Section"&lt;&lt;term&gt;CDATA sections&lt;/term&gt; may occur anywhere character data may occur; they are used to escape blocks of text containing characters which would otherwise be recognized as markup. CDATA sections begin with the string "&lt;code&gt;&lt;![CDATA[&lt;/code&gt;" and end with the string "&lt;code&gt;]]&gt;&lt;/code&gt;"`

解析后，此文本将显示如下：

CDATA 部分可能出现在可能出现字符数据的任何地方它们用于转义包含字符的文本块，否则这些字符将被识别为标记。 CDATA 部分以字符串“`&lt;！[CDATA [`”开头，以字符串“`]]&gt;` ”结束。

CDATA 的存在使得 XML 的正确回应有点棘手。如果要输出的文本不在 CDATA 部分中，则文本中的任何尖括号，&符号和其他特殊字符都应替换为相应的实体引用。 （更换左尖括号和&符号是最重要的，其他字符将被正确解释而不会误导解析器。）但是如果输出文本在 CDATA 部分中，则不应发生替换，从而产生类似前面示例中的文本。在一个简单的程序中，例如我们的 `SAXLocalNameCount` 应用程序，这并不是特别严重。但是许多 XML 过滤应用程序需要跟踪文本是否出现在 CDATA 部分中，以便它们可以正确处理特殊字符。

## 设置解析器

以下代码设置解析器并启动它：

```
static public void main(String[] args) throws Exception {

    // Code to parse command-line arguments 
    //(shown above)
    // ...

    SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    SAXParser saxParser = spf.newSAXParser();
}

```

这些代码行创建 `SAXParserFactory` 实例，由 `javax.xml.parsers.SAXParserFactory` 系统属性的设置决定。要创建的工厂设置为通过将 `setNamespaceAware` 设置为 true 来支持 XML 命名空间，然后通过调用其 `newSAXParser（）从工厂获取 `SAXParser` 实例`方法。

* * *

**注 -** `javax.xml.parsers.SAXParser` 类是一个定义了许多便捷方法的包装器。它包装（稍微不那么友好） `org.xml.sax.Parser` 对象。如果需要，您可以使用 `SAXParser` 类的 `getParser（）`方法获取该解析器。

* * *

您现在需要实现所有解析器必须实现的 `XMLReader` 。应用程序使用 `XMLReader` 告诉 SAX 解析器对相关文档执行哪些处理。 `XMLReader` 由 `main` 方法中的以下代码实现。

```
// ...
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(new SAXLocalNameCount());
xmlReader.parse(convertToFileURL(filename));

```

在这里，通过调用 `SAXParser` 实例的 `getXMLReader（）`方法，为解析器获取 `XMLReader` 实例。然后 `XMLReader` 将 `SAXLocalNameCount` 类注册为其内容处理程序，以便解析器执行的操作将是 `startDocument（）`，`的操作。 [处理内容事件](#gclnc)中显示的 startElement（）`和 `endDocument（）`方法。最后， `XMLReader` 以 `convertToFileURL 生成的 `File` URL 的形式告诉解析器通过传递相关 XML 文件的位置来解析哪个文档（ ）` [设置 I / O](#gcnsk) 中定义的方法。

## 设置错误处理

您现在可以开始使用解析器，但实现一些错误处理会更安全。解析器可以生成三种错误：致命错误，错误和警告。发生致命错误时，解析器无法继续。因此，如果应用程序不生成异常，则默认的错误事件处理程序会生成一个异常。但是对于非致命错误和警告，默认错误处理程序永远不会生成异常，也不会显示任何消息。

如[文档事件](#gclmb)所示，应用程序的事件处理方法抛出 `SAXException` 。例如， `ContentHandler` 接口中 `startDocument（）`方法的签名被定义为返回 `SAXException` 。

`public void startDocument() throws SAXException { /* ... */ }`

可以使用消息，另一个例外或两者来构造 `SAXException` 。

因为默认解析器只生成致命错误的异常，并且由于默认解析器提供的错误信息有限， `SAXLocalNameCount` 程序通过 `MyErrorHandler` 定义自己的错误处理。 ]班。

```
xmlReader.setErrorHandler(new MyErrorHandler(System.err));

// ...

private static class MyErrorHandler implements ErrorHandler {
    private PrintStream out;

    MyErrorHandler(PrintStream out) {
        this.out = out;
    }

    private String getParseExceptionInfo(SAXParseException spe) {
        String systemId = spe.getSystemId();

        if (systemId == null) {
            systemId = "null";
        }

        String info = "URI=" + systemId + " Line=" 
            + spe.getLineNumber() + ": " + spe.getMessage();

        return info;
    }

    public void warning(SAXParseException spe) throws SAXException {
        out.println("Warning: " + getParseExceptionInfo(spe));
    }

    public void error(SAXParseException spe) throws SAXException {
        String message = "Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }

    public void fatalError(SAXParseException spe) throws SAXException {
        String message = "Fatal Error: " + getParseExceptionInfo(spe);
        throw new SAXException(message);
    }
}

```

以与[设置解析器](#gclmt)相同的方式，显示 `XMLReader` 指向正确的内容处理程序，这里 `XMLReader` 指向新的错误处理程序通过调用其 `setErrorHandler（）`方法。

`MyErrorHandler` 类实现标准 `org.xml.sax.ErrorHandler` 接口，并定义一种方法来获取由生成的任何 `SAXParseException` 实例提供的异常信息由解析器。这个方法， `getParseExceptionInfo（）`，通过调用标准 `SAXParseException` 方法，只需获取 XML 文档中发生错误的行号和运行它的系统的标识符 `getLineNumber（）`和 `getSystemId（）`。然后将此异常信息反馈到基本 SAX 错误处理方法`错误（）`，`警告（）`和 `fatalError（）`的实现中，这些实现更新为发送有关文档中错误的性质和位置的相应消息。

### 处理非致命错误

当 XML 文档未通过有效性约束时，会发生非致命错误。如果解析器发现文档无效，则会生成错误事件。给定文档类型定义（DTD）或模式时，如果文档具有无效标记，则在发现不允许的标记时，或者（在模式的情况下）时，由验证解析器生成此类错误。元素包含无效数据。

理解非致命错误的最重要原则是默认情况下会忽略它们。但是，如果文档中出现验证错误，您可能不希望继续处理它。您可能希望将此类错误视为致命错误。

要接管错误处理，请覆盖 `DefaultHandler` 方法，这些方法处理致命错误，非致命错误和警告，作为 `ErrorHandler` 接口的一部分。如上一节中的代码提取所示，SAX 解析器为每个方法提供 `SAXParseException` ，因此在发生错误时生成异常就像将其抛回一样简单。

* * *

**注 -** 检查 `org.xml.sax.helpers.DefaultHandler` 中定义的错误处理方法可能很有帮助。你会看到`错误（）`和`警告（）`方法什么都不做，而 `fatalError（）`会抛出异常。当然，你总是可以覆盖 `fatalError（）`方法来抛出不同的异常。但是如果您的代码在发生致命错误时没有抛出异常，那么 SAX 解析器将会。 XML 规范需要它。

* * *

### 处理警告

默认情况下也会忽略警告。警告是提供信息的，只能在存在 DTD 或架构的情况下生成。例如，如果在 DTD 中定义了两次元素，则会生成警告。它不是非法的，它不会引起问题，但它可能是你想知道的，因为它可能不是故意的。将针对 DTD 验证 XML 文档将在本节中显示。

## 在没有验证的情况下运行 SAX Parser 示例

如本课程开头所述，从 [JAXP 源代码下载区](http://jaxp.java.net/downloads.html)下载并安装 JAXP API 的源代码后，可以在下面找到运行它的示例程序和相关文件。位置。

*   该示例的不同 Java 归档（JAR）文件位于目录 _install-dir_ `/ jaxp-1_4_2-` _ 发布日期 _ `/ lib` 。

*   `SAXLocalNameCount.java` 文件位于 _install-dir_ `/ jaxp-1_4_2-` _ 发布日期 _ `/ samples / sax` 。

*   `SAXLocalNameCount` 与之交互的 XML 文件可在 _install-dir_ `/ jaxp-1_4_2-` _ 发布日期 _ `/样本中找到/ data` 。

以下步骤说明如何在未经验证的情况下运行 SAX 解析器示例。

### 在没有验证的情况下运行 `SAXLocalNameCount` 示例

1.  **导航至`样本`目录。** `％cd _install-dir_ `/ jaxp-1_4_2-` _ 释放日期 _ `/样品`。`
2.  **编译示例类。** `% javac sax/*`
3.  **Run the `SAXLocalNameCount` program on an XML file.**

    选择`数据`目录中的一个 XML 文件，然后在其上运行 `SAXLocalNameCount` 程序。在这里，我们选择在文件 `rich_iii.xml` 上运行程序。

    `% java sax/SAXLocalNameCount data/rich_iii.xml`

    XML 文件 `rich_iii.xml` 包含威廉·莎士比亚戏剧 _Richard III_ 的 XML 版本。当您在其上运行 `SAXLocalNameCount` 时，您应该看到以下输出。

    ```
    Local Name "STAGEDIR" occurs 230 times
    Local Name "PERSONA" occurs 39 times
    Local Name "SPEECH" occurs 1089 times
    Local Name "SCENE" occurs 25 times
    Local Name "ACT" occurs 5 times
    Local Name "PGROUP" occurs 4 times
    Local Name "PLAY" occurs 1 times
    Local Name "PLAYSUBT" occurs 1 times
    Local Name "FM" occurs 1 times
    Local Name "SPEAKER" occurs 1091 times
    Local Name "TITLE" occurs 32 times
    Local Name "GRPDESCR" occurs 4 times
    Local Name "P" occurs 4 times
    Local Name "SCNDESCR" occurs 1 times
    Local Name "PERSONAE" occurs 1 times
    Local Name "LINE" occurs 3696 times

    ```

    `SAXLocalNameCount` 程序解析 XML 文件，并提供它包含的每种 XML 标记的实例数量的计数。

4.  **Open the file `data/rich_iii.xml` in a text editor.**

    要检查错误处理是否正常，请从 XML 文件中的条目中删除结束标记，例如结束标记`&lt; / PERSONA&gt;` ，来自第 30 行，如下所示。

    `30 &lt;PERSONA&gt;EDWARD, Prince of Wales, afterwards King Edward V.&lt;/PERSONA&gt;`
5.  **Run `SAXLocalNameCount` again.**

    这一次，您应该看到以下致命错误消息。

    `% java sax/SAXLocalNameCount data/rich_iii.xml Exception in thread "main" org.xml.sax.SAXException: Fatal Error: URI=file:_install-dir_ /JAXP_sources/jaxp-1_4_2-_release-date_/samples/data/rich_iii.xml Line=30: The element type "PERSONA" must be terminated by the matching end-tag "&lt;/PERSONA&gt;".`

    如您所见，当遇到错误时，解析器生成 `SAXParseException` ， `SAXException` 的子类，用于标识文件和发生错误的位置。