Java 中文官方教程 2022 版(三十八)(1)https://developer.aliyun.com/article/1488147
处理词法事件
到目前为止,您已经消化了许多 XML 概念,包括 DTD 和外部实体。您还学会了如何使用 SAX 解析器。本课程的其余部分涵盖了您只有在编写基于 SAX 的应用程序时才需要理解的高级主题。如果您的主要目标是编写基于 DOM 的应用程序,您可以直接跳转到文档对象模型。
您之前看到,如果您将文本写出为 XML,您需要知道是否处于 CDATA 部分中。如果是,则尖括号(<)和和号(&)应保持不变输出。但如果不在 CDATA 部分中,则应将它们替换为预定义的实体<
和&
。但是您如何知道自己是否在处理 CDATA 部分?
另一方面,如果您以某种方式过滤 XML,您希望传递注释。通常解析器会忽略注释。您如何获取注释以便可以回显它们?
本节回答了这些问题。它向您展示了如何使用org.xml.sax.ext.LexicalHandler
来识别注释、CDATA 部分和对解析实体的引用。
注释、CDATA 标记和对解析实体的引用构成词法信息-即,涉及 XML 文本本身而不是 XML 信息内容的信息。当然,大多数应用程序只关注 XML 文档的内容。这些应用程序将不使用LexicalEventListener
API。但是输出 XML 文本的应用程序会发现它非常有价值。
注意 - 词法事件处理是一个可选的解析器功能。解析器实现不需要支持它。(参考实现是这样的。)本讨论假定您的解析器支持它。
LexicalHandler
的工作原理
要在 SAX 解析器看到词法信息时得到通知,您需要使用LexicalHandler
配置解析器底层的XmlReader
。LexicalHandler
接口定义了以下事件处理方法。
comment(String comment)
将注释传递给应用程序。
startCDATA()
, endCDATA()
告诉您 CDATA 部分何时开始和结束,这告诉您的应用程序下次调用characters()
时可以期望什么样的字符。
startEntity(String name)
, endEntity(String name)
给出解析实体的名称。
startDTD(String name, String publicId, String systemId)
, endDTD()
告诉您正在处理 DTD,并标识它。
要激活词法处理程序,您的应用程序必须扩展DefaultHandler
并实现LexicalHandler
接口。然后,您必须配置您的XMLReader
实例,使解析器委托给它,并配置它将词法事件发送到您的词法处理程序,如下所示。
// ... SAXParser saxParser = factory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler); // ...
在这里,您可以使用XMLReader
类中定义的setProperty()
方法来配置XMLReader
。作为 SAX 标准的一部分定义的属性名称是 URN,http://xml.org/sax/properties/lexical-handler
。
最后,添加类似以下代码来定义将实现接口的适当方法。
// ... public void warning(SAXParseException err) { // ... } public void comment(char[] ch, int start, int length) throws SAXException { // ... } public void startCDATA() throws SAXException { // ... } public void endCDATA() throws SAXException { // ... } public void startEntity(String name) throws SAXException { // ... } public void endEntity(String name) throws SAXException { // ... } public void startDTD(String name, String publicId, String systemId) throws SAXException { // ... } public void endDTD() throws SAXException { // ... } private void echoText() { // ... } // ...
这段代码将把您的解析应用程序转换为一个词法处理程序。剩下的就是为这些新方法中的每一个指定一个要执行的操作。
使用 DTDHandler 和 EntityResolver
本节介绍了另外两个 SAX 事件处理程序:DTDHandler
和EntityResolver
。当 DTD 遇到未解析的实体或符号声明时,将调用DTDHandler
。当需要将 URN(公共 ID)解析为 URL(系统 ID)时,将使用EntityResolver
。
DTDHandler
API
选择解析器实现展示了引用包含二进制数据(如图像文件)的文件的方法,使用 MIME 数据类型。这是最简单、最可扩展的机制。但是,为了与旧的 SGML 样式数据兼容,也可以定义未解析的实体。
NDATA
关键字定义了一个未解析的实体:
NDATA
关键字表示此实体中的数据不是可解析的 XML 数据,而是使用其他符号的数据。在本例中,符号被命名为gif
。然后 DTD 必须包含该符号的声明,类似于以下内容。
当解析器看到未解析的实体或符号声明时,除了将其传递给应用程序使用DTDHandler
接口外,它不会对信息做任何处理。该接口定义了两个方法。
notationDecl(String name, String publicId, String systemId)
unparsedEntityDecl(String name, String publicId, String systemId, String notationName
notationDecl
方法传递符号的名称和公共或系统标识符,或两者,取决于 DTD 中声明了哪个。unparsedEntityDecl
方法传递实体的名称、适当的标识符和它使用的符号的名称。
注意 - DTDHandler
接口由DefaultHandler
类实现。
符号也可以用于属性声明。例如,以下声明需要 GIF 和 PNG 图像文件格式的符号。
<!ENTITY image EMPTY> <!ATTLIST image ... type NOTATION (gif | png) "gif">
在这里,类型声明为 gif 或 png。如果没有指定,则默认为 gif。
无论符号引用用于描述未解析的实体还是属性,都由应用程序进行适当处理。解析器对符号的语义一无所知。它只传递声明。
EntityResolver
API
EntityResolver
API 允许您将公共 ID(URN)转换为系统 ID(URL)。例如,您的应用程序可能需要将类似href="urn:/someName"
的内容转换为"http://someURL"
。
EntityResolver
接口定义了一个方法:
resolveEntity(String publicId, String systemId)
这种方法返回一个InputSource
对象,可以用来访问实体的内容。将 URL 转换为InputSource
很容易。但作为系统 ID 传递的 URL 很可能是原始文档的位置,而这个位置很可能在网络上的某个地方。要访问本地副本(如果有的话),必须在系统的某处维护一个目录,将名称(公共 ID)映射到本地 URL。
更多信息
以下链接提供了关于本课程中介绍的技术的进一步有用信息。
- 有关 SAX 标准的更多信息,请参见SAX 标准页面:
www.saxproject.org
. - 有关 StAX 拉解析器的更多信息,请参见:
Java 社区流程页面:jcp.org/en/jsr/detail?id=173
.
Elliot Rusty Harold 的介绍:www.xml.com/pub/a/2003/09/17/stax.html
. - 有关基于模式的验证机制的更多信息,请参见
W3C 标准验证机制,XML Schema:www.w3.org/XML/Schema
.
RELAX NG 的基于正则表达式的验证机制:
绿洲 Relax NG TC.
Schematron 基于断言的验证机制:www.ascc.net/xml/resource/schematron/schematron.html
.
课程:文档对象模型
这节课介绍了文档对象模型(DOM)。DOM 是一种标准的树结构,其中每个节点包含 XML 结构中的一个组件。最常见的节点类型是元素节点和文本节点。使用 DOM 函数可以创建节点,删除节点,更改它们的内容,并遍历节点层次结构。
这节课的示例演示了如何解析现有的 XML 文件以构建 DOM,显示和检查 DOM 层次结构,并探索命名空间的语法。它还展示了如何从头开始创建 DOM,并了解如何使用 Sun 的 JAXP 实现中的一些特定于实现的功能将现有数据集转换为 XML。
何时使用 DOM
文档对象模型标准首先是为文档(例如文章和书籍)设计的。此外,JAXP 1.4.2 实现支持 XML Schema,这对于任何特定应用程序都可能是一个重要考虑因素。
另一方面,如果您处理简单的数据结构,且 XML Schema 不是您计划的重要部分,那么您可能会发现更适合您目的的是 JDOM 或 dom4j 等更面向对象的标准之一。
从一开始,DOM 旨在是与语言无关的。由于它是为诸如 C 和 Perl 之类的语言设计的,DOM 并没有利用 Java 的面向对象特性。这一事实,加上文档和数据之间的区别,也有助于解释处理 DOM 与处理 JDOM 或 dom4j 结构之间的差异。
在本节中,我们将研究这些标准背后的模型之间的差异,以帮助您选择最适合您应用程序的标准。
文档与数据
DOM 中使用的文档模型与 JDOM 或 dom4j 中使用的数据模型之间的主要差异在于:
- 存在于层次结构中的节点类型
- 混合内容的能力
主要是数据层次结构中的“节点”构成的差异主要导致了使用这两种模型进行编程的差异。然而,与其他任何因素相比,混合内容的能力最能解释标准如何定义节点的差异。因此,我们首先来看一下 DOM 的混合内容模型。
混合内容模型
在 DOM 层次结构中,文本和元素可以自由混合。这种结构称为 DOM 模型中的混合内容。
文档中经常出现混合内容。例如,假设您想要表示这种结构:
这是一个重要的想法。
DOM 节点的层次结构可能如下所示,其中每行代表一个节点:
ELEMENT: sentence + TEXT: This is an + ELEMENT: bold + TEXT: important + TEXT: idea.
请注意,sentence 元素包含文本,然后是一个子元素,然后是额外的文本。文本和元素的混合定义了混合内容模型。
节点类型
为了提供混合内容的能力,DOM 节点本质上非常简单。在上述示例中,第一个元素的“内容”(其值)只是标识它是什么类型的节点。
第一次使用 DOM 的用户通常会被这个事实搞糊涂。在导航到节点后,他们要求节点的“内容”,并期望得到一些有用的东西。相反,他们只能找到元素的名称,
sentence
。
注意 - DOM 节点 API 定义了nodeValue()
、nodeType()
和nodeName()
方法。对于第一个元素节点,nodeName()
返回sentence
,而nodeValue()
返回 null。对于第一个文本节点,nodeName()
返回#text
,而nodeValue()
返回“This is an
”。重要的一点是,元素的值与其内容不同。
在上面的例子中,询问“句子”的“文本”是什么意思?根据您的应用程序,以下任何一种都可能是合理的:
- 这是一个
- 这是一个想法。
- 这是一个重要的想法。
- 这是一个重要的想法。
一个更简单的模型
使用 DOM,您可以自由创建所需的语义。但是,您还需要进行必要的处理以实现这些语义。另一方面,像 JDOM 和 dom4j 这样的标准使得执行简单任务变得更容易,因为层次结构中的每个节点都是一个对象。
尽管 JDOM 和 dom4j 允许元素具有混合内容,但它们并非主要设计用于这种情况。相反,它们针对的是 XML 结构包含数据的应用程序。
数据结构中的元素通常只包含文本或其他元素,而不是两者兼有。例如,这里是代表简单地址簿的一些 XML:
<addressbook> <entry> <name>Fred</name> <email>fred@home</email> </entry> ... </addressbook>
注意 - 对于像这样非常简单的 XML 数据结构,您还可以使用内置在 Java 平台 1.4 版本中的正则表达式包(java.util.regex
)。
在 JDOM 和 dom4j 中,当您导航到包含文本的元素后,您可以调用诸如text()
之类的方法来获取其内容。但是,在处理 DOM 时,您必须检查子元素列表以“组合”节点的文本,就像您之前看到的那样 - 即使该列表只包含一个项目(TEXT 节点)。
因此,对于简单的数据结构,比如地址簿,您可以通过使用 JDOM 或 dom4j 来节省一些工作量。即使数据在技术上是“混合的”,但在给定节点中始终只有一个(且仅有一个)文本段落时,使用其中一个模型可能是有意义的。
这是一个这种结构的示例,也可以很容易地在 JDOM 或 dom4j 中处理:
<addressbook> <entry>Fred <email>fred@home</email> </entry> ... </addressbook>
在这里,每个条目都有一些标识性文本,后面跟着其他元素。有了这种结构,程序可以导航到一个条目,调用text()
来找出它属于谁,并在正确的节点处处理子元素。
增加复杂性
但是,为了全面了解在搜索或操作 DOM 时需要执行的处理类型,了解 DOM 可能包含的节点类型是很重要的。
这里有一个说明这一点的示例。这是这些数据的表示:
<sentence> The &projectName; <![CDATA[<i>project</i>]]> is <?editor: red><bold>important</bold><?editor: normal>. </sentence>
这个句子包含一个实体引用 - 指向在其他地方定义的实体的指针。在这种情况下,实体包含项目的名称。示例还包含一个 CDATA 部分(未解释的数据,类似于 HTML 中的
数据)以及处理指令(
),在这种情况下告诉编辑器在呈现文本时使用的颜色。
这是该数据的 DOM 结构。它代表了一个健壮应用程序应该准备处理的结构类型:
+ ELEMENT: sentence + TEXT: The + ENTITY REF: projectName + COMMENT: The latest name we are using + TEXT: Eagle + CDATA: <i>project</i> + TEXT: is + PI: editor: red + ELEMENT: bold + TEXT: important + PI: editor: normal
这个例子描述了 DOM 中可能出现的节点类型。尽管你的应用程序可能大部分时间都能忽略它们,但一个真正健壮的实现需要识别和处理每一个节点。
类似地,导航到一个节点的过程涉及处理子元素,忽略你不感兴趣的元素并检查你感兴趣的元素,直到找到你感兴趣的节点。
一个处理固定、内部生成数据的程序可以承担简化假设:处理指令、注释、CDATA 节点和实体引用在数据结构中不存在。但是真正健壮的应用程序,尤其是处理来自外部世界的各种数据的应用程序,必须准备处理所有可能的 XML 实体。
(一个“简单”的应用程序只能在输入数据包含它所期望的简化 XML 结构时工作。但是没有验证机制来确保更复杂的结构不存在。毕竟,XML 的设计目的就是允许它们存在。)
为了更加健壮,DOM 应用程序必须做到以下几点:
- 在搜索元素时:
- 忽略注释、属性和处理指令。
- 允许子元素不按预期顺序出现的可能性。
- 如果不进行验证,则跳过包含可忽略空格的 TEXT 节点。
- 在提取节点的文本时:
- 从 CDATA 节点以及文本节点提取文本。
- 在收集文本时忽略注释、属性和处理指令。
- 如果遇到实体引用节点或另一个元素节点,则递归(即对所有子节点应用文本提取过程)。
当然,许多应用程序不必担心这些事情,因为它们看到的数据类型将受到严格控制。但如果数据可能来自各种外部来源,那么应用程序可能需要考虑这些可能性。
执行这些功能所需的代码在本课程的末尾的 搜索节点 和 获取节点内容 中给出。现在,目标只是确定 DOM 是否适合你的应用程序。
选择你的模型
正如您所见,当您使用 DOM 时,即使是从节点获取文本这样的简单操作也需要一些编程。因此,如果您的程序处理简单的数据结构,那么 JDOM、dom4j,甚至 1.4 版本的正则表达式包(java.util.regex
)可能更适合您的需求。
另一方面,对于完整的文档和复杂的应用程序,DOM 为您提供了很大的灵活性。如果需要使用 XML Schema,那么再次选择 DOM 是明智之举 - 至少目前是这样。
如果您在开发的应用程序中处理文档和数据,那么 DOM 可能仍然是您最佳选择。毕竟,一旦编写了用于检查和处理 DOM 结构的代码,就很容易为特定目的定制它。因此,选择在 DOM 中执行所有操作意味着您只需处理一组 API,而不是两组。
此外,DOM 标准是内存中文档模型的规范标准。它功能强大且稳健,并且有许多实现。这对许多大型安装来说是一个重要的决策因素,特别是对于需要尽量减少由 API 更改造成的成本的大型应用程序。
最后,即使通讯录中的文本今天可能不允许粗体、斜体、颜色和字体大小,但将来您可能会希望处理这些内容。因为 DOM 能处理几乎任何您提出的要求,选择 DOM 可以更轻松地使您的应用程序具备未来的可扩展性。
将 XML 数据读入 DOM
在本节中,您将通过读取现有的 XML 文件构造一个文档对象模型。
注意 - 在可扩展样式表语言转换中,您将看到如何将 DOM 写出为 XML 文件。(您还将看到如何相对容易地将现有数据文件转换为 XML。)
创建程序
文档对象模型提供了让您创建、修改、删除和重新排列节点的 API。在尝试创建 DOM 之前,了解 DOM 的结构是很有帮助的。这一系列示例将通过一个名为DOMEcho
的示例程序展示 DOM 的内部结构,您可以在安装了 JAXP API 后在目录*INSTALL_DIR*/jaxp-*version*/samples/dom
中找到它。
创建骨架
首先,构建一个简单的程序,将 XML 文档读入 DOM,然后再将其写回。
从应用程序的正常基本逻辑开始,并检查确保命令行上已提供了参数:
public class DOMEcho { static final String outputEncoding = "UTF-8"; private static void usage() { // ... } public static void main(String[] args) throws Exception { String filename = null; for (int i = 0; i < args.length; i++) { if (...) { // ... } else { filename = args[i]; if (i != args.length - 1) { usage(); } } } if (filename == null) { usage(); } } }
此代码执行所有基本的设置操作。DOMEcho
的所有输出都使用 UTF-8 编码。如果未指定参数,则调用usage()
方法会简单地告诉您DOMEcho
期望的参数,因此此处不显示代码。还声明了一个filename
字符串,它将是要由DOMEcho
解析为 DOM 的 XML 文件的名称。
导入所需的类
在本节中,所有类都以单独命名,以便您可以看到每个类来自何处,以便在需要引用 API 文档时参考。在示例文件中,导入语句使用较短的形式,如javax.xml.parsers.*
。
这些是DOMEcho
使用的 JAXP API:
package dom; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory;
这些类用于在解析 XML 文档时可能抛出的异常:
import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.*
这些类读取示例 XML 文件并管理输出:
import java.io.File; import java.io.OutputStreamWriter; import java.io.PrintWriter;
最后,导入 W3C 定义的 DOM、DOM 异常、实体和节点:
import org.w3c.dom.Document; import org.w3c.dom.DocumentType; import org.w3c.dom.Entity; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node;
处理错误
接下来,添加错误处理逻辑。最重要的一点是,当 JAXP 符合标准的文档构建器在解析 XML 文档时遇到问题时,需要报告 SAX 异常。DOM 解析器实际上不必在内部使用 SAX 解析器,但由于 SAX 标准已经存在,因此使用它来报告错误是有意义的。因此,DOM 应用程序的错误处理代码与 SAX 应用程序的错误处理代码非常相似:
private static class MyErrorHandler implements ErrorHandler { private PrintWriter out; MyErrorHandler(PrintWriter 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); } }
正如您所看到的,DomEcho
类的错误处理程序使用PrintWriter
实例生成其输出。
实例化工厂
接下来,在main()
方法中添加以下代码,以获取一个可以提供文档构建器的工厂实例。
public static void main(String[] args) throws Exception { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); // ... }
获取解析器并解析文件
现在,在main()
中添加以下代码以获取一个构建器实例,并使用它来解析指定的文件。
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new File(filename));
被解析的文件由在 main()
方法开头声明的 filename
变量提供,当程序运行时,它作为参数传递给 DOMEcho
。
配置工厂
默认情况下,工厂返回一个不进行验证的解析器,不了解命名空间。要获得一个验证解析器,或者一个了解命名空间的解析器(或两者兼有),您可以配置工厂来设置这两个选项中的一个或两个,使用以下代码。
public static void main(String[] args) throws Exception { String filename = null; boolean dtdValidate = false; boolean xsdValidate = false; String schemaSource = null; for (int i = 0; i < args.length; i++) { if (args[i].equals("-dtd")) { dtdValidate = true; } else if (args[i].equals("-xsd")) { xsdValidate = true; } else if (args[i].equals("-xsdss")) { if (i == args.length - 1) { usage(); } xsdValidate = true; schemaSource = args[++i]; } else { filename = args[i]; if (i != args.length - 1) { usage(); } } } if (filename == null) { usage(); } DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); dbf.setValidating(dtdValidate || xsdValidate); // ... DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new File(filename)); }
如您所见,命令行参数已设置好,以便您可以通知 DOMEcho
对 DTD 或 XML Schema 执行验证,并且工厂已配置为了解命名空间并执行用户指定的验证类型。
注意 - 符合 JAXP 标准的解析器并不需要支持所有这些选项的所有组合,即使参考解析器支持。如果您指定了无效的选项组合,在尝试获取解析器实例时,工厂会生成一个 ParserConfigurationException
。
有关如何使用命名空间和验证的更多信息,请参阅使用 XML Schema 进行验证,其中将描述上述摘录中缺失的代码。
Java 中文官方教程 2022 版(三十八)(3)https://developer.aliyun.com/article/1488149