使用 SAX 解析 XML 文件

原文: https://docs.oracle.com/javase/tutorial/jaxp/sax/parsing.html

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


:从 JAXP 下载区下载并安装了 JAXP API 的源代码后,本示例的示例程序可在 install-dir 目录中找到 / jaxp-1_4_2- 释放日期 /样例/ sax 。与其交互的 XML 文件可在 install-dir / jaxp-1_4_2- 发布日期 /样例/数据中找到。


创建骨架

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

  1. public class SAXLocalNameCount {
  2. static public void main(String[] args) {
  3. // ...
  4. }
  5. }

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

导入类

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

  1. package sax;
  2. import javax.xml.parsers.*;
  3. import org.xml.sax.*;
  4. import org.xml.sax.helpers.*;
  5. import java.util.*;
  6. import java.io.*;
  7. public class SAXLocalNameCount {
  8. // ...
  9. }

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

设置 I / O.

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

  1. static public void main(String[] args) throws Exception {
  2. String filename = null;
  3. for (int i = 0; i < args.length; i++) {
  4. filename = args[i];
  5. if (i != args.length - 1) {
  6. usage();
  7. }
  8. }
  9. if (filename == null) {
  10. usage();
  11. }
  12. }

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

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

  1. public class SAXLocalNameCount {
  2. private static String convertToFileURL(String filename) {
  3. String path = new File(filename).getAbsolutePath();
  4. if (File.separatorChar != '/') {
  5. path = path.replace(File.separatorChar, '/');
  6. }
  7. if (!path.startsWith("/")) {
  8. path = "/" + path;
  9. }
  10. return "file:" + path;
  11. }
  12. // ...
  13. }

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

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

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

实现ContentHandler接口

SAXLocalNameCount中最重要的接口是ContentHandler 。此接口需要 SAX 解析器为响应各种解析事件而调用的许多方法。主要的事件处理方法是: startDocumentendDocumentstartElementendElement

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

  1. public class SAXLocalNameCount extends DefaultHandler {
  2. // ...
  3. }

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


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

处理内容事件

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

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

文件活动

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

  1. public class SAXLocalNameCount extends DefaultHandler {
  2. private Hashtable tags;
  3. public void startDocument() throws SAXException {
  4. tags = new Hashtable();
  5. }
  6. public void endDocument() throws SAXException {
  7. Enumeration e = tags.keys();
  8. while (e.hasMoreElements()) {
  9. String tag = (String)e.nextElement();
  10. int count = ((Integer)tags.get(tag)).intValue();
  11. System.out.println("Local Name \"" + tag + "\" occurs "
  12. + count + " times");
  13. }
  14. }
  15. private static String convertToFileURL(String filename) {
  16. // ...
  17. }
  18. // ...
  19. }

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

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

元素事件

文档事件中所述,由startDocument方法创建的哈希表需要填充解析器在文档中找到的各种元素。以下代码处理 start-element 和 end-element 事件:

  1. public void startDocument() throws SAXException {
  2. tags = new Hashtable();
  3. }
  4. public void startElement(String namespaceURI,
  5. String localName,
  6. String qName,
  7. Attributes atts)
  8. throws SAXException {
  9. String key = localName;
  10. Object value = tags.get(key);
  11. if (value == null) {
  12. tags.put(key, new Integer(1));
  13. }
  14. else {
  15. int count = ((Integer)value).intValue();
  16. count++;
  17. tags.put(key, new Integer(count));
  18. }
  19. }
  20. public void endDocument() throws SAXException {
  21. // ...
  22. }

此代码处理元素标记,包括在 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;**

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

为了严格准确,字符处理器应扫描&符号(&)和左角括号字符(<)并用字符串&amp; amp;替换它们。&amp; lt; ,视情况而定。这将在下一节中解释。

处理特殊字符

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

&entityName;

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

CDATA 部分的示例,取自示例 XML 文件 install-dir / jaxp-1_4_2- 发布日期 / samples / data / REC-xml-19980210.xml如下所示。

<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 部分以字符串“<![CDATA [”开头,以字符串“]]> ”结束。

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

设置解析器

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

  1. static public void main(String[] args) throws Exception {
  2. // Code to parse command-line arguments
  3. //(shown above)
  4. // ...
  5. SAXParserFactory spf = SAXParserFactory.newInstance();
  6. spf.setNamespaceAware(true);
  7. SAXParser saxParser = spf.newSAXParser();
  8. }

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


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


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

  1. // ...
  2. SAXParser saxParser = spf.newSAXParser();
  3. XMLReader xmlReader = saxParser.getXMLReader();
  4. xmlReader.setContentHandler(new SAXLocalNameCount());
  5. xmlReader.parse(convertToFileURL(filename));

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

设置错误处理

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

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

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

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

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

  1. xmlReader.setErrorHandler(new MyErrorHandler(System.err));
  2. // ...
  3. private static class MyErrorHandler implements ErrorHandler {
  4. private PrintStream out;
  5. MyErrorHandler(PrintStream out) {
  6. this.out = out;
  7. }
  8. private String getParseExceptionInfo(SAXParseException spe) {
  9. String systemId = spe.getSystemId();
  10. if (systemId == null) {
  11. systemId = "null";
  12. }
  13. String info = "URI=" + systemId + " Line="
  14. + spe.getLineNumber() + ": " + spe.getMessage();
  15. return info;
  16. }
  17. public void warning(SAXParseException spe) throws SAXException {
  18. out.println("Warning: " + getParseExceptionInfo(spe));
  19. }
  20. public void error(SAXParseException spe) throws SAXException {
  21. String message = "Error: " + getParseExceptionInfo(spe);
  22. throw new SAXException(message);
  23. }
  24. public void fatalError(SAXParseException spe) throws SAXException {
  25. String message = "Fatal Error: " + getParseExceptionInfo(spe);
  26. throw new SAXException(message);
  27. }
  28. }

以与设置解析器相同的方式,显示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 源代码下载区下载并安装 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-14_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时,您应该看到以下输出。

    1. Local Name "STAGEDIR" occurs 230 times
    2. Local Name "PERSONA" occurs 39 times
    3. Local Name "SPEECH" occurs 1089 times
    4. Local Name "SCENE" occurs 25 times
    5. Local Name "ACT" occurs 5 times
    6. Local Name "PGROUP" occurs 4 times
    7. Local Name "PLAY" occurs 1 times
    8. Local Name "PLAYSUBT" occurs 1 times
    9. Local Name "FM" occurs 1 times
    10. Local Name "SPEAKER" occurs 1091 times
    11. Local Name "TITLE" occurs 32 times
    12. Local Name "GRPDESCR" occurs 4 times
    13. Local Name "P" occurs 4 times
    14. Local Name "SCNDESCR" occurs 1 times
    15. Local Name "PERSONAE" occurs 1 times
    16. Local Name "LINE" occurs 3696 times

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

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

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

    30 &lt;PERSONA&gt;EDWARD, Prince of Wales, afterwards King Edward V.&lt;/PERSONA>

  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;".

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