Earth Guardian

You are not LATE!You are not EARLY!

0%

Java 使用 DOM 操作 XML 文件

XML 简介

XML(EXtensible Markup Language) 指可扩展标记语言,是一种标记语言类似 HTML。设计宗旨是传输数据,而非显示数据,是 W3C 的推荐标准。

树结构

0036-xml-tree.gif

对应的标准 XML 文件为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<bookstore>
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="CHILDREN">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>

名称解释

  • 声明
    第一行为 XML 的声明,包含 XML 的版本 version ,字符编码集 encoding,是否独立 standalone。注意:这三个声明顺序不能颠倒。
  • version
    版本,基本上都是 1.0 版本。
  • encoding
    编码,常用编码 utf-8
  • standalone
    表示该 XML 是不是独立的,如果是 yes,则表示这个 XML 文档是独立的,不能引用外部的 DTD 规范文件;如果是 no,则该 XML 文档不是独立的,表示可以用外部的 DTD 规范文档。一般情况下不设置。
  • 根元素
    bookstore:表示根元素。
  • 元素 element
    book:表示为元素,可以并列多个。title, author, year, price:也是元素,它们是 book 的子元素。只是 book 元素没有值,而 title 它们每个元素有一个对应值。
  • 文本 text
    <title lang="en">Everyday Italian</title>:元素 title 的文本为 Everyday Italian
  • 属性 attribute
    category, lang:表示为元素的属性,每个元素可以有多个属性,用空格分开。Android 中布局文件就是典型的多属性无文本方案。
  • 属性的名称和值
    属性:lang="en"。属性的名称为:lang,对应的值为:en

DOM 简介

XML DOM (XML Document Object Model) 定义了访问和操作 XML 文档的标准方法,是 W3C(万维网联盟) 的推荐标准。
DOMXML 文档作为树结构来查看。能够通过 DOM 树来访问所有元素。可以修改或删除它们的内容,并创建新的元素。元素,它们的文本,以及它们的属性,都被认为是节点。

名称解释 - 节点

XML 文档中的每个成分都是一个节点(Node),DOM 是这样规定的:

  • 整个文档是一个文档节点
  • 每个 XML 标签是一个元素节点
  • 包含在 XML 元素中的文本是文本节点
  • 每一个 XML 属性是一个属性节点
  • 注释属于注释节点

在上面的 XML 中,根节点是 <bookstore>,文档中的所有其他节点都被包含在 <bookstore> 中。根节点有四个 <book> 节点,第一个 <book> 节点有四个节点:<title>, <author>, <year>, <price>,其中每个节点都包含一个文本节点:"Harry Potter", "J K. Rowling", "2005", "29.99"

注意:在 DOM 处理中一个普遍的错误是,认为元素节点包含文本。实际上元素是一个节点,文本是另外一个节点;只不过,元素节点的文本是存储在文本节点中的。

在这个例子中:<year>2005</year>,元素节点 <year>,拥有一个值为 "2005" 的文本节点。所以 "2005" 不是 <year> 元素节点的值!

节点树

XML DOMXML 文档视为一种树结构,这种树结构被称为节点树 (node-tree)。可通过这棵树访问所有节点,可以修改或删除它们的内容,也可以创建新的元素。
节点树中的节点彼此之间都有等级关系,父、子和同级节点用于描述这种关系。父节点拥有子节点,位于相同层级上的子节点称为同级节点(兄弟或姐妹)。

  • 在节点树中,顶端的节点成为根节点
  • 根节点之外的每个节点都有一个父节点
  • 节点可以有任何数量的子节点
  • 叶子是没有子节点的节点
  • 同级节点是拥有相同父节点的节点

0036-node-tree.gif

因为 XML 数据是按照树的形式进行构造的,所以可以在不了解树的确切结构且不了解其中包含的数据类型的情况下,对其进行遍历。

节点信息

nodeName 属性

nodeName 属性规定节点的名称:

  • nodeName 是只读的
  • 元素节点的 nodeName 与标签名相同
  • 属性节点的 nodeName 是属性的名称
  • 文本节点的 nodeName 永远是 #text
  • 文档节点的 nodeName 永远是 #document

nodeValue 属性

nodeValue 属性规定节点的值:

  • 元素节点的 nodeValueundefined
  • 文本节点的 nodeValue 是文本自身
  • 属性节点的 nodeValue 是属性的值

常见 nodeNamenodeValue 的对应关系:

节点类型 nodeName 的返回值 nodeValue 的返回值
Document #document null
Element element name null
Attr 属性名称 属性值
Comment #comment 注释文本
Text #text 节点内容

表中可以看出,基本只有属性节点的 nodeValue 有意义。

nodeType 属性

nodeType 属性规定节点的类型:

  • nodeType 是只读的

常见节点类型:

元素类型 节点类型
元素 1
属性 2
文本 3
注释 8
文档 9

常见 API

  • node.getNodeType():获取节点的类型
  • node.getNodeName():获取节点的名称
  • node.getTextContent():获取节点的文本
  • node.getNodeValue():常用于属性节点获取属性值
  • node.getNextSibling():获取同级下一个节点
  • node.getPreviousSibling():获取同级上一个节点
  • node.getParentNode():获取父节点

DOMXML 文件增删改查

源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<resources this="root"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_enabled="false"
app:color="?unknown_attr_ref: 1010038"
app:alpha="?unknown_attr_ref: 1010033" />
<item app:color="?attr/colorAccent" />
<integer name="design_snackbar_text_max_lines">2</integer>
<integer name="design_snackbar_text_max_lines">1</integer>
<integer name="abc_config_activityDefaultDur">220</integer>
<style name="Base.TextAppearance.AppCompat.Inverse">
<item my="hello">my style</item>
<item>?unknown_attr_ref: 1010039</item>
<item>?unknown_attr_ref: 101003f</item>
</style>
</resources>

1
2
3
4
5
6
7
String filePath = "***\***.xml";
// 1. 获取工厂
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 2. 产生解析器
DocumentBuilder builder = factory.newDocumentBuilder();
// 3. 解析 xml 文档,得到代表文档的 document 对象
Document document = builder.parse(new File(filePath));

查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 查找节点
* @param root: 当前元素
* @param tagName: 元素名称
* @param attributeName: 元素属性名称
* @param attributeValue: 元素属性值
* @return 匹配的节点
*/
private Node findNodeByAttribute(Node root, String tagName,
String attributeName, String attributeValue){
if (root instanceof Element) {
NodeList nodeList = ((Element) root).getElementsByTagName(tagName);
if (nodeList != null) {
for (int i = 0; i < nodeList.getLength(); i++){
Element child = (Element) nodeList.item(i);
String value = child.getAttribute(attributeName);
if (value != null && value.equals(attributeValue)){
return child;
}
}
}
}
return null;
}

添加

1
2
3
4
5
6
7
8
9
10
11
// 找到 <item app:color="?attr/colorAccent"/>,在它后面添加新元素
Node addedNode = findNodeByAttribute(root, "item",
"app:color", "?attr/colorAccent");
if (addedNode != null) {
Element newElement = document.createElement("code-add");
newElement.setAttribute("attribute1", "value1");
newElement.setAttribute("attribute2", "value2");
newElement.setTextContent("text-content");
//addedNode.appendChild(newElement);
root.insertBefore(newElement, addedNode.getNextSibling());
}

注意:写入时需要设置缩进字符,否则新加节点不会缩进。

ts.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

修改

1
2
3
4
5
6
7
// 将名称为 integer, 属性为 name="abc_config_activityDefaultDur" 
// 的元素,对应文本修改为 110
Node updateNode = findNodeByAttribute(root, "integer",
"name", "abc_config_activityDefaultDur");
if (updateNode != null){
updateNode.setTextContent("110");
}

删除

1
2
3
4
5
6
// 将 <integer name="design_snackbar_text_max_lines"> 第一个匹配的元素删掉
Node deleteNode = findNodeByAttribute(root, "integer",
"name", "design_snackbar_text_max_lines");
if (deleteNode != null){
root.removeChild(deleteNode);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String filePath = "***\***.xml";
// 将内存中修改后的 doucement 写回 xml 文档
//1. 获取工厂
TransformerFactory tf = TransformerFactory.newInstance();
//2. 获取转换器
Transformer ts = tf.newTransformer();
//3. 处理缩进,并设置缩进为 4 个空格
ts.setOutputProperty(OutputKeys.INDENT, "yes");
ts.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
//4. 去掉standalone
ts.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
document.setXmlStandalone(true);
//5. 将 document 写回 xml 文档
ts.transform(new DOMSource(document), new StreamResult(filePath));

增删改查后的 XML 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:app="http://schemas.android.com/apk/res-auto" this="root">
<item app:alpha="?unknown_attr_ref: 1010033" app:color="?unknown_attr_ref: 1010038" app:state_enabled="false"/>
<item app:color="?attr/colorAccent"/>
<code-add attribute1="value1" attribute2="value2">text-content</code-add>

<integer name="design_snackbar_text_max_lines">1</integer>
<integer name="abc_config_activityDefaultDur">110</integer>
<style name="Base.TextAppearance.AppCompat.Inverse">
<item my="hello">my style</item>
<item>?unknown_attr_ref: 1010039</item>
<item>?unknown_attr_ref: 101003f</item>
</style>
</resources>

遍历 XML 所有节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 获取根节点,遍历所有节点
Node root = document.getFirstChild();
showNodeInfo(root, false);
traverseNodeTree(root);

private void traverseNodeTree(final Node node){
NodeList childLists = node.getChildNodes();
for (int i = 0; i < childLists.getLength(); i++){
Node child = childLists.item(i);
// only show element node info.
if (child.getNodeType() == Node.ELEMENT_NODE){
int length = child.getChildNodes().getLength();
if (length > 1){
// length > 1, there is more than one text content.
// It's mean there are some child element nodes.
showNodeInfo(child, false);
traverseNodeTree(child);
}else {
// length == 0, there is no text content.
// length == 1, there is one text content
showNodeInfo(child, true);
}
}
}
}

/**
* 显示节点对应的元素,属性,文本
* @param node:节点
*/
private void showNodeInfo(final Node node, boolean showTextContent){
String name = node.getNodeName();
StringBuilder attributes = new StringBuilder("");
NamedNodeMap namedNodeMap = node.getAttributes();
if (namedNodeMap != null && namedNodeMap.getLength() > 0){
for (int i = 0; i < namedNodeMap.getLength(); i++) {
Node attributeNode = namedNodeMap.item(i);
attributes.append(attributeNode.toString());
attributes.append(" ");

// show attribute name and value, eg:
// app:color="?unknown_attr_ref: 1010038"
// attribute name is -> app:color
// attribute value is -> ?unknown_attr_ref: 1010038
// System.out.println("attribute.name = " + attributeNode.getNodeName()
// + ", attribute.value = " + attributeNode.getNodeValue());
}
}
String textContent = node.getTextContent();

StringBuilder info = new StringBuilder();
info.append("name-> " + name);
if (!attributes.toString().isEmpty()){
info.append("| attributes-> " + attributes);
}
if (showTextContent && textContent != null && !textContent.isEmpty()){
info.append("| textContent-> " + textContent);
}
System.out.println(info);
}

/**
* 显示 XML 文件的声明:版本,编码,standalone 三个信息
* @param document: 文档节点
*/
private void showDocumentInfo(final Document document){
System.out.println("version = " + document.getXmlVersion());
System.out.println("encoding = " + document.getXmlEncoding());
System.out.println("standalone = " + document.getXmlStandalone());
//System.out.println("node.name = " + document.getNodeName()
// + ", node.value = " + document.getNodeValue() + ", node.type = " + document.getNodeType());
}

流程简析:

  • 获取根节点 document.getFirstChild();,并打印根节点名称和属性
  • 递归遍历所有节点 traverseNodeTree
  • 只关心是元素的节点,所以先做节点类型判断 node.getNodeType() == Node.ELEMENT_NODE
  • 获取元素节点的名称 node.getNodeName(),元素节点的文本 node.getNodeValue()
  • 获取元素节点的所有属性 node.getAttributes()
  • 属性节点获取名称 node.getNodeName(),获取属性值 node.getNodeValue()

standaloneindent 的问题

在写 xml 文件时遇到了如下问题,源文件:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="design_snackbar_text_max_lines">2</integer>
<integer name="design_snackbar_text_max_lines">1</integer>
<integer name="abc_config_activityDefaultDur">220</integer>
</resources>

出现 standalone 以及第一行不缩进

1
2
3
4
5
<?xml version="1.0" encoding="utf-8" standalone="no"?><resources>
<integer name="design_snackbar_text_max_lines">2</integer>

<integer name="abc_config_activityDefaultDur">220</integer>
</resources>

解决缩进,但是还是存在 standalone

缩进解决方案:ts.setOutputProperty(OutputKeys.INDENT, "yes");,生成结果为:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<resources>
<integer name="design_snackbar_text_max_lines">2</integer>

<integer name="abc_config_activityDefaultDur">220</integer>
</resources>

果然有效!再来去掉 standalone

去掉 standalone 后缩进失效

去掉 standalone 解决方案,生成 DOMSource 前加入 document.setXmlStandalone(true);,生成结果为:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"><resources>
<integer name="design_snackbar_text_max_lines">2</integer>

<integer name="abc_config_activityDefaultDur">220</integer>
</resources>

第一行缩进失效???且看下面的终极方案。

同时去掉 standalone 和解决缩进

终极方案(XML Indent and no standalone):

1
2
3
4
ts.setOutputProperty(OutputKeys.INDENT, "yes");
ts.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
ts.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
document.setXmlStandalone(true);

生成结果为:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8">
<resources>
<integer name="design_snackbar_text_max_lines">2</integer>

<integer name="abc_config_activityDefaultDur">220</integer>
</resources>

参考文档

  1. XML 教程
  2. W3school XML DOM 教程
  3. XML Indent and no standalone
  4. 解决xml缩进和添加节点的问题