找回密码
 立即注册
收起左侧

Qt学习之路第60篇 使用 DOM 处理 XML

1
回复
11188
查看
[复制链接]
累计签到:3 天
连续签到:1 天
来源: 2013-9-10 10:45:45 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
版权声明

该文章原创于Qter开源社区(www.qter.org),作者devbean,博客www.devbean.net,转载请注明出处!


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



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

  1. <doc>
  2.     <quote>Scio me nihil scire</quote>
  3.     <translation>I know that I know nothing</translation>
  4. </doc>
复制代码

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

  1. Document
  2.   |--Element(doc)
  3.        |--Element(quote)
  4.        |    |--Text("Scio me nihil scire")
  5.        |--Element(translation)
  6.             |--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 的规定,我们有如下的包含规则:

  1. [Document]
  2.   <- [Element]
  3.   <- DocumentType
  4.   <- ProcessingInstrument
  5.   <- Comment
  6. [Attr]
  7.   <- [EntityReference]
  8.   <- Text
  9. [DocumentFragment] | [Element] | [EntityReference] | [Entity]
  10.   <- [Element]
  11.   <- [EntityReference]
  12.   <- Text
  13.   <- CDATASection
  14.   <- ProcessingInstrument
  15.   <- Comment
复制代码

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



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



QT += xml

头文件也是类似的:

  1. class MainWindow : public QMainWindow
  2. {
  3.     Q_OBJECT
  4. public:
  5.     MainWindow(QWidget *parent = 0);
  6.     ~MainWindow();

  7.     bool readFile(const QString &fileName);
  8. private:
  9.     void parseBookindexElement(const QDomElement &element);
  10.     void parseEntryElement(const QDomElement &element, QTreeWidgetItem *parent);
  11.     void parsePageElement(const QDomElement &element, QTreeWidgetItem *parent);
  12.     QTreeWidget *treeWidget;
  13. };
复制代码

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

  1. MainWindow::MainWindow(QWidget *parent)
  2.     : QMainWindow(parent)
  3. {
  4.     setWindowTitle(tr("XML DOM Reader"));

  5.     treeWidget = new QTreeWidget(this);
  6.     QStringList headers;
  7.     headers << "Items" << "Pages";     treeWidget->setHeaderLabels(headers);
  8.     setCentralWidget(treeWidget);
  9. }

  10. MainWindow::~MainWindow()
  11. {
  12. }
复制代码

readFile() 函数则有了变化:

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

  9.     QString errorStr;
  10.     int errorLine;
  11.     int errorColumn;

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

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

  26.     parseBookindexElement(root);
  27.     return true;
  28. }
复制代码

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

  1. bool QDomDocument::setContent ( QIODevice * dev,
  2.                                 bool namespaceProcessing,
  3.                                 QString * errorMsg = 0,
  4.                                 int * errorLine = 0,
  5.                                 int * errorColumn = 0 )
复制代码

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

  1. bool QDomDocument::setContent ( const QByteArray & data,
  2.                                 bool namespaceProcessing,
  3.                                 QString * errorMsg = 0,
  4.                                 int * errorLine = 0,
  5.                                 int * errorColumn = 0 )
复制代码

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



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

  1. void MainWindow::parseBookindexElement(const QDomElement &element)
  2. {
  3.     QDomNode child = element.firstChild();
  4.     while (!child.isNull()) {
  5.         if (child.toElement().tagName() == "entry") {
  6.             parseEntryElement(child.toElement(),
  7.                               treeWidget->invisibleRootItem());
  8.         }
  9.         child = child.nextSibling();
  10.     }
  11. }
复制代码

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

  1. void MainWindow::parseEntryElement(const QDomElement &element,
  2.                                    QTreeWidgetItem *parent)
  3. {
  4.     QTreeWidgetItem *item = new QTreeWidgetItem(parent);
  5.     item->setText(0, element.attribute("term"));

  6.     QDomNode child = element.firstChild();
  7.     while (!child.isNull()) {
  8.         if (child.toElement().tagName() == "entry") {
  9.             parseEntryElement(child.toElement(), item);
  10.         } else if (child.toElement().tagName() == "page") {
  11.             parsePageElement(child.toElement(), item);
  12.         }
  13.         child = child.nextSibling();
  14.     }
  15. }
复制代码

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

  1. void MainWindow::parsePageElement(const QDomElement &element,
  2.                                   QTreeWidgetItem *parent)
  3. {
  4.     QString page = element.text();
  5.     QString allPages = parent->text(1);
  6.     if (!allPages.isEmpty()) {
  7.          allPages += ", ";
  8.     }
  9.     allPages += page;
  10.     parent->setText(1, allPages);
  11. }
复制代码

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



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



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





回复

使用道具 举报

累计签到:2 天
连续签到:2 天
2018-11-14 22:40:09 显示全部楼层
讲的比较深,看不懂 ,哈啊哈
先保存
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了