QXmlStreamReader
QXmlStreamReader类通过简单的流式API为我们提供了一种快速的读取xml文件的方式。他比Qt自己使用的SAX解析方式还要快。
所谓的流式读取即将一个xml文档读取成一系列标记的流,类似于SAX。而QXmlStreamReader类和SAX的主要区别就是解析这些标记的方式。使用SAX解析时,应用程序必须提供一些处理器(回调函数)来处理来自解析器的一系列的所谓的xml事件,不同的xml标记会触发不同的事件,从而进行相应的处理。而使用QXmlStreamReader,应用程序自己可以驱动整个循环,从解析器中一个接一个的拉取出xml标记。这个动作可以通过readNext()来完成,该函数会读取出下一个完整的标记,然后其tokenType()。然后,我们就可以使用一系列方便的函数,如isStartElement(),text()等来确定或得到具体所读取的内容。这种拉模式(pull)的解析方法的好处就在于,我们可以将对一个xml文档的解析分开到多个函数中来完成,对不同的标记使用一个单独的函数来处理。
QXmlStreamReader类的典型使用方法如下:
QXmlStreamReader xml;
...
while (!xml.atEnd()) {
xml.readNext();
... // do processing
}
if (xml.hasError()) {
... // do error handling
}
如果在解析的过程中出现了错误,atEnd()和hasError()会返回true,error()会返回所出现的具体错误类型。errorString(),lineNumber(),columnNumber()和characterOffset()函数可以用来得到错误的具体信息,一般我们使用这几个函数来构建一个错误字符串来提示用户具体的错误信息。同时,为了简化应用程序代码,QXmlStreamReader还提供了一个raiseError()的机制,可以让我们在必要时触发一个自定义的错误信息。
QXmlStreamReader是一个增量式的解析器。它可以处理文档不能被一下处理完的情况,比如该xml文件来自于多个文件或来自于网络。当QXmlStreamReader解析完所有的数据但该xml文档是不完整的,这时它会返回一个PrematureEndOfDocumentError类型的错误。然后,当有更多的数据到来时,它会从这个错误中恢复,然后继续调用readNext()来解析新的数据。
还有,QXmlStreamReader是不太消耗内存的,因为它不会在内存中存储整个xml文档树,仅仅存储当前它所解析的标记。此外,QXmlStreamReader使用QStringRef来解析所有的字符串数据而不是真实的QString对象,这可以避免不必要的小字符串内存分配代价。QStringRef是对QString或其子串的一个简单包装,并提供了一些类似于QString类的API,但它不会进行内存的分配,并在底层使用了引用计数来共享数据。我们可以在需要时,调用QStringRef的toString()来得到一个真实的QString对象。
读取XML文件
实例1
这是一个基于控制台的应用程序,解析XML格式的文件
xml文件如下,命名为labels.xml
<?xml version="1.0" encoding="UTF-8"?>
<labels map="demo1" ver="1.0">
<label id="1802232">
<x>1568</x>
<y>666</y>
</label>
<label id="1802230">
<x>1111</x>
<y>622</y>
</label>
</labels>
main.cpp
#include <QtCore/QCoreApplication>
#include "xmlreader.h"
#include <iostream>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
xmlreader reader;
reader.readFile("labels.xml");
return a.exec();
}
xmlreader.h
#ifndef XMLREAGER_H
#define XMLREAGER_H
#include <QXmlStreamReader>
class xmlreader
{
public:
xmlreader();
bool readFile(const QString &fileName);
private:
void readlabelsElement(); //读取label标签
void readlabelElement(); //读取label标签
void readxElement(); //读取x标签
void readyElement(); //读取y标签
void skipUnknownElement(); //跳过未知标签
QXmlStreamReader reader;
};
#endif // XMLREAGER_H
xmlreader.cpp
#include "xmlreader.h"
#include <iostream>
#include <QDebug>
#include <QFile>
xmlreader::xmlreader()
{
}
bool xmlreader::readFile(const QString &fileName)
{
//以只读和文本方式打开文件,如果打开失败输出错误日志,并且返回false
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;
}
//将文件设置成xml阅读器的输入设备
reader.setDevice(&file);
reader.readNext(); //直接读取下一个节点,因为首先读到的标签是XML文件的头部(第一行)
while (!reader.atEnd()) //外部循环,未到文件结尾就一直循环读取
{
if (reader.isStartElement()) //外部分支,如果不是起始标签,则直接读取下一个节点
{
if (reader.name() == "labels") //内部分支,如果根节点不是 == labels,
//说明读取的文件是错误的
{
qDebug() << reader.name();//通过qDebug()输出当前节点的名字,这里输出labels
readlabelsElement(); //读取labels节点的内容
}
else
{ //raiseError()函数用来自定义输出错误日志的内容,这里输出Not a labels file
reader.raiseError(QObject::tr("Not a labels file"));
}
}
else
{
reader.readNext();
}
}
//关闭文件,如果读取发生错误(hasError())或者文件有错误,输出错误信息,返回false,
file.close();
if (reader.hasError()) {
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;
}
void xmlreader::readlabelsElement()
{
reader.readNext();//读取了根节点labels后,继续读取下一个节点
while (!reader.atEnd())
{
if (reader.isEndElement())
{
reader.readNext();
break; //如果是结束节点,则结束循环
//循环执行下去,读到的第一个结束节点是</labels>,而不是</label>;
//这是执行readlabelElement()函数中得到的结果,当读到</label>时,
//该函数跳出循环并读取下一个节点,而下一个节点是<label>或者</labels>
}
if (reader.isStartElement())
{
if (reader.name() == "label")
{ //获得label的attributes()值,也就是id,转换成字符串输出
qDebug() << reader.attributes().value("id").toString();
qDebug() << reader.name();
readlabelElement();
}
else
{
skipUnknownElement();//未知节点直接跳过
}
}
else
{
reader.readNext();
}
}
}
void xmlreader::readlabelElement()
{
reader.readNext();
while (!reader.atEnd())
{
if (reader.isEndElement())
{
reader.readNext();
break;
}
if (reader.isStartElement())
{
if (reader.name() == "x")
{
readxElement();
}
else if (reader.name() == "y")
{
readyElement();
}
else
{
skipUnknownElement();
}
}
else
{
reader.readNext();
}
}
}
void xmlreader::readxElement()
{
QString x = reader.readElementText();
qDebug() <<"x:" << x;
if (reader.isEndElement())
reader.readNext();
}
void xmlreager::readyElement()
{
QString y = reader.readElementText();//执行这个函数以后,y获得了坐标值,并且当前节点
//自动变成结束节点</y>
qDebug() << "y:" << y;
if (reader.isEndElement())
reader.readNext(); //在这里,读取下一个节点,就是</label>
}
//是一个递归函数
void xmlreader::skipUnknownElement()
{
reader.readNext();
while (!reader.atEnd()) {
if (reader.isEndElement()) {
reader.readNext();
break;
}
if (reader.isStartElement()) {
skipUnknownElement();//函数的递归调用
} else {
reader.readNext();
}
}
}
实例2
XML文件的意思是:学校(school)三楼(floor3)的老师信息,还有一个学生的信息。
结构比较常见,但是数据很粗糙。大家主要看结构。
XML文件的名字 teachers.xml
XML文件的内容,6个老师,1个学生
<?xml version="1.0" ?>
<school>
<floor3 id="3" time="2019/10/11">
<teacher>
<entry name="Job">
<age>30</age>
<sport>soccer</sport>
</entry>
<entry name="Tom">
<age>32</age>
<sport>swimming</sport>
</entry>
</teacher>
<teacher>
<entry name="Job2">
<age>30</age>
<sport>soccer</sport>
</entry>
<entry name="Tom">
<age>32</age>
<sport>swimming</sport>
</entry>
</teacher>
<teacher>
<entry name="Job3">
<age>30</age>
<sport>soccer</sport>
</entry>
<entry name="Tom">
<age>32</age>
<sport>swimming</sport>
</entry>
</teacher>
<teacher>
<entry name="Job4">
<age>30</age>
<sport>soccer</sport>
</entry>
<entry name="Tom">
<age>32</age>
<sport>swimming</sport>
</entry>
</teacher>
<teacher>
<entry name="Job5">
<age>30</age>
<sport>soccer</sport>
</entry>
<entry name="Tom">
<age>32</age>
<sport>swimming</sport>
</entry>
</teacher>
<student>
<entry name="Lily">
<age>20</age>
<sport>dancing</sport>
</entry>
<entry name="Keith">
<age>21</age>
<sport>running</sport>
</entry>
</student>
<teacher>
<entry name="Job6">
<age>30</age>
<sport>soccer</sport>
</entry>
<entry name="Tom">
<age>32</age>
<sport>swimming</sport>
</entry>
</teacher>
</floor3>
</school>
注意:
重点在函数中的代码,移植出来可以使用。
记得将“用来计数”的变量,进行整理。
还有文件所在地址,也要更换。
将XML的文件内容,首先读取到一个变量中,再分析这个变量的内容。
受XML文件的编码格式影响很大,如果有中文乱码的现象出现,要慎重使用这种方法,可能无法读取。
#include <QtCore/QCoreApplication>
#include <QXmlStreamReader>
#include <QFile>
#include <iostream>
void ReadXml()
{
//用来计数
int teacherCount = 0;
int ageCount = 0;
int sanlouCount = 0;
int schoolCount = 0;
//读取文件
QString fileName = "D:/JBXML/teachers.xml";
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text))
{
return ;
}
//QXmlStreamReader操作任何QIODevice.
QXmlStreamReader xml(&file);
//解析XML,直到结束
while (!xml.atEnd() && !xml.hasError())
{
//读取下一个element.
QXmlStreamReader::TokenType token = xml.readNext();
/*以下内容用于分析读取的内容,可以将每一个读取到的标签名字打印出来*//*
if (token == QXmlStreamReader::Invalid)
{
//如果有读取错误,将被打印出来
std::cout << xml.errorString().toStdString();
}
std::cout << xml.tokenString().toStdString() << "\t";
std::cout << xml.name().toString().toStdString() << std::endl;*/
/*显示这个分析过程,你会看到很清晰的读取过程*/
//如果获取的仅为StartDocument,则进行下一个
if (token == QXmlStreamReader::StartDocument)
{
continue;
}
//如果获取了StartElement,则尝试读取
if (token == QXmlStreamReader::StartElement)
{
//如果为person,则对其进行解析
if (xml.name() == "teacher")
{
teacherCount++;
}
if (xml.name() == "age")
{
ageCount++;
}
if (xml.name() == "floor3")
{
sanlouCount++;
}
if (xml.name() == "school")
{
schoolCount++;
}
}
}
if (xml.hasError())
{
//QMessageBox::information(NULL, QString("parseXML"), xml.errorString());
}
file.close();
std::cout << teacherCount << " teacher" << std::endl;
std::cout << ageCount << " ages" << std::endl;
std::cout << sanlouCount << " 3rdFloors" << std::endl;
std::cout << schoolCount << " schools" << std::endl;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
ReadXml();
return a.exec();
}
本程序,可以得到XML文件中,各个标签tag, 即尖括号内的信息,然后进行判断。
如果是“teacher”,会计数,然后可以看到,总共有6个老师。
直接运行结果如下:
如果把中间注释的代码打开,可以看到每一个读取到的标签,并将读取过程打印出来。
运行结果如下:
示例3
考虑如下 XML 片段:
<doc>
<quote>Einmal ist keinmal</quote>
</doc>
一次解析过后,我们通过readNext()的遍历可以获得如下信息:
StartDocument
StartElement (name() == "doc")
StartElement (name() == "quote")
Characters (text() == "Einmal ist keinmal")
EndElement (name() == "quote")
EndElement (name() == "doc")
EndDocument
通过readNext()函数的循环调用,我们可以使用isStartElement()、isCharacters()这样的函数检查当前读取的类型,当然也可以直接使用state()函数。
<?xml version="1.0" encoding="utf-8"?>
<bookindex> <!--根标签-->
<entry term="叶节点0">
<page>10</page>
<page>34-35</page>
<page>307-308</page>
</entry>
<entry term="叶节点1">
<entry term="叶节点1.1">
<page>115</page>
<page>244</page>
</entry>
<entry term="叶节点1.2">
<page>9</page>
</entry>
</entry>
<entry term="叶节点2">
<entry term="叶节点2.1">
<page>115</page>
<page>244</page>
</entry>
<entry term="叶节点2.2">
<page>9</page>
</entry>
</entry>
</bookindex>
首先来看头文件:
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
bool readFile(const QString &fileName);
private:
void readBookindexElement();
void readEntryElement(QTreeWidgetItem *parent);
void readPageElement(QTreeWidgetItem *parent);
void skipUnknownElement();
QTreeWidget *treeWidget;
QXmlStreamReader reader;
private:
Ui::MainWindow *ui;
};
MainWindow显然就是我们的主窗口,即构造函数
setWindowTitle(tr("XML Reader"));
treeWidget = new QTreeWidget(this);
QStringList headers;
headers << "Items" << "Pages";
treeWidget->setHeaderLabels(headers);
setCentralWidget(treeWidget);
readFile()
QFile file(QApplication::applicationDirPath() + "/demo.xml");
if (!file.open(QFile::ReadOnly | QFile::Text))
{
QMessageBox::critical(this, tr("Error"),
tr("Cannot read file %1").arg(fileName));
return false;
}
reader.setDevice(&file);
while (!reader.atEnd())
{
if (reader.isStartElement()) //检测当前读取类型 {
qDebug()<<"2222222222222222";
if (reader.name() == "bookindex")
{
readBookindexElement();//递归下降算法,层层读取
}
else
{
reader.raiseError(tr("Not a valid book file"));
}
}
else
{
qDebug()<<"111111111111111";
reader.readNext(); //循坏调用首次移动3次,后面移动一次
}
}
file.close();
if (reader.hasError())
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to parse file %1").arg(fileName));
return false;
}
else if (file.error() != QFile::NoError)
{
QMessageBox::critical(this, tr("Error"),
tr("Cannot read file %1").arg(fileName));
return false;
}
return true;
readFile()函数用于打开给定文件。我们使用QFile打开文件,将其设置为QXmlStreamReader的设备。也就是说,此时QXmlStreamReader就可以从这个设备(QFile)中读取内容进行分析了。接下来便是一个 while 循环,只要没读到文件末尾,就要一直循环处理。首先判断是不是StartElement,如果是的话,再去处理 bookindex 标签。注意,因为我们的根标签就是 bookindex,如果读到的不是 bookindex,说明标签不对,就要发起一个错误(raiseError())。如果不是StartElement(第一次进入循环的时候,由于没有事先调用readNext(),所以会进入这个分支),则调用readNext()。为什么这里要用 while 循环,XML 文档不是只有一个根标签吗?直接调用一次readNext()函数不就好了?这是因为,XML 文档在根标签之前还有别的内容,比如声明,比如 DTD,我们不能确定第一个readNext()之后就是根标签。正如我们提供的这个 XML 文档,首先是 声明,其次才是根标签。如果你说,第二个不就是根标签吗?但是 XML 文档还允许嵌入 DTD,还可以写注释,这就不确定数目了,所以为了通用起见,我们必须用 while 循环判断。处理完之后就可以关闭文件,如果有错误则显示错误。
void MainWindow::readBookindexElement()
{
Q_ASSERT(reader.isStartElement() && reader.name() == "bookindex");//不是则会报错
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();
}
}
}
注意第一行我们加了一个断言。意思是,如果在进入函数的时候,reader 不是StartElement状态,或者说标签不是 bookindex,就认为出错。然后继续调用readNext(),获取下面的数据。后面还是 while 循环。如果是EndElement,退出,如果又是StartElement,说明是 entry 标签(注意我们的 XML 结构,bookindex 的子元素就是 entry),那么开始处理 entry,否则跳过。
那么下面来看readEntryElement()函数:
void MainWindow::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();
}
}
}
这个函数接受一个QTreeWidgetItem指针,作为根节点。这个节点被当做这个 entry 标签在QTreeWidget中的根节点。我们设置其名字是 entry 的 term 属性的值。然后继续读取下一个数据。同样使用 while 循环,如果是EndElement就继续读取;如果是StartElement,则按需调用readEntryElement()或者readPageElement()。由于 entry 标签是可以嵌套的,所以这里有一个递归调用。如果既不是 entry 也不是 page,则跳过位置标签。
然后是readPageElement()函数:
void MainWindow::readPageElement(QTreeWidgetItem *parent)
{
QString page = reader.readElementText();
if (reader.isEndElement())
{
qDebug()<<"3333333333333333";
reader.readNext();
}
QString allPages = parent->text(1);
if (!allPages.isEmpty())
{
allPages += ", ";
}
allPages += page;
parent->setText(1, allPages);
}
由于 page 是叶子节点,没有子节点,所以不需要使用 while 循环读取。我们只是遍历了 entry 下所有的 page 标签,将其拼接成合适的字符串。
skipUnknownElement()函数
void MainWindow::skipUnknownElement()
{
reader.readNext();
while (!reader.atEnd())
{
if (reader.isEndElement())
{
reader.readNext();
break;
}
if (reader.isStartElement())
{
skipUnknownElement();
}
else
{
reader.readNext();
}
}
}
我们没办法确定到底要跳过多少位置标签,所以还是得用 while 循环读取,注意位置标签中所有子标签都是未知的,因此只要是StartElement,都直接跳过。