使用 SAX 解析 XML 文件

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


注意-JAXP 下载专区下载并安装 JAXP API 的源之后,可以在目录 install-dir /jaxp\-1_4_2\- release-date /samples/sax中找到该示例的示例程序。与之交互的 XML 文件位于 install-dir /jaxp\-1_4_2\- release-date /samples/data中。


创建骨架

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

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

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

Importing Classes

应用程序将使用的类的 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 解析器的所有interface。 org\.xml\.sax\.helpers程序包包含DefaultHandler,它定义了将处理解析器生成的 SAX 事件的类。 java\.utiljava\.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();
    } 
}

这段代码设置了遇到问题时抛出Exception的主要方法,并定义了告诉应用程序要处理的 XML 文件名所需的命令行选项。在本节的稍后部分,当我们开始研究验证时,将检查代码这一部分中的其他命令行参数。

您在运行应用程序时提供的filename字符串 将通过内部方法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应用程序的usage\(\)方法,以在屏幕上打印出正确的选项。

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

解决验证问题后,还将在本类的后面部分讨论其他usage\(\)个选项。

实现 ContentHandler interface

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

实现此interface的最简单方法是扩展org\.xml\.sax\.helpers包中定义的DefaultHandler类。该类为所有ContentHandler事件提供了什么都不做的方法。示例程序扩展了该类。

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

注意- DefaultHandler还为其他主要事件定义了不执行任何操作的方法,这些方法在DTDHandlerEntityResolverErrorHandlerinterface中定义。在本类的后面,您将学到更多有关这些方法的信息。


interface需要这些方法中的每一个都引发SAXException。此处引发的异常被发送回解析器,解析器将其发送到调用解析器的代码。

处理内容事件

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

遇到开始标签或结束标签时,标签的名称将作为字符串 适当地传递给startElementendElement方法。遇到开始标记时,它定义的所有属性也会在Attributes列表中传递。在元素中找到的字符将作为字符数组以及字符数(Long 度)和指向第一个字符的offset量作为数组传递。

Document Events

以下代码处理开始文档和结束文档事件:

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) {
        // ...
    }

    // ...
}

此代码定义当解析器遇到要解析的文档的起点和 endpoints 时应用程序将执行的操作。 ContentHandlerinterface的startDocument\(\)方法创建一个java\.util\.Hashtable实例,该实例在Element Events中将填充解析器在文档中找到的 XML 元素。当解析器到达文档末尾时,将调用endDocument\(\)方法,以获取哈希表中包含的元素的名称和计数,并在屏幕上打印出一条消息,告诉用户发现了每个元素多少次。

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

Element Events

Document Events中提到的那样,需要使用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 {
    // ...
}

此代码处理元素标签,包括在开始标签中定义的任何属性,以获得该元素的名称空间通用资源标识符(URI),本地名称和限定名称。然后,startElement\(\)方法针对每种类型的元素,使用本地名称及其计数填充由startDocument\(\)创建的哈希图。请注意,在调用startElement\(\)方法时,如果未启用名称空间处理,则元素和属性的本地名称可能变为空字符串。只要简单名称为空字符串,代码就会使用限定名称来处理这种情况。

Character Events

JAXP SAX API 还允许您使用ContentHandler\.characters\(\)方法处理解析器传递给应用程序的字符。


注意- SAXLocalNameCount示例中未演示字符事件,但是为了完整起见,本节中包含简短描述。


解析器不需要一次返回任何特定数量的字符。解析器一次最多可以返回单个字符中的任何内容,并且仍然是符合标准的实现。因此,如果您的应用程序需要处理看到的字符,明智的做法是让characters\(\)方法在java\.lang\.StringBuffer中累积字符并仅在确定已找到所有字符时对其进行操作。

您在元素结束时完成了文本的解析,因此通常在这一点上执行字符处理。但是,您可能还想在元素开始时处理文本。这对于文档样式的数据是必需的,其中可以包含与文本混合在一起的 XML 元素。例如,考虑以下文档片段:

<para>This paragraph contains <bold>important</bold> ideas.</para>

初始文本This paragraph contains\<bold\>元素的开头终止。文本important由结束标记\</bold\>终止,而final文本ideas\.由结束标记\</para\>终止。

为了严格准确,字符处理程序应扫描&字符(&)和左尖括号字符(<),并根据需要用字符串&amp;&lt;替换它们。下一节将对此进行说明。

处理特殊字符

在 XML 中,实体是具有名称的 XML 结构(或纯文本)。通过名称引用实体会导致它代替实体引用插入到文档中。要创建实体引用,请用“&”号和分号将实体名称括起来:

&entityName;

当处理包含许多特殊字符的 XML 或 HTML 大块时,可以使用 CDATA 节。 CDATA 节在 HTML 中的作用类似于\<code\>\.\.\.\</code\>,但更多的是:CDATA 节中的所有空白都是有效的,并且其中的字符不会解释为 XML。 CDATA 节以\<!\[\[CDATA\[开始,以\]\]\>结尾。

下面显示了一个 CDATA 节的示例,该节取自示例 XML 文件 install-dir /jaxp\-1_4_2\- release-date /samples/data/REC\-xml\-19980210\.xml

<p><termdef id="dt-cdsection" term="CDATA Section"<<term>CDATA sections</term> 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 "<code>&lt;![CDATA[</code>" and end with the string "<code>]]&gt;</code>"

解析后,该文本将显示如下:

CDATA 节可能出现在任何可能出现字符数据的地方;它们用于转义包含字符的文本块,否则这些字符将被视为标记。 CDATA 部分以字符串“ \<!\[CDATA\[”开头,以字符串“ \]\]\>”结尾。

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\(\)startElement\(\)endDocument\(\)方法的动作。最后,XMLReader通过将要讨论的 XML 文件的位置(以设置 I/O中定义的convertToFileURL\(\)方法生成的File URL 的形式)传递给解析器,从而告知解析器要解析的文档。

设置错误处理

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

Document Events所示,应用程序的事件处理方法抛出SAXException。例如,ContentHandlerinterface中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);
    }
}

以与设置解析器相同的方式显示设置解析器指向正确的内容处理程序,在这里XMLReader通过调用其setErrorHandler\(\)方法指向新的错误处理程序。

MyErrorHandler类实现标准的org\.xml\.sax\.ErrorHandlerinterface,并定义一种方法来获取由解析器生成的任何SAXParseException实例提供的异常信息。该方法getParseExceptionInfo\(\)通过调用标准SAXParseException方法getLineNumber\(\)getSystemId\(\)来简单地获取 XML 文档中发生错误的行号以及运行该系统的系统的标识符。然后,将此异常信息馈入基本 SAX 错误处理方法error\(\)warning\(\)fatalError\(\)的实现中,这些方法已更新为发送有关错误性质和位置的适当消息。

处理非致命错误

当 XML 文档未通过有效性约束时,将发生非致命错误。如果解析器发现文档无效,那么将生成错误事件。如果给定文档类型定义(DTD)或架构,则当文档具有无效标签时,如果在不允许的位置找到标签,或者当文档具有无效标签时(在架构的情况下),此类错误将由验证解析器生成。元素包含无效数据。

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

要接管错误处理,请覆盖DefaultHandler方法,这些方法将致命错误,非致命错误和警告作为ErrorHandlerinterface的一部分进行处理。如上一节中的代码摘录所示,SAX 解析器将SAXParseException传递给这些方法中的每一个,因此在发生错误时生成异常就像将其扔回一样简单。


注意- 检查org\.xml\.sax\.helpers\.DefaultHandler中定义的错误处理方法可能很有帮助。您将看到error\(\)warning\(\)方法什么也不做,而fatalError\(\)引发异常。当然,您始终可以覆盖fatalError\(\)方法以引发其他异常。但是,如果发生致命错误时代码没有引发异常,则 SAX 解析器将。 XML 规范需要它。


Handling Warnings

默认情况下,也会忽略警告。警告是信息性的,只能在存在 DTD 或 Pattern 的情况下生成。例如,如果某个元素在 DTD 中定义了两次,则会生成警告。它不是非法的,并且不会引起问题,但是您可能想知道这件事,因为它可能不是故意的。本节将显示根据 DTD 验证 XML 文档。

运行未经验证的 SAX 分析器示例

如本课开始所述,从JAXP 源码下载区下载并安装 JAXP API 的源之后,可以在以下位置找到示例程序和运行它所需的相关文件。

  • 该示例的不同 Java 归档(JAR)文件位于目录 install-dir /jaxp\-1_4_2\- release-date /lib中。

  • SAXLocalNameCount\.java文件位于 install-dir /jaxp\-1_4_2\- release-date /samples/sax中。

  • SAXLocalNameCount与之交互的 XML 文件位于 install-dir /jaxp\-1_4_2\- release-date /samples/data中。

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

运行未经验证的 SAXLocalNameCount 示例

  • 导航到samples目录. % cd install-dir/jaxp-1_4_2-release-date/samples.

  • 编译示例类. % javac sax/*

  • 在 XML 文件上运行SAXLocalNameCount程序.

data目录中选择一个 XML 文件,然后在其上运行SAXLocalNameCount程序。在这里,我们选择在文件rich_iii\.xml上运行程序。

% java sax/SAXLocalNameCount data/rich_iii.xml

XML 文件rich_iii\.xml包含 William Shakespeare 的戏剧 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 标签类型的实例数。

  • 在文本编辑器中打开文件data/rich_iii\.xml.

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

30 <PERSONA>EDWARD, Prince of Wales, afterwards King Edward V.</PERSONA>

  • 再次运行SAXLocalNameCount.

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

% 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 "</PERSONA>".

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