将 XML 数据读入 DOM
原文: https://docs.oracle.com/javase/tutorial/jaxp/dom/readingXML.html
在本节中,您将通过读入现有 XML 文件来构建文档对象模型。
注:在可扩展样式表语言转换中,您将看到如何将 DOM 写为 XML 文件。 (您还将看到如何相对轻松地将现有数据文件转换为 XML。)
创建程序
文档对象模型提供的 API 允许您创建,修改,删除和重新排列节点。在尝试创建 DOM 之前,了解 DOM 的结构是很有帮助的。这一系列示例将通过名为DOMEcho
的示例程序使 DOM 内部可见,您可以在目录_INSTALL_DIR_ / jaxp- _ 版本 _ /中找到安装 JAXP API 后的 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
所期望的参数,因此此处不显示代码。还声明了文件名
字符串,它将是DOMEcho
要解析为 DOM 的 XML 文件的名称。
导入必需的类
在本节中,所有类都单独命名,以便您可以查看每个类的来源,以防您想要引用 API 文档。在示例文件中,import 语句使用较短的形式,例如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;
最后,导入 DOM,DOM 异常,实体和节点的 W3C 定义:
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;
处理错误
接下来,添加错误处理逻辑。最重要的一点是,当解析 XML 文档时,需要符合 JAXP 的文档构建器来报告 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()
方法的开头声明,当作为参数传递给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 架构验证,其中将描述上述摘录中缺少的代码。
处理验证错误
根据 SAX 标准的规定,对验证错误的默认响应是什么都不做。 JAXP 标准要求抛出 SAX 异常,因此您使用与用于 SAX 应用程序完全相同的错误处理机制。特别是,您使用DocumentBuilder
类的setErrorHandler
方法为其提供实现 SAX ErrorHandler
接口的对象。
注意 - DocumentBuilder
也有一个setEntityResolver
方法可以使用。
以下代码将文档构建器配置为使用句柄错误中定义的错误处理器。
DocumentBuilder db = dbf.newDocumentBuilder();
OutputStreamWriter errorWriter = new OutputStreamWriter(System.err,
outputEncoding);
db.setErrorHandler(new MyErrorHandler (new PrintWriter(errorWriter, true)));
Document doc = db.parse(new File(filename));
到目前为止您看到的代码已设置文档构建器,并将其配置为根据请求执行验证。错误处理也已到位。但是, DOMEcho
还没有做任何事情。在下一节中,您将看到如何显示 DOM 结构并开始探索它。例如,您将看到 DOM 中的实体引用和 CDATA 部分。也许最重要的是,您将看到文本节点(包含实际数据)如何驻留在 DOM 中的元素节点下。
显示 DOM 节点
要创建或操作 DOM,有助于清楚地了解 DOM 中的节点是如何构造的。本教程的这一部分公开了 DOM 的内部结构,以便您可以看到它包含的内容。 DOMEcho
示例通过回显 DOM 节点,然后在屏幕上打印它们,并使用适当的缩进来使节点层次结构显而易见。这些节点类型的规范可以在DOM Level 2 核心规范中的
节点的规范下找到。 [下表 3-1](#gfzpy) 改编自该规范。
表 3-1 节点类型
|
节点
|
节点名称
|
的 nodeValue
|
属性
|
| :— | :— | :— | :— |
| Attr
| 属性名称 | 属性的值 | 空值 |
| CDATASection
| #cdata-section
| CDATA 部分的内容 | 空值 |
| 评论
| #comment
| 评论的内容 | 空值 |
| 文件
| #document
| 空值 | 空值 |
| DocumentFragment
| #documentFragment
| 空值 | 空值 |
| DocumentType
| 文档类型名称 | 空值 | 空值 |
| 元素
| 标签名称 | 空值 | 空值 |
| 实体
| 实体名称 | 空值 | 空值 |
| EntityReference
| 引用的实体名称 | 空值 | 空值 |
| 符号
| 符号名称 | 空值 | 空值 |
| ProcessingInstruction
| 目标 | 整个内容不包括目标 | 空值 |
| 正文
| #text
| 文本节点的内容 | 空值 |
该表中的信息非常有用;使用 DOM 时需要它,因为所有这些类型都混合在 DOM 树中。
获取节点类型信息
DOM 节点元素类型信息是通过调用org.w3c.dom.Node
类的各种方法获得的。由DOMEcho
公开的节点属性由以下代码回显。
private void printlnCommon(Node n) {
out.print(" nodeName=\"" + n.getNodeName() + "\"");
String val = n.getNamespaceURI();
if (val != null) {
out.print(" uri=\"" + val + "\"");
}
val = n.getPrefix();
if (val != null) {
out.print(" pre=\"" + val + "\"");
}
val = n.getLocalName();
if (val != null) {
out.print(" local=\"" + val + "\"");
}
val = n.getNodeValue();
if (val != null) {
out.print(" nodeValue=");
if (val.trim().equals("")) {
// Whitespace
out.print("[WS]");
}
else {
out.print("\"" + n.getNodeValue() + "\"");
}
}
out.println();
}
每个 DOM 节点至少都有一个类型,一个名称和一个值,它们可能是空的,也可能不是。在上面的例子中,节点
接口的getNamespaceURI()
, getPrefix()
, getLocalName()
和getNodeValue( )
方法返回并打印回显节点的名称空间 URI,名称空间前缀,本地限定名称和值。请注意, trim()
方法在getNodeValue()
返回的值上调用,以确定节点的值是否为空白空格并相应地打印消息。
有关节点
方法的完整列表及其返回的不同信息,请参阅 Node
的 API 文档。
接下来,定义一种方法来在节点打印时为节点设置缩进,以便可以容易地看到节点层次结构。
private void outputIndentation() {
for (int i = 0; i < indent; i++) {
out.print(basicIndent);
}
}
basicIndent
常量定义DOMEcho
显示节点树层次结构时使用的缩进的基本单位,通过将以下突出显示的行添加到DOMEcho
构造器类来定义。
public class DOMEcho {
static final String outputEncoding = "UTF-8";
private PrintWriter out;
private int indent = 0;
private final String basicIndent = " ";
DOMEcho(PrintWriter out) {
this.out = out;
}
}
与句柄错误中定义的错误处理器一样, DOMEcho
程序将其输出创建为PrintWriter
实例。
词汇控制
词法信息是重建 XML 文档原始语法所需的信息。保留词汇信息对于编辑应用程序非常重要,在这些应用程序中,您希望保存一份准确反映原始文档的文档,其中包含注释,实体引用以及一开始可能包含的任何 CDATA 部分。
但是,大多数应用程序仅关注 XML 结构的内容。他们可以忽略评论,他们不关心数据是以 CDATA 部分编码还是以纯文本编码,或者是否包含实体参考。对于此类应用程序,需要最少的词法信息,因为它简化了应用程序必须准备检查的 DOM 节点的数量和种类。
以下DocumentBuilderFactory
方法可让您控制在 DOM 中看到的词法信息。
setCoalescing()
将CDATA
节点转换为Text
节点并附加到相邻的Text
节点(如果有)。
setExpandEntityReferences()
扩展实体引用节点。
setIgnoringComments()
忽略评论。
setIgnoringElementContentWhitespace()
忽略不是元素内容重要部分的空格。
所有这些属性的默认值都是 false,它保留了以原始形式重建传入文档所需的所有词法信息。将它们设置为 true 可以构建最简单的 DOM,以便应用程序可以专注于数据的语义内容,而无需担心词法语法细节。 表 3-2 总结了设置的效果。
表 3-2 词汇控制设置
|
API
|
保留词汇信息
|
专注于内容
|
| :— | :— | :— |
| setCoalescing()
| 假 | 真正 |
| setExpandEntityReferences()
| 假 | 真正 |
| setIgnoringComments()
| 假 | 真正 |
| setIgnoringElementContent
Whitespace()
| 假 | 真正 |
在DomEcho
示例的主要方法中实现这些方法如下所示。
// ...
dbf.setIgnoringComments(ignoreComments);
dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
dbf.setCoalescing(putCDATAIntoText);
dbf.setExpandEntityReferences(!createEntityRefs);
// ...
布尔变量ignoreComments
, ignoreWhitespace
, putCDATAIntoText
和createEntityRefs
在主方法代码的开头声明,并设置它们运行DomEcho
时通过命令行参数。
public static void main(String[] args) throws Exception {
// ...
boolean ignoreWhitespace = false;
boolean ignoreComments = false;
boolean putCDATAIntoText = false;
boolean createEntityRefs = false;
for (int i = 0; i < args.length; i++) {
if (...) { // Validation arguments here
// ...
}
else if (args[i].equals("-ws")) {
ignoreWhitespace = true;
}
else if (args[i].startsWith("-co")) {
ignoreComments = true;
}
else if (args[i].startsWith("-cd")) {
putCDATAIntoText = true;
}
else if (args[i].startsWith("-e")) {
createEntityRefs = true;
// ...
}
else {
filename = args[i];
// Must be last arg
if (i != args.length - 1) {
usage();
}
}
}
// ...
}
打印 DOM 树节点
DomEcho
应用程序允许您查看 DOM 的结构,并演示构成 DOM 的节点以及它们的排列方式。通常,DOM 树中的绝大多数节点将是Element
和Text
节点。
注:文本节点在 DOM 中的元素节点下存在,数据始终存储在文本节点中。也许 DOM 处理中最常见的错误是导航到元素节点并期望它包含存储在该元素中的数据。不是这样!即使最简单的元素节点下面也有一个包含数据的文本节点。
使用适当的缩进打印出 DOM 树节点的代码如下所示。
private void echo(Node n) {
outputIndentation();
int type = n.getNodeType();
switch (type) {
case Node.ATTRIBUTE_NODE:
out.print("ATTR:");
printlnCommon(n);
break;
case Node.CDATA_SECTION_NODE:
out.print("CDATA:");
printlnCommon(n);
break;
case Node.COMMENT_NODE:
out.print("COMM:");
printlnCommon(n);
break;
case Node.DOCUMENT_FRAGMENT_NODE:
out.print("DOC_FRAG:");
printlnCommon(n);
break;
case Node.DOCUMENT_NODE:
out.print("DOC:");
printlnCommon(n);
break;
case Node.DOCUMENT_TYPE_NODE:
out.print("DOC_TYPE:");
printlnCommon(n);
NamedNodeMap nodeMap = ((DocumentType)n).getEntities();
indent += 2;
for (int i = 0; i < nodeMap.getLength(); i++) {
Entity entity = (Entity)nodeMap.item(i);
echo(entity);
}
indent -= 2;
break;
case Node.ELEMENT_NODE:
out.print("ELEM:");
printlnCommon(n);
NamedNodeMap atts = n.getAttributes();
indent += 2;
for (int i = 0; i < atts.getLength(); i++) {
Node att = atts.item(i);
echo(att);
}
indent -= 2;
break;
case Node.ENTITY_NODE:
out.print("ENT:");
printlnCommon(n);
break;
case Node.ENTITY_REFERENCE_NODE:
out.print("ENT_REF:");
printlnCommon(n);
break;
case Node.NOTATION_NODE:
out.print("NOTATION:");
printlnCommon(n);
break;
case Node.PROCESSING_INSTRUCTION_NODE:
out.print("PROC_INST:");
printlnCommon(n);
break;
case Node.TEXT_NODE:
out.print("TEXT:");
printlnCommon(n);
break;
default:
out.print("UNSUPPORTED NODE: " + type);
printlnCommon(n);
break;
}
indent++;
for (Node child = n.getFirstChild(); child != null;
child = child.getNextSibling()) {
echo(child);
}
indent--;
}
此代码首先使用 switch 语句打印出不同的节点类型和任何可能的子节点,并使用适当的缩进。
节点属性不作为子层次结构包含在 DOM 层次结构中。它们是通过节点
接口的getAttributes
方法获得的。
DocType
接口是w3c.org.dom.Node
的扩展。它定义了getEntities
方法,用于获取实体
节点 - 定义实体的节点。与属性
节点类似,实体
节点不会显示为 DOM 节点的子节点。
节点操作
本节简要介绍一些您可能想要应用于 DOM 的操作。
创建节点
遍历节点
搜索节点
获取节点内容
创建属性
删除和更改节点
插入节点
创建节点
您可以使用Document
接口的方法创建不同类型的节点。例如, createElement
, createComment
, createCDATAsection
, createTextNode
等等。 org.w3c.dom.Document
的 API 文档中提供了创建不同节点的完整方法列表。
遍历节点
org.w3c.dom.Node
接口定义了许多可用于遍历节点的方法,包括getFirstChild
, getLastChild
, getNextSibling
], getPreviousSibling
和getParentNode
。这些操作足以从树中的任何位置到树中的任何其他位置。
正在搜索节点
当您搜索具有特定名称的节点时,还需要考虑更多内容。尽管获得第一个孩子并检查它是否是正确的孩子是很诱人的,但搜索必须考虑到子列表中的第一个孩子可能是评论或处理指令的事实。如果 XML 数据尚未经过验证,它甚至可能是包含可忽略空格的文本节点。
实质上,您需要查看子节点列表,忽略那些无关紧要的节点并检查您关心的节点。下面是在 DOM 层次结构中搜索节点时需要编写的例程的示例。它在此处完整呈现(包含注释),以便您可以将其用作应用程序中的模板。
/**
* Find the named subnode in a node's sublist.
* <ul>
* <li>Ignores comments and processing instructions.
* <li>Ignores TEXT nodes (likely to exist and contain
* ignorable whitespace, if not validating.
* <li>Ignores CDATA nodes and EntityRef nodes.
* <li>Examines element nodes to find one with
* the specified name.
* </ul>
* @param name the tag name for the element to find
* @param node the element node to start searching from
* @return the Node found
*/
public Node findSubNode(String name, Node node) {
if (node.getNodeType() != Node.ELEMENT_NODE) {
System.err.println("Error: Search node not of element type");
System.exit(22);
}
if (! node.hasChildNodes()) return null;
NodeList list = node.getChildNodes();
for (int i=0; i < list.getLength(); i++) {
Node subnode = list.item(i);
if (subnode.getNodeType() == Node.ELEMENT_NODE) {
if (subnode.getNodeName().equals(name))
return subnode;
}
}
return null;
}
有关此代码的更深入说明,请参阅在中增加复杂度何时使用 DOM 。另请注意,您可以使用 Lexical Controls 中描述的 API 来修改解析器构造的 DOM 类型。不过,关于这段代码的好处是它几乎可以用于任何 DOM。
获取节点内容
当您想要获取节点包含的文本时,您还需要查看子节点列表,忽略无关紧要的条目并累积您在TEXT
节点中找到的文本, CDATA
节点和EntityRef
节点。以下是您可以用于该过程的例程的示例。
/**
* Return the text that a node contains. This routine:
* <ul>
* <li>Ignores comments and processing instructions.
* <li>Concatenates TEXT nodes, CDATA nodes, and the results of
* recursively processing EntityRef nodes.
* <li>Ignores any element nodes in the sublist.
* (Other possible options are to recurse into element
* sublists or throw an exception.)
* </ul>
* @param node a DOM node
* @return a String representing its contents
*/
public String getText(Node node) {
StringBuffer result = new StringBuffer();
if (! node.hasChildNodes()) return "";
NodeList list = node.getChildNodes();
for (int i=0; i < list.getLength(); i++) {
Node subnode = list.item(i);
if (subnode.getNodeType() == Node.TEXT_NODE) {
result.append(subnode.getNodeValue());
}
else if (subnode.getNodeType() == Node.CDATA_SECTION_NODE) {
result.append(subnode.getNodeValue());
}
else if (subnode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
// Recurse into the subtree for text
// (and ignore comments)
result.append(getText(subnode));
}
}
return result.toString();
}
有关此代码的更深入说明,请参阅在中增加复杂度何时使用 DOM 。同样,您可以使用 Lexical Controls 中描述的 API 来简化此代码,以修改解析器构造的 DOM 类型。但是这个代码的优点在于它几乎可以用于任何 DOM。
创建属性
扩展 Node 的org.w3c.dom.Element
接口定义了setAttribute
操作,该操作向该节点添加了一个属性。 (从 Java 平台的角度来看,更好的名称是addAttribute
。该属性不是该类的属性,并且创建了一个新对象。)您还可以使用文档
的createAttribute
操作创建属性
的实例,然后使用setAttributeNode
方法添加它。
删除和更改节点
要删除节点,请使用其父节点的removeChild
方法。要更改它,您可以使用父节点的replaceChild
操作或节点的setNodeValue
操作。
插入节点
创建新节点时要记住的重要一点是,在创建元素节点时,您指定的唯一数据是名称。实际上,该节点为您提供挂钩的挂钩。通过添加到子节点列表中挂起项目。例如,您可以添加文本节点, CDATA
节点或属性节点。在构建时,请记住您在本教程中看到的结构。请记住:层次结构中的每个节点都非常简单,只包含一个数据元素。
运行DOMEcho
样例
要运行DOMEcho
样例,请按照以下步骤操作。
- 导航至
样例
目录。%cd _install-dir_
/ jaxp-14_2-` 释放日期 _/样例
。` - 编译示例类。
% javac dom/*
Run the
DOMEcho
program on an XML file.选择
数据
目录中的一个 XML 文件,然后运行DOMEcho
程序。在这里,我们选择在文件personal-schema.xml
上运行程序。% java dom/DOMEcho data/personal-schema.xml
XML 文件
personal-schema.xml
包含小公司的人事档案。当您在其上运行DOMEcho
程序时,您应该看到以下输出。DOC: nodeName="#document"
ELEM: nodeName="personnel"
local="personnel"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="person"
local="person"
ATTR: nodeName="id"
local="id"
nodeValue="Big.Boss"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="name"
local="name"
ELEM: nodeName="family"
local="family"
TEXT: nodeName="#text"
nodeValue="Boss"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="given"
local="given"
TEXT: nodeName="#text"
nodeValue="Big"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="email"
local="email"
TEXT: nodeName="#text"
nodeValue="chief@foo.example.com"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="link"
local="link"
ATTR: nodeName="subordinates"
local="subordinates"
nodeValue="one.worker two.worker
three.worker four.worker
five.worker"
TEXT: nodeName="#text"
nodeValue=[WS]
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="person"
local="person"
ATTR: nodeName="id"
local="id"
nodeValue="one.worker"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="name"
local="name"
ELEM: nodeName="family"
local="family"
TEXT: nodeName="#text"
nodeValue="Worker"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="given"
local="given"
TEXT: nodeName="#text"
nodeValue="One"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="email"
local="email"
TEXT: nodeName="#text"
nodeValue="one@foo.example.com"
TEXT: nodeName="#text"
nodeValue=[WS]
ELEM: nodeName="link"
local="link"
ATTR: nodeName="manager"
local="manager"
nodeValue="Big.Boss"
TEXT: nodeName="#text"
nodeValue=[WS]
[...]
如您所见,
DOMEcho
打印出文档中不同元素的所有节点,并使用正确的缩进来显示节点层次结构。