一、XML是什么?
XML(eXtensible Markup Language,可扩展标记语言)是普遍用于数据交换和数据存储的一种多用途文本文件格式。XML首先是由万维网协会(World Wide Web Consortium,W3C)作为SGML的一个替代品来开发的。它的语法规则与HTML相似,不过XML是一种用于语言分析的语言,它并没有要求专门的标记符,属性或者条目。HTML的XML兼容版称为XHTML。
对于比较流行的SVG(可标量化矢量图形)XML格式,QtSvg模块提供了可用于载入并呈现SVG图像的类。对于使用MathML(数学标记语言)XML格式的绘制文档,可以使用Qt Solutions中的QtMmLWidget。
对于一般的XML数据处理,Qt提供了QtXml模块,这是本文的主题。
二、XML的读取方式
QtXml模块提供了三种截然不同的应用程序编程接口用来读取XML文件:
- (1)QXmlStreamReader是一个用于读取格式良好的XML文档的快速解析器。
- (2)DOM(文档对象模型)把XML文档转换为应用程序可以遍历的树形结构。
- (3)SAX(XML简单应用程序编程接口)通过虚拟函数直接向应用程序报告“解析事件”。
QXmlStreamReader类最快且最容易使用,它同时还提供了与其他Qt兼容的应用程序编程接口。它很适用于编写单通解析器。DOM的主要优点是它能以任意顺序遍历XML文档的树形表示,同时可以实现多通解析算法。有一些应用程序甚至使用DOM树作为它们的基本数据结构。SAX则因为一些历史原因而被得以沿用至今,使用QXmlStreamReader通常会有更加简单高效的编码。
使用QXmlStreamReader是在Qt中读取XML文档的最快且最简单的方式。因为解析器的工作能力是逐渐递增的,所以它尤其适用于诸如查找XML文档中一个给定的标记符号出现的次数、读取内存容纳不了的特大文件、组装定制的数据结构以反映XML文档的内容等。
QXmlStreamReader解析器根据下图中所列出的记号工作。每次只要调用readNext()函数,下一个记号就会被读取并变成当前的记号。当前记号的属性取决于记号的类型,可以使用表格中列出的getter函数读取当前记号。
考虑如下的XML文档:
<doc>
<quote>Einmal ist keinmal</quote>
</doc>
如果解析这个文档,则readNext()每调用一次都将生成一个新记号,若使用getter函数还会获得额外的信息;
StartDocument
StartElement (name() == "doc")
StartElement (name() == "quote")
Characters (text() == " Einmal ist keinmal")
EndElement (name() == "quote")
EndElement (name() == "doc")
EndDocument
每次调用readNext()后,都可以使用isStartElement(),isCharacters()及类似的函数或者仅仅用state()来测试当前记号的类型。
三、QXmlStreamReader读取XML实例
下面将查看一个实例,它告诉我们如何使用QXmlStreamReader解析一个专门的XML文件格式(图1的bookindex.xml)并在QTreeWidget中显示其内容。所解析的是那种具有书刊索引目录且包含索引条目和子条目的文档格式。下图是在QTreeWidget中显示的书刊索引文件。
1、QtXml库
在这个应用程序中使用的QXmlStreamReader类是QtXml库中的一部分。必须在.pro文件中加入如下一行命令:
QT += xml
在代码中添加头文件:
#include <QXmlStreamReader>
2、主程序代码
首先查看应用程序中XML阅读器在上下文中是如何使用的。
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
ui->treeWidget->setColumnCount(2);
QStringList header;
header<<"Terms"<<"Pages";
ui->treeWidget->setHeaderLabels(header);
XmlStreamReader reader(ui->treeWidget);
reader.readFile("bookindex.xml");
}
在图中显示的应用程序界面中创建一个QTreeWidget。之后,这个应用程序创建一个XmlStreamReader,并将树形窗口部件值传递给该XmlStreamReader,并要求它解析所指定的一个文件。
3、XmlStreamReader类的定义
然后,我们将查看阅读器的实现代码。
class XmlStreamReader
{
public:
XmlStreamReader(QTreeWidget *tree);
bool readFile(const QString &fileName);
private:
void readBookindexElement();
void readEntryElement(QTreeWidgetItem *parent);
void readPageElement(QTreeWidgetItem *parent);
void skipUnknownElement();
QTreeWidget *treeWidget;
QXmlStreamReader reader;
};
XmlStreamReader类提供了两个公共函数:构造函数和readFile()函数。这个类使用QXmlStreamReader解析XML文件,并配合QTreeWidget窗口以反映其读入的XML数据。通过使用向下递归的方法来实现这一解析过程。
- readBookindexElement()解析一个含有0或0个以上<entry>元素的<bookindex>…</bookindex>元素。
- readEntryElemen()解析一个含有0或0个以上<page>元素的<entry>…</entry>元素,以及嵌套任意层次的含有0或0个以上<entry>元素。
- readPageElement()解析一个<page> …</page>元素。
- skipUnknownElement()跳过不能识别的元素。
4、XmlStreamReader类的实现
现在看看XmlStreamReader类的实现,由构造函数开始。
XmlStreamReader::XmlStreamReader(QTreeWidget *tree)
{
treeWidget=tree;
}
构造函数只是用来建立阅读器将使用的那个QTreeWidget。所有的操作都将在readFile()函数中完成(由main()函数调用)。
bool XmlStreamReader::readFile(const QString &fileName)
{
QFile file(fileName);
if(!file.open(QFile::ReadOnly | QFile::Text)){ // (a)
std::cerr<<"Error: Cannot read file "<<qPrintable(fileName)
<<": "<<qPrintable(file.errorString())<<std::endl;
return false;
}
reader.setDevice(&file);
reader.readNext(); // (b)
while (!reader.atEnd()) {
if(reader.isStartElement()){
if(reader.name()=="bookindex"){ // (c)
readBookindexElement();
}else{
reader.raiseError(QObject::tr("Not a bookindex file"));
}
}else{
reader.readNext();
}
}
file.close();
if(reader.hasError()){ // (d)
std::cerr<<"Error: Failed to parse file"
<<qPrintable(fileName)<<": "
<<qPrintable(reader.errorString())<<std::endl;
return false;
}else if(file.error()!=QFile::NoError){
std::cerr<<"Error: Cannot read file "<<qPrintable(fileName)
<<": "<<qPrintable(file.errorString())<<std::endl;
return false;
}
return true;
}
其中:
- (a):readFile()函数首先会尝试打开文件。如果失败,则会输出一条出错信息并返回false值;如果成功,则它将被设置为QXmlStreamReader的输人设备。
- (b):QXmlStreamReader的readNext()函数从输人流中读取下一个记号。如果成功而且还没有到达XML文件的结尾,函数将进人循环。由于索引文件的结构,我们知道在该循环内部只有三种可能性发生:<bookindex>开始标签正好被读入;另一个开始标签正好被读入(在这种情况下,读取的文件不是一个书刊索引);读入的是其他种类的记号。
- (c):如果有正确的开始标签,就调用readBookindexElement()继续完成处理。否则,就调用QXmlStreamReader::raiseError()并给出出错信息。下一次(在while循环条件下)调用atEnd()时,它将返还true值。这就确保了解析过程可以在遇到错误时能尽快停止。通过对QFile调用error()和errorString(),就可以在稍后查询这些出错信息。当在书刊索引文件中检测到有错误时,也会立即返回一个类似的出错信息。其实,使用raiseError()通常会更加方便,因为它对低级的XML解析错误和与应用程序相关的错误使用了相同的错误报告机制,而这些低级的XML解析错误会在QXmlStreamReader运行到无效的XML时就自动出现。
- (d):一旦处理完成,就会关闭文件,如果存在解析器错误或者文件错误,该函数就输出一个出错信息并返回false值;否则,返回true值并报告解析成功。
void XmlStreamReader::readBookindexElement()
{
reader.readNext();
while(!reader.atEnd()){
if(reader.isEndElement()){
reader.readNext();
break;
}
if(reader.isStartElement()){
if(reader.name()=="entry"){
readEntryElement(treeWidget->invisibleRootItem());
}else{
skipUnknownElement();
}
}else{
reader.readNext();
}
}
}
readBookindexElement()的作用就是读取文件的主体部分。它首先跳过当前的记号(此处只可能是<bookindex>开始标签),然后遍历读取整个输人文件。
如果读取到了关闭标签,那么它只可能是</bookindex>标签,否则QXmlStreamReader早就已经报告出错。如果是那样的话,就跳过这个标签并跳出循环。否则将应该有一个顶级索引<entry>开始标签。如果情况确实如此,调用readEntryElement()来处理条目数据;不然,就调用skipUnknownElement()。使用skipUnknownElement()而不调用raiseError(),意味着如果要在将来扩展书刊索引格式以包含新的标签的话这个阅读器将继续存效,因为它仅忽略了不能识别的标签。
readEntryElement()具有一个确认父对象条目的QTreeWidgetItem *参数。我们将QTreeWidget::invisibleRootItem()作为父对象项传递,以使新的项以其为根基。在readEntryElement()中,用一个不同的父对象项循环调用readEntryElement()。
void XmlStreamReader::readEntryElement(QTreeWidgetItem *parent)
{
QTreeWidgetItem *item = new QTreeWidgetItem(parent);
item->setText(0,reader.attributes().value("term").toString());
reader.readNext();
while(!reader.atEnd()) {
if (reader.isEndElement()) {
reader.readNext();
break;
}
if (reader.isStartElement()) {
if (reader.name() == "entry") {
readEntryElement(item);
} else if (reader.name() == "page") {
readPageElement(item);
} else {
skipUnknownElement();
}
} else {
reader.readNext();
}
}
}
每当遇到一个<entry>开始标签时,就会调用readEntryElement()函数。我们希望为每一个索引条目创建一个树形的窗口部件项,因此创建一个新的QTreeWidgetltem,并将其第一列的文本值设置为条目的项属性文本。
一旦条目被添加到树中,就开始读取下一个记号。如果这是一个关闭标签,就跳过该标签并跳出循环。如果遇到的是开始标签,那么它可能是<entry>标签(表示一个子条目),<page>标签 (该条目项的页码数),或者是一个未知的标签。如果开始标签是一个子条,就递归调用readEntryElement()。如果该标签是<page>标签,就调用readPageElement()。
void XmlStreamReader::readPageElement(QTreeWidgetItem *parent)
{
QString page = reader.readElementText();
if (reader.isEndElement())
reader.readNext();
QString allPages = parent->text(1);
if (! allPages.isEmpty())
allPages += ", ";
allPages += page;
parent->setText(1, allPages);
}
只要读取的是<page>标签,就调用readPageElement()函数。被传递的正是符合页码文本所属条目的树项。我们从读取<page>和</page>标签之间的文本开始。成功读取完以后,readElementText()函数将让解析器停留在必须跳过的</page>标签上。
这些页被存储在树形窗口部件项的第二列。我们首先提取那里已有的文本。如果文本不为空值,就在其后添加一个逗号,为新页的文本做好淮备。然后,添加新的文本并相应地更新该列的文本。
void XmlStreamReader::skipUnknownElement()
{
reader.readNext();
while (!reader.atEnd()) {
if (reader.isEndElement()) {
reader.readNext();
break;
}
if (reader.isStartElement()) {
skipUnknownElement();
} else {
reader.readNext();
}
}
}
最后,当遇到未知的标签时,将继续读取,直到读取到也将跳过的未知元素的关闭标签为止。这意味着我们将跳过那些具有良好形式但却无法识別的元素,并从XML文件中读取尽可能多的可识別的数据。
——————————————————
对于本文实例完整代码有需要的朋友,可关注并在评论区留言!