首页 Qt 学习之路 2 Qt 学习之路 2(60):使用 DOM 处理 XML

Qt 学习之路 2(60):使用 DOM 处理 XML

15 2.5K

DOM 是由 W3C 提出的一种处理 XML 文档的标准接口。Qt 实现了 DOM Level 2 级别的不验证读写 XML 文档的方法。

上一章所说的流的方式不同,DOM 一次性读入整个 XML 文档,在内存中构造为一棵树(被称为 DOM 树)。我们能够在这棵树上进行导航,比如移动到下一节点或者返回上一节点,也可以对这棵树进行修改,或者是直接将这颗树保存为硬盘上的一个 XML 文件。考虑下面一个 XML 片段:

<doc>
    <quote>Scio me nihil scire</quote>
    <translation>I know that I know nothing</translation>
</doc>

我们可以认为是如下一棵 DOM 树:

Document
  |--Element(doc)
       |--Element(quote)
       |    |--Text("Scio me nihil scire")
       |--Element(translation)
            |--Text("I know that I know nothing")

上面所示的 DOM 树包含了不同类型的节点。例如,Element 类型的节点有一个开始标签和对应的一个结束标签。在开始标签和结束标签之间的内容作为这个 Element 节点的子节点。在 Qt 中,所有 DOM 节点的类型名字都以 QDom 开头,因此,QDomElement就是 Element 节点,QDomText就是 Text 节点。不同类型的节点则有不同类型的子节点。例如,Element 节点允许包含其它 Element 节点,也可以是其它类型,比如 EntityReference,Text,CDATASection,ProcessingInstruction 和 Comment。按照 W3C 的规定,我们有如下的包含规则:

[Document]
  <- [Element]
  <- DocumentType
  <- ProcessingInstrument
  <- Comment
[Attr]
  <- [EntityReference]
  <- Text
[DocumentFragment] | [Element] | [EntityReference] | [Entity]
  <- [Element]
  <- [EntityReference]
  <- Text
  <- CDATASection
  <- ProcessingInstrument
  <- Comment

上面表格中,带有 [] 的可以带有子节点,反之则不能。

下面我们还是以上一章所列出的 books.xml 这个文件来作示例。程序的目的还是一样的:用QTreeWidget 来显示这个文件的结构。需要注意的是,由于我们选用 DOM 方式处理 XML,无论是 Qt4 还是 Qt5 都需要在 .pro 文件中添加这么一句:

QT += xml

头文件也是类似的:

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

    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;
};

MainWindow的构造函数和析构函数和上一章是一样的,没有任何区别:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    setWindowTitle(tr("XML DOM Reader"));

    treeWidget = new QTreeWidget(this);
    QStringList headers;
    headers << "Items" << "Pages";     treeWidget->setHeaderLabels(headers);
    setCentralWidget(treeWidget);
}

MainWindow::~MainWindow()
{
}

readFile()函数则有了变化:

bool MainWindow::readFile(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        QMessageBox::critical(this, tr("Error"),
                              tr("Cannot read file %1").arg(fileName));
        return false;
    }

    QString errorStr;
    int errorLine;
    int errorColumn;

    QDomDocument doc;
    if (!doc.setContent(&file, false, &errorStr, &errorLine,
                        &errorColumn)) {
        QMessageBox::critical(this, tr("Error"),
                              tr("Parse error at line %1, column %2: %3")
                                .arg(errorLine).arg(errorColumn).arg(errorStr));
        return false;
    }

    QDomElement root = doc.documentElement();
    if (root.tagName() != "bookindex") {
        QMessageBox::critical(this, tr("Error"),
                              tr("Not a bookindex file"));
        return false;
    }

    parseBookindexElement(root);
    return true;
}

readFile()函数显然更长更复杂。首先需要使用QFile打开一个文件,这点没有区别。然后我们创建一个QDomDocument对象,代表整个文档。注意看我们上面介绍的结构图,Document 是 DOM 树的根节点,也就是这里的QDomDocument;使用其setContent()函数填充 DOM 树。setContent()有八个重载,我们使用了其中一个:

bool QDomDocument::setContent ( QIODevice * dev,
                                bool namespaceProcessing,
                                QString * errorMsg = 0,
                                int * errorLine = 0,
                                int * errorColumn = 0 )

不过,这几个重载形式都是调用了同一个实现:

bool QDomDocument::setContent ( const QByteArray & data,
                                bool namespaceProcessing,
                                QString * errorMsg = 0,
                                int * errorLine = 0,
                                int * errorColumn = 0 )

两个函数的参数基本类似。第二个函数有五个参数,第一个是QByteArray,也就是所读取的真实数据,由QIODevice即可获得这个数据,而QFile就是QIODevice的子类;第二个参数确定是否处理命名空间,如果设置为 true,处理器会自动设置标签的前缀之类,因为我们的 XML 文档没有命名空间,所以直接设置为 false;剩下的三个参数都是关于错误处理。后三个参数都是输出参数,我们传入一个指针,函数会设置指针的实际值,以便我们在外面获取并进行进一步处理。

QDomDocument::setContent()函数调用完毕并且没有错误后,我们调用QDomDocument::documentElement()函数获得一个 Document 元素。如果这个 Document 元素标签是 bookindex,则继续向下处理,否则则报错。

void MainWindow::parseBookindexElement(const QDomElement &element)
{
    QDomNode child = element.firstChild();
    while (!child.isNull()) {
        if (child.toElement().tagName() == "entry") {
            parseEntryElement(child.toElement(),
                              treeWidget->invisibleRootItem());
        }
        child = child.nextSibling();
    }
}

如果根标签正确,我们取第一个子标签,判断子标签不为空,也就是存在子标签,然后再判断其名字是不是 entry。如果是,说明我们正在处理 entry 标签,则调用其自己的处理函数;否则则取下一个标签(也就是nextSibling()的返回值)继续判断。注意我们使用这个 if 只选择 entry 标签进行处理,其它标签直接忽略掉。另外,firstChild()nextSibling()两个函数的返回值都是QDomNode。这是所有节点类的基类。当我们需要对节点进行操作时,我们必须将其转换成正确的子类。这个例子中我们使用toElement()函数将QDomNode转换成QDomElement。如果转换失败,返回值将是空的QDomElement类型,其tagName()返回空字符串,if 判断失败,其实也是符合我们的要求的。

void MainWindow::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()函数中,我们创建了一个树组件的节点,其父节点是根节点或另外一个 entry 节点。接着我们又开始遍历这个 entry 标签的子标签。如果是 entry 标签,则递归调用自身,并且把当前节点作为父节点;否则则调用parsePageElement()函数。

void MainWindow::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);
}

parsePageElement()则比较简单,我们还是通过字符串拼接设置叶子节点的文本。这与上一章的步骤大致相同。

程序运行结果同上一章一模一样,这里不再贴出截图。

通过这个例子我们可以看到,使用 DOM 当时处理 XML 文档,除了一开始的setContent()函数,其余部分已经与原始文档没有关系了,也就是说,setContent()函数的调用之后,已经在内存中构建好了一个完整的 DOM 树,我们可以在这棵树上面进行移动,比如取相邻节点(nextSibling())。对比上一章流的方式,虽然我们早早关闭文件,但是我们始终使用的是readNext()向下移动,同时也不存在readPrevious()这样的函数。

15 评论

Kqz 2013年8月12日 - 07:37

Thanks for your great articles. Any plan on QML?

回复
豆子 2013年8月12日 - 14:56

QML 在后面的章节,是有计划的

回复
Kqz 2013年8月13日 - 08:00

Thanks, can't wait.

回复
wenmd0703 2014年7月18日 - 17:29

QDomDocument doc;
if (!doc.setContent(&file, false, &errorStr, &errorLine,
&errorColumn)) {
QMessageBox::critical(this, tr("Error"),
tr("Parse error at line %1, column %2: %3")
.arg(errorLine).arg(errorColumn).arg(errorStr));
return false;
}

豆子,我在执行上面的代码时输出
"unexpected end of file" line: 1 col: 1
程序异常结束。
可是我的内容都是对的,为啥呀?求指点啊

回复
wenmd 2014年7月20日 - 12:19

问题已解决是配置 的问题

回复
chao 2020年6月8日 - 13:46

我也出现了同样的问题,请问你是怎么解决的?

回复
wenmd 2014年7月20日 - 17:41

豆子,想请教你个问题,看到你上面写的Text是孩子节点,那么属性(attribute)是不是叶子节点?我看到标注的DOM结构属性也是子节点,但我在代码实验室发现不是叶子节点,比如

100

0.6
good

这里的id,并没有算在叶子节点里

回复
wenmd 2014年7月20日 - 17:43

额 居然没有显示到 ,就是
<record id="01">
...

</record>
这种形式

回复
Justor 2017年1月9日 - 18:29

QString xmlData;
...
QDomDocument *m_doc = new QDomDocument();
if( !m_doc->setContent( xmlData ) )
{
std::cerr << "Error"
<< " Cannot read file";
return;
}
请问豆子哥,我要处理一个超长字符串xmlData,但是setContext貌似只能处理10000以内的字符,有什么比较好的解决方案吗?

回复
豆子 2017年1月12日 - 09:53

setContext 好像并没有限制字符数,你的那个错误很有可能是 xmlData 里面有不被 XML 接受的非法字符,导致 XML 处理器失败。首先你要确定里面 xmlData 的所有字符都是符合要求的;其次,使用 QString 并不是最好的方案,因为 QString 内部使用 UTF-16 存储文本,而 XML 则使用头部设置的存储,这种编码不一致也可能导致问题,解决方案是使用 QByteArray 或者从文件直接读取流设置。你可以参考:https://forum.qt.io/topic/17228/solved-qdomdocument/11

回复
tyw66 2018年7月13日 - 14:38

哇 终于把代码展示器外观换了,以前每次都习惯性爱双击,然后。。。

回复
豆子 2018年7月13日 - 21:17

替换一遍不容易啊... 正则表达式写到吐,哈哈

回复
tyw66 2018年7月24日 - 15:22

谢谢豆子哥!教程很棒

回复
jack 2018年7月25日 - 15:03

具体怎解决的?我也碰到了这个问题

回复
李十一 2019年5月4日 - 18:47

想请问 运行的时候在
if(!doc.setContent(&file){
file.close();
return;
}
这里出现 Debug Assertion failed是怎么回事啊

回复

回复 wenmd 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2