Qt之XML文件解析(DOM)

XML简介

​ 和HTML的语法很相似,但不同之处在于: HTML 被设计用来显示数据,其关注的是数据的外观,XML 被设计用来传输和存储数据,其关注的是数据的内容,因此,XML主要用来作为数据的存储和共享。

​ XML文档是一种树的结构,从根部扩展到枝叶。以下是一个XML示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<root>
<!--写入矩形图元的信息-->
<class name="Rect">
<object name="obj1">
<x1>10</x1>
<y1>10</y1>
<x2>50</x2>
<y1>50</y1>
<linewidth>2</linewidth>
<scale>0</scale>
<rotate>0</rotate>
</object>
</class>
</root>

其中第一行 是XML 声明。它定义 XML 的版本和所使用的编码格式,<root> </root>为根节点的起始(在XML中可以自定义节点名称),<class> </class>为子元素,其中name为其属性,值为Rect;每一个子元素都可以拥有子元素,故class的子元素为object,依次类推; 所有的元素都可以有文本内容和属性,如x1的文本为10,x2的文本为50。

DOM类介绍

Classes说明
QDomAttr表示 QDomElement 的一个属性
QDomCDATASection表示 XML CDATA 部分
QDomCharacterData表示 DOM 中的通用字符串
QDomComment表示 XML 注释
QDomDocument表示一个 XML 文档
QDomDocumentFragmentQDomNodes 树,不是完整的QDomDocument
QDomDocumentTypeDTD 在文档树中的表示
QDomElement表示 DOM 树中的一个元素
QDomEntity表示一个 XML 实体
QDomEntityReference表示 XML 实体引用
QDomImplementation有关 DOM 实现的功能的信息
QDomNamedNodeMap包含可以按名称访问的节点集合
QDomNodeDOM 树中所有节点的基类
QDomNodeListQDomNode 对象列表
QDomNotation表示 XML 表示法
QDomProcessingInstruction表示 XML 处理指令
QDomText表示解析的 XML 文档中的文本数据

其相关继承关系如下:

在这里插入图片描述

一个XML文档如果只做保存数据使用,那么以下XML的构成就足够使用了

在这里插入图片描述

节点类型

在QDomNode中,对XML中各种参数的区分是通过NodeType枚举实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum NodeType {
ElementNode = 1,
AttributeNode = 2,
TextNode = 3,
CDATASectionNode = 4,
EntityReferenceNode = 5,
EntityNode = 6,
ProcessingInstructionNode = 7,
CommentNode = 8,
DocumentNode = 9,
DocumentTypeNode = 10,
DocumentFragmentNode = 11,
NotationNode = 12,
BaseNode = 21,// this is not in the standard
CharacterDataNode = 22 // this is not in the standard
};

节点类型判断

可以通过以下函数对节点的类型进行判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool isAttr() const { return nodeType() == QDomNode::AttributeNode; }
bool isCDATASection() const { return nodeType() == QDomNode::CDATASectionNode; }
bool isDocumentFragment() const { return nodeType() == QDomNode::DocumentFragmentNode; }
bool isDocument() const { return nodeType() == QDomNode::DocumentNode; }
bool isDocumentType() const { return nodeType() == QDomNode::DocumentTypeNode; }
bool isElement() const { return nodeType() == QDomNode::ElementNode; }
bool isEntityReference() const { return nodeType() == QDomNode::EntityReferenceNode; }
bool isText() const{ const QDomNode::NodeType nt = nodeType();return (nt == QDomNode::TextNode)|| (nt == QDomNode::CDATASectionNode); }
bool isEntity() const { return nodeType() == QDomNode::EntityNode; }
bool isNotation() const { return nodeType() == QDomNode::NotationNode; }
bool isProcessingInstruction() const { return nodeType() == QDomNode::ProcessingInstructionNode; }
bool isCharacterData() const { const QDomNode::NodeType nt = nodeType();return (nt == QDomNode::CharacterDataNode)|| (nt == QDomNode::TextNode)
|| (nt == QDomNode::CommentNode); }
bool isComment() const { return nodeType() == QDomNode::CommentNode; }

节点增删改

对于节点的操作主要有插入替换移除追加等,相关API如下

1
2
3
4
5
QDomNodePrivate* insertBefore(QDomNodePrivate* newChild, QDomNodePrivate* refChild);
QDomNodePrivate* insertAfter(QDomNodePrivate* newChild, QDomNodePrivate* refChild);
QDomNodePrivate* replaceChild(QDomNodePrivate* newChild, QDomNodePrivate* oldChild);
QDomNodePrivate* removeChild(QDomNodePrivate* oldChild);
QDomNodePrivate* appendChild(QDomNodePrivate* newChild);

节点创建

创建节点有如下工厂函数可供使用

1
2
3
4
5
6
7
8
9
10
11
12
QDomElement createElement(const QString& tagName);
QDomDocumentFragment createDocumentFragment();
QDomText createTextNode(const QString& data);
QDomComment createComment(const QString& data);
QDomCDATASection createCDATASection(const QString& data);
QDomProcessingInstruction createProcessingInstruction(const QString& target, const QString& data);
QDomAttr createAttribute(const QString& name);
QDomEntityReference createEntityReference(const QString& name);
QDomNodeList elementsByTagName(const QString& tagname) const;
QDomNode importNode(const QDomNode& importedNode, bool deep);
QDomElement createElementNS(const QString& nsURI, const QString& qName);
QDomAttr createAttributeNS(const QString& nsURI, const QString& qName);

节点查找

节点的查找。需要注意的是,虽然Qt提供了elementById这个方法,但是并没有去实现,所以是不可使用的。而通过elementsByTagName方法可以通过name返回所有名为name的节点,通过elementsByTagNameNS可以通过命名空间和名字一起使用得到指定的节点,同时在创建元素时使用createElementNS创建一个带有命名空间的元素即可。

1
2
3
QDomNodeList elementsByTagName(const QString& tagname) const;
QDomNodeList elementsByTagNameNS(const QString& nsURI, const QString& localName);
QDomElement elementById(const QString& elementId);

节点复制

节点的复制,节点类通过隐式共享来共享数据,这样获取到节点指针后可以很方便的对数据进行修改。同时也可以对节点进行深拷贝。

1
QDomNode QDomNode::cloneNode(bool deep = true) const

节点遍历

节点的遍历,从前往后遍历可以通过

1
2
QDomNode firstChild() const;
QDomNode nextSibling() const;

从后往前遍历可以通过

1
2
3
QDomNode lastChild() const;
QDomNode previousSibling() const;
QDomNode parentNode() const;

配合函数namedItem()可以进行更精确的遍历

节点转化

节点的转化。将一个节点类型转化为另一种节点类型

1
2
3
4
5
6
7
8
9
10
11
12
13
QDomAttr toAttr() const;
QDomCDATASection toCDATASection() const;
QDomDocumentFragment toDocumentFragment() const;
QDomDocument toDocument() const;
QDomDocumentType toDocumentType() const;
QDomElement toElement() const;
QDomEntityReference toEntityReference() const;
QDomText toText() const;
QDomEntity toEntity() const;
QDomNotation toNotation() const;
QDomProcessingInstruction toProcessingInstruction() const;
QDomCharacterData toCharacterData() const;
QDomComment toComment() const;

节点清除

节点的清除

1
void clear();

节点非空判断

节点的非空判断

1
bool isNull() const;

元素属性与文本

元素中可以有属性和文本,有关属性的操作主要有获取属性值,添加属性,移除属性和判断是否含有某个属性等,如下所示

1
2
3
4
QString attribute(const QString& name, const QString& defValue) const;
void setAttribute(const QString& name, const QString& value);
void removeAttribute(const QString& name);
bool hasAttribute(const QString& name);

文本似乎没有提供属性这样方便的方法,但是文本作为文本节点,其具有节点的操作方法也同样可以使用,属性也是如此。

其它

  1. 要找出一个节点是否有子节点,可以使用hasChildNodes()或者通过childNodes()获取一个节点所有子节点的列表。

  2. 节点的名称和值(其含义取决于它的节点类型)分别由nodeName()nodeValue()返回。节点的类型由nodeType()返回。Node的值可以通过setNodeValue()来设置。

  3. 通过ownerDocument()返回节点所属的文档

  4. 通过normalize()合并两个相邻的QDomText节点

  5. 通过lineNumber()columnNumber()获取节点的行数和列数

  6. XML文档保存通过函数,可以指定文本流,缩进以及编码方式

    1
    void QDomNode::save(QTextStream &stream, int indent, QDomNode::EncodingPolicy encodingPolicy = QDomNode::EncodingFromDocument) const

创建XML示例

使用DOM解析XML文档时,需要在.pro文件中添加

1
QT += 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
73
74
75
76
77
78
79
void MainWindow::on_btnWrite_clicked()
{
QString fileName = QFileDialog::getSaveFileName(this, "Save File",
"./untitled.xml","Xml(*.xml)");
qDebug() << fileName;

QFile file(fileName);

if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate))
{
qDebug() << "Save failed";
return;
}

QDomDocument doc; /* 创建一个文档 */
/* 创建并添加文档声明 */
QDomProcessingInstruction docStatement =
doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
doc.appendChild(docStatement);

QDomElement root; /* 根节点 */
QDomElement _class; /* 根节点下的class属性 */
QDomElement object; /* class下的object属性 */
QDomElement x1; /* object下的x1属性 */
QDomElement y1; /* object下的y1属性 */
QDomComment comment;/* 注释 */
QDomText text; /* 文本 */

/* 创建并在文档中添加一个根节点 */
root = doc.createElement("root");
doc.appendChild(root);

/* 创建一个注释 */
comment = doc.createComment("以下是矩形图元");
root.appendChild(comment); /* 将注释追加在根节点后面 */

/* 创建一个带有命名空间的元素 */
_class = doc.createElementNS("rect", "class");
root.appendChild(_class); /* 将元素追加在根节点后面 */
_class.setAttribute("name", "Rect"); /* 为元素添加一个属性 */

/* 创建一个元素 */
object = doc.createElement("object");
_class.appendChild(object); /* 将元素追加在class元素后面 */
object.setAttribute("name", "obj1"); /* 为元素添加一个属性 */

/* 创建一个元素 */
x1 = doc.createElement("x1");
object.appendChild(x1); /* 将元素追加在object元素后面 */

/* 创建一个文本 */
text = doc.createTextNode("10");
x1.appendChild(text); /* 将文本追加在x1元素后面 */

/* 创建一个元素 */
object = doc.createElement("object");
_class.appendChild(object); /* 将元素追加在class元素后面 */
object.setAttribute("name", "obj2"); /* 为元素添加一个属性 */

/* 创建一个元素 */
x1 = doc.createElement("x1");
object.appendChild(x1); /* 将元素追加在object元素后面 */

/* 创建一个文本 */
text = doc.createTextNode("20");
x1.appendChild(text); /* 将文本追加在x1元素后面 */

comment = doc.createComment("以下是线图元");
root.appendChild(comment);
_class = doc.createElementNS("line", "class");
root.appendChild(_class);
_class.setAttribute("name", "Line");
text = doc.createTextNode("没有线图元");
_class.appendChild(text);

QTextStream stream(&file);
doc.save(stream, 4);

}

所产生的xml文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<root>
<!--以下是矩形图元-->
<class xmlns="rect" name="Rect">
<object name="obj1">
<x1>10</x1>
</object>
<object name="obj2">
<x1>20</x1>
</object>
</class>
<!--以下是线图元-->
<class xmlns="line" name="Line">没有线图元</class>
</root>

读取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
void MainWindow::on_btnRead_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, "Save this file As"
, "./", "Xml(*.xml)");
qDebug() << fileName;

QFile file(fileName);

if (!file.open(QIODevice::ReadOnly))
{
qDebug() << "Save failed";
return;
}
QDomDocument doc; /* 创建一个文档 */
doc.setContent(&file, true); /* 设置doc的内容来自文件,并且支持命名空间 */
/* 查找命名空间是rect且名字为class的节点 */
QDomNodeList nodeList = doc.elementsByTagNameNS("rect", "class");
qDebug() << nodeList.size();

QDomNode node = nodeList.at(0); /* 因为实际只有一个,这里直接取第0个元素即可 */
QDomElement domElement;
if(node.isElement()) /* 判断是否是元素,然后转化为元素节点 */
domElement = node.toElement();

/* 获取obj1和obj2中的x1的值 */
QDomNode cnode;
cnode = domElement.firstChild();

for(cnode = domElement.firstChild(); cnode.isNull()==false; cnode = cnode.nextSibling())
{
if(cnode.isElement())
{
QDomElement dElement = cnode.toElement();
qDebug() << dElement.attribute("name"); // obj1,obj2

for(QDomNode cnode2 = dElement.firstChild(); cnode2.isNull()==false; cnode2 = cnode2.nextSibling())
{
QDomText tDom = cnode2.firstChild().toText(); /* 把元素x1,y1的第一个子节点转化为文本类节点 */
qDebug() << cnode2.nodeName() << ":" << tDom.data();
}

}
}

}

打印输出:

1
2
3
4
5
1
"obj1"
"x1" : "10"
"obj2"
"x1" : "20"

增删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
void MainWindow::on_btnAddAndRemove_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, "Save this file As"
, "./", "Xml(*.xml)");
qDebug() << fileName;

QFile file(fileName);

if (!file.open(QIODevice::ReadOnly))
{
qDebug() << "Save failed";
return;
}
QDomDocument doc; /* 创建一个文档 */
doc.setContent(&file, true); /* 设置doc的内容来自文件,并且支持命名空间 */
file.close();

/* 查找命名空间是rect且名字为class的节点 */
QDomNodeList nodeList = doc.elementsByTagNameNS("rect", "class");
qDebug() << nodeList.size();

QDomNode node = nodeList.at(0); /* 因为实际只有一个,这里直接取第0个元素即可 */
QDomElement domElement;
if(node.isElement()) /* 判断是否是元素,然后转化为元素节点 */
domElement = node.toElement();

QDomNode last_node = domElement.lastChild(); /* 获取class的子节点中的最后一个节点 */

/* 创建一个新的元素 obj3 */
QDomElement new_object = doc.createElement("object");
new_object.setAttribute("name", "obj3");
QDomElement new_x1 = doc.createElement("x1");
new_object.appendChild(new_x1);
QDomText new_text = doc.createTextNode("50");
new_x1.appendChild(new_text);
QDomElement new_y1 = doc.createElement("y1");
new_object.appendChild(new_y1);
new_text = doc.createTextNode("80");
new_y1.appendChild(new_text);
/* 通过追加或者插入的方式将新的节点加入 */
// domElement.appendChild(new_object);
domElement.insertAfter(new_object, last_node);


/* 删除 class Line元素 */
QDomNode root = doc.documentElement();
QDomNode oldNode = doc.elementsByTagNameNS("line", "class").at(0);
root.removeChild(oldNode);

if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
qDebug() << "Save failed";
return;
}
QTextStream stream(&file);
doc.save(stream, 4);

file.close();
}

依然以写入的文档作为示例,经过增加一个子节点obj3和删除一个命名空间为lineclass节点后的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version='1.0' encoding='UTF-8'?>
<root>
<!--以下是矩形图元-->
<class xmlns="rect" name="Rect">
<object xmlns="rect" name="obj1">
<x1 xmlns="rect">10</x1>
</object>
<object xmlns="rect" name="obj2">
<x1 xmlns="rect">20</x1>
</object>
<object name="obj3">
<x1>50</x1>
<y1>80</y1>
</object>
</class>
<!--以下是线图元-->
</root>

需要注意的是再次写入的时候,程序自动在旧的元素上增添了命名空间。

修改XML示例

示例代码:以修改元素obj1x1的文本为例,假如修改为100

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
void MainWindow::on_btnModify_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, "Save this file As"
, "./", "Xml(*.xml)");
qDebug() << fileName;

QFile file(fileName);

if (!file.open(QIODevice::ReadOnly))
{
qDebug() << "Save failed";
return;
}
QDomDocument doc; /* 创建一个文档 */
if(!doc.setContent(&file, true)) /* 设置doc的内容来自文件,并且支持命名空间 */
{
file.close();
return;
}
file.close();
/* 查找命名空间是rect且名字为object的节点 */
QDomNodeList nodeList = doc.elementsByTagNameNS("rect", "object");
qDebug() << nodeList.size(); /* 会获取到3个 */

for (int i = 0; i < nodeList.size(); i++) {
QDomElement objDom;
if(nodeList.at(i).isElement())
objDom = nodeList.at(i).toElement();

if(QString::compare(objDom.attribute("name"), "obj1") == 0)
{
/* 找到x1节点下的节点,因为只有一个,直接用firstChild,转化为文本节点类型,之后设置参数 */
objDom.elementsByTagName("x1").at(0).firstChild().toText().setData("100");
}
}

QDomNode node = nodeList.at(0); /* 因为实际只有一个,这里直接取第0个元素即可 */
QDomElement domElement;
if(node.isElement()) /* 判断是否是元素,然后转化为元素节点 */
domElement = node.toElement(); /* 获取obj1的节点 */

QDomNode node1 = domElement.elementsByTagName("x1").at(0);
QDomText domText;
if(node.isText()) /* 判断是否是元素,然后转化为元素节点 */
domText = node.toText(); /* 获取obj1的节点 */
domText.setData("100");

if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
qDebug() << "Save failed";
return;
}
QTextStream stream(&file);
doc.save(stream, 4);

file.close();
}

依然以写入的XML文档作为示例,修改后的文件为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version='1.0' encoding='UTF-8'?>
<root>
<!--以下是矩形图元-->
<class xmlns="rect" name="Rect">
<object xmlns="rect" name="obj1">
<x1 xmlns="rect">100</x1>
</object>
<object xmlns="rect" name="obj2">
<x1 xmlns="rect">20</x1>
</object>
</class>
<!--以下是线图元-->
<class xmlns="line" name="Line">没有线图元</class>
</root>

总结

​ 以DOM方式对xml文件进行解析,可以进行对文件进行修改,这是SAX做不到的,但是其读写并没有SAX方便,两者结合,以SAX方式进行一次性读取,以DOM方式进行写入和修改应该是个不错的方式。

参考

  1. XML文件详解
  2. Qt XML C++ Classes