用DOM读取XML
DOM 是一种解析由万维网协会(W3C) 所开发的 XML 文档的标准应用程序编程接口。 Qt 提供一套用于读取、操作和编写 XML 文挡的非验证型二级 DOM 实现。
DOM 把 XML 文件表示成内存中的一棵树。我们可以按需要遍历这个 DOM 树,也可以修改这个树并把它作为 XML 文件保存到磁盘中。
让我们考虑如下这个,XML 文档:
<doc>
<quote>Scio me nihil scire</quote>
<translation>I know that I know nothing</translation>
</doc>
它对应如下所示的 DOM 树:
这个 DOM 树包含不同类型的节点。例如,Element 节点对应打开标签以及与它匹配的关闭标签。在这两个标签之间的内容则作为这个Element 节点的子节点出现。在 Qt 中,节点类型(和其他所有与 DOM 有关的类一样)具有一个 QDom 前缀。因此,QDomElement 就代表一个Elernent 节点,而QDomText 就代表一个 Text 节点。
不同类型的节点可以具有不同种类的子节点。例如,一个 Element 节点可以包含其他 Element
节点,也可以包含 EntityReference、Text、CDATASection、ProcessingInstruction 以及 Connnent 节点。
图16.3 给出了节点可以包含在的子节点的种类。图中显示为灰色的节点则不能拥有它自己的子节点。
为了演示如何使用 DOM 读取 XML 文件,我们将为前一节中提到的书刊索引文件格式编写一个解析器。
class DomParser
{
public:
DomParser(QTreeWidget *tree);
bool readFile(const QString &fileName);
private:
void parseBookindexElement(const QDomElement &element);
void parseEntryElement(const QDomElement &element,
QTreeWidgetItem *parent);
void parsePageElement(const QDomElement &element,
QTreeWidgetItem *parent);
QTreeWidget *treeWidget;
};
定义了一个称为 DomParse 的类,它将会解析 XML 书刊索引文档并且在 QTreeWidget 中显示结果。
DomParser::DomParser(QTreeWidget *tree)
{
treeWidget = tree;
}
在构造函数中,我们仅将给定的树形窗口部件赋给成员变量。所有的解析都在readFile() 函数的内部完成。
bool DomParser::readFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
std::cerr << "Error: Cannot read file " << qPrintable(fileName)
<< ": " << qPrintable(file.errorString())
<< std::endl;
return false;
}
QString errorStr;
int errorLine;
int errorColumn;
QDomDocument doc;
if (!doc.setContent(&file, false, &errorStr, &errorLine,
&errorColumn)) {
std::cerr << "Error: Parse error at line " << errorLine << ", "
<< "column " << errorColumn << ": "
<< qPrintable(errorStr) << std::endl;
return false;
}
QDomElement root = doc.documentElement();
if (root.tagName() != "bookindex") {
std::cerr << "Error: Not a bookindex file" << std::endl;
return false;
}
parseBookindexElement(root);
return true;
}
在readFile()中首先尝试打开那些文件名已经被传递进来的文件。如果有错误发生,就输出一个出错信息并返回false值表示文件打开失败。否则,就设置一此变量用以保存那些需要的解析出错信息然后创建一个QDomDocument。当对DOM文档调用setContent()函数时,由QIODevice提供的整个XML文档将被读取并解析。如果该文档还未打开setContent()函数将自动打开设备。 setConent()的false参数将禁用命名空间的处理。关于XML命名空间及其如何在Qt中处理,可以查阅QtXml的参考文档。
如果有错误发生,就输出一个出错信息并返回false值表示解析失败。如果解析成功。就对 QDomDocument调用documentElement()以获得它唯一的QDomElement子对象,同时检查它是否为<bookindex>
元素。如果已经有<bookindex>
元素了,就调用parseBookindexElement()来解析它。与前一节中介绍的内容相似,解析过程是使用向下递归方法来实现的。
void DomParser::parseBookindexElement(const QDomElement &element)
{
QDomNode child = element.firstChild();
while (!child.isNull()) {
if (child.toElement().tagName() == "entry")
parseEntryElement(child.toElement(),
treeWidget->invisibleRootItem());
child = child.nextSibling();
}
}
在parseBookindexElement()中,遍历所有的子节点。我们希望每一个节点都是一个<entry>
素那样的话就可以调用parseEntry()来解析每一个节点。我们忽略那些未知的节点,以使书刊索引格式在不阻止旧的解析器工作的情况下今后也能被扩展。所有的<entrv>
节点都是<bookindex>
节点的直接子对象,在组装的用于反映DOM树的窗口部件中它还是顶级节点。所以当我们想透一解析这些节点时,将传递节点元素和树的不可见根项以作为窗口部件树项的父对象。
QDomNode类可以存储任何类型的节点。如果想进一步处理一个节点,首先必须把它转换头正确的数据类型。在这个实例中,我们仅仅关心Element节点,所以对QDomNode调用toElement()以把它转换成QDomElement然后调用tagName()来取得元素的标签名称。如果节点不是Elemen类型那么toElement()函数就返回一个空QDomElement对象和一个空的标签。
void DomParser::parseEntryElement(const QDomElement &element,
QTreeWidgetItem *parent)
{
QTreeWidgetItem *item = new QTreeWidgetItem(parent);
item->setText(0, element.attribute("term"));
QDomNode child = element.firstChild();
while (!child.isNull()) {
if (child.toElement().tagName() == "entry") {
parseEntryElement(child.toElement(), item);
} else if (child.toElement().tagName() == "page") {
parsePageElement(child.toElement(), item);
}
child = child.nextSibling();
}
}
在parseEntryElement()中,我们创建一个树形窗口部件项。传入的父对象项既可以是树的不可见根项(如果这是一个顶级条目的话),也可以是其他的条目(如果它只是一个子条目的话)。我们调用setText()将显示在项的第一列中的文本设置为<entry>
标签的tem属性值。
一旦已经初始化了QTreeWidgetItem就遍历对应于当前<entry>
标签的QDomElement节点下的所有子节点。对于每一个<entry>
标签的子元素,我们利用当前项作为第二个参数来递归调用 parseEntryElement()。然后,利用当前条目作为父对象,创建每一子节点的QTreeWidgetltem。如果子元素为<page>
,就调用parsePageElement()。
void DomParser::parsePageElement(const QDomElement &element,
QTreeWidgetItem *parent)
{
QString page = element.text();
QString allPages = parent->text(1);
if (!allPages.isEmpty())
allPages += ", ";
allPages += page;
parent->setText(1, allPages);
}
在parseElement( )中,对元素调用text()以获得 <page>
和 </page>
。标签之间文本;然后,将文本添加到 QTreeWidgetItem 的第二列,这是一列以逗号分隔的页码列表。DomElement::text()函数遍历元素的所有子节点并连接存储在Text和CDATA节点中的所有文本。
现在看看如醉使用DomParser 类来解析文件:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QStringList args = QApplication::arguments();
...
QTreeWidget treeWidget;
...
DomParser parser(&treeWidget);
for (int i = 1; i < args.count(); ++i)
parser.readFile(args[i]);
return app.exec();
}
首先设立一个 QTreeWidget,然后再创建一个 DomParser。对于命令行中列出的每一个文件,都调用 DomParser::readFile()来打开并解析它,同时组装树形窗口部件。
与前面的例子相似,为了连接到QtXml库,需要将如下命令行添加到应用程序的 .pro 文件中:
QT += xml
就像这个实例所描绘的,尽管并不如使用QXmlStreamReader 那么方便,遍历一个 DOM 树也还是相当简单和直接的。频繁使用 DOM 的程序员会经常编写他们自己的高级封装函数来简化那些常规需求的操作。
domparser.h
#ifndef DOMPARSER_H
#define DOMPARSER_H
class QDomElement;
class QString;
class QTreeWidget;
class QTreeWidgetItem;
class DomParser
{
public:
DomParser(QTreeWidget *tree);
bool readFile(const QString &fileName);
private:
void parseBookindexElement(const QDomElement &element);
void parseEntryElement(const QDomElement &element,
QTreeWidgetItem *parent);
void parsePageElement(const QDomElement &element,
QTreeWidgetItem *parent);
QTreeWidget *treeWidget;
};
#endif
domparser.cpp
#include <QtGui>
#include <QtXml>
#include <iostream>
#include "domparser.h"
DomParser::DomParser(QTreeWidget *tree)
{
treeWidget = tree;
}
bool DomParser::readFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
std::cerr << "Error: Cannot read file " << qPrintable(fileName)
<< ": " << qPrintable(file.errorString())
<< std::endl;
return false;
}
QString errorStr;
int errorLine;
int errorColumn;
QDomDocument doc;
if (!doc.setContent(&file, false, &errorStr, &errorLine,
&errorColumn)) {
std::cerr << "Error: Parse error at line " << errorLine << ", "
<< "column " << errorColumn << ": "
<< qPrintable(errorStr) << std::endl;
return false;
}
QDomElement root = doc.documentElement();
if (root.tagName() != "bookindex") {
std::cerr << "Error: Not a bookindex file" << std::endl;
return false;
}
parseBookindexElement(root);
return true;
}
void DomParser::parseBookindexElement(const QDomElement &element)
{
QDomNode child = element.firstChild();
while (!child.isNull()) {
if (child.toElement().tagName() == "entry")
parseEntryElement(child.toElement(),
treeWidget->invisibleRootItem());
child = child.nextSibling();
}
}
void DomParser::parseEntryElement(const QDomElement &element,
QTreeWidgetItem *parent)
{
QTreeWidgetItem *item = new QTreeWidgetItem(parent);
item->setText(0, element.attribute("term"));
QDomNode child = element.firstChild();
while (!child.isNull()) {
if (child.toElement().tagName() == "entry") {
parseEntryElement(child.toElement(), item);
} else if (child.toElement().tagName() == "page") {
parsePageElement(child.toElement(), item);
}
child = child.nextSibling();
}
}
void DomParser::parsePageElement(const QDomElement &element,
QTreeWidgetItem *parent)
{
QString page = element.text();
QString allPages = parent->text(1);
if (!allPages.isEmpty())
allPages += ", ";
allPages += page;
parent->setText(1, allPages);
}
main.cpp
#include <QtGui>
#include <iostream>
#include "domparser.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QStringList args = QApplication::arguments();
if (args.count() < 2) {
std::cerr << "Usage: domparser file1.xml..." << std::endl;
return 1;
}
QStringList labels;
labels << QObject::tr("Terms") << QObject::tr("Pages");
QTreeWidget treeWidget;
treeWidget.setHeaderLabels(labels);
treeWidget.header()->setResizeMode(QHeaderView::Stretch);
treeWidget.setWindowTitle(QObject::tr("DOM Parser"));
treeWidget.show();
DomParser parser(&treeWidget);
for (int i = 1; i < args.count(); ++i)
parser.readFile(args[i]);
return app.exec();
}