本帖最后由 baizy77 于 2019-9-2 16:19 编辑
---------------------------------------------------------------- 作者: 女儿叫老白 转载请注明出处! ----------------------------------------------------------------
---------------------------------------------------------------- 引言---------------------------------------------------------------- 前面的章节中我们介绍了使用DOM方式读写XML文件。但是使用DOM方式有一个明显的缺陷,它在读取XML文件过程中需要在内存中建立DOM树结构。也就是要把DOM节点在内存中创建出来,这导致内存占用非常大,如果解析一个稍微大些的XML文件,内存占用可能非常惊人,而且性能要差些。本节我们为朋友们介绍使用流方式访问XML文件的方法。
学习建议 ---------------------------------------------------------------- 对照本节配套的源代码进行学习。
正文 ---------------------------------------------------------------- 使用流方式读取XML需要用到QXmlStreamReader;保存XML文件用QXmlStreamWriter。 QXmlStreamReader解析XML文件的特点是,只能从前向后解析XML文件,不能像前面章节用DOM方式那样,得到整个节点树或者某个节点的属性列表进行任意顺序访问。 我们仍然用两个例子来展示读写XML文件,为了进行对比,我们写的XML文件内容同前面用DOM方式保存的文件基本一致。
- <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <!--软件特攻队-->
- <doc>
- <courses count="4" institution="软件特攻队" teacher="女儿叫老白">
- <lesson url="https://study.163.com/instructor/1143174933.htm" id="1" fee="免费">C++老鸟日记</lesson>
- <lesson url="https://study.163.com/instructor/1143174933.htm" id="2" fee="免费">C++跨平台开发中的编译错误</lesson>
- <lesson url="https://study.163.com/instructor/1143174933.htm" id="3">Qt入门与提高-GUI产品开发</lesson>
- <lesson url="sorry, not ready" id="4">C++跨平台服务模块开发</lesson>
- </courses>
- </doc>
复制代码 现在进入开发环节。 同样的,首先要在pro中增加对xml模块的包含:
保存XML文件-QXmlStreamWriter 先设置XML文件的保存路径,并自动添加路径最后的斜杠。 代码清单04-19-01 main.cpp
- void example01() {
- QString strPath =
- ns_train::getPath("$TRAINDEVHOME/test/chapter04/ks04_20/");
- if (!strPath.endsWith("/")) {
- strPath += "/";
- }
- //......
- }
复制代码 下列保存xml的代码均位于example01接口内。 然后,用如下代码自动创建该目录。
- QDir dir(strPath);
- dir.mkpath(strPath); // mkpath会自动创建strPath的整个目录层次。
复制代码 然后,组织文件名,并创建文件对象:
- QString strFileName = strPath + "test04_20.xml"; // 程序运行目录下的xml文件
- QFile file(strFileName);
- if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
- // QFile::Truncate,需要清空原来的内容
- return ;
- }
复制代码下面开始创建XML流对象并设置编码、版本:
- QXmlStreamWriter writer(&file);
- writer.setCodec("UTF-8"); // XML 编码设置为UTF-8
- writer.setAutoFormatting(true); // 自动格式化
- writer.writeStartDocument("1.0", true); // 开始文档(XML 声明)
复制代码 读者可以将自动格式化设置为false,然后看看保存的XML文件有何不同。 然后我们添加一行注释:
- writer.writeComment(QString::fromLocal8Bit("软件特攻队")); // 注释
复制代码开始写入根元素:
- writer.writeStartElement("doc"); // 开始根元素 <doc>
复制代码开始写入doc的第一个子元素<courses>:
- writer.writeStartElement("courses"); // 开始子元素 <courses>
复制代码为courses元素设置属性:
- writer.writeAttribute("count", "4");
- writer.writeAttribute("institution", QString::fromLocal8Bit("软件特攻队"));
- writer.writeAttribute("teacher", QString::fromLocal8Bit("女儿叫老白"));
复制代码开始写入courses元素的第一个子元素:
- // lesson
- writer.writeStartElement("lesson"); // 开始子元素 <lesson>
- writer.writeAttribute("url", "https://study.163.com/instructor/1143174933.htm");
- writer.writeAttribute("id", "1");
- writer.writeAttribute("fee", QString::fromLocal8Bit("免费"));
复制代码 然后写入lesson的文本子元素:
- writer.writeCharacters(QString::fromLocal8Bit("C++老鸟日记"));
复制代码 接着,完成第一个lesson子元素:
- writer.writeEndElement(); // 结束子元素 </lesson>
复制代码 从上面的代码可以看出,writeEndElement()接口并未提供任何参数。因此,该接口内部实现是根据之前的代码产生的element堆栈(队列)自动添加结束元素。这就像括号嵌套一样,这里相当于添加一个右括号,它对应的是跟他配对的左括号。比如,之前最后一个元素为lesson,那么此时调用writeEndElement()接口就表示结束lesson元素(即</lesson>标签)。 然后,我们循环写入另外几个lesson。在此不再赘述。
写入courses元素的结束标签,需要使用如下语句:
- writer.writeEndElement(); // 结束子元素 </courses>
复制代码 这里也是调用writeEndElement()接口。如前所述,它结束的元素是跟他配对的startElement,也就是courses。 最后,我们写入doc的结束元素:
- writer.writeEndElement(); // 结束子元素 </doc>
复制代码 当所有元素结束后,我们写入文档的结束标志并关闭文档: - writer.writeEndDocument(); // 结束文档
- file.close();
复制代码
读取XML文件-QXmlStreamReader 读取XML文件时,也需要构造文件对象: 代码清单04-19-02 mian.cpp - void example02() {
- QString strPath =
- ns_train::getPath("$TRAINDEVHOME/test/chapter04/ks04_20/");
- if (!strPath.endsWith("/")) {
- strPath += "/";
- }
- // 程序运行目录下的xml文件
- QString strFileName = strPath + "test04_20.xml";
- QFile file(strFileName);
- if (!file.open(QFile::ReadOnly | QFile::Text)) {
- return;
- }
- //......
- }
复制代码 代码清单04-19-02的代码跟保存XML文件时的代码类似,不再详述。 然后构建读取XML文件用的流对象: - QXmlStreamReader reader(&file);
复制代码 接着,进入主循环体进行遍历: 代码清单04-19-03 main.cpp
- QXmlStreamReader::TokenType nType = reader.readNext();
- while (!reader.atEnd()) {
- // 读取下一个元素
- nType = reader.tokenType();
- switch (nType) {
- //......
- default:
- break;
- }
- reader.readNext();
- }
- }
复制代码 代码清单04-19-03中: 在第4行,我们先得到元素的类型,reader.tokenType()返回的类型为QXmlStreamReader::TokenType。 当reader对象打开XML文件后,默认指向第一行,此时,reader指向文档开头,其类型为QXmlStreamReader::StartDocument。这时可以读取XML的版本号、XML编码以及Standalone信息。其中Standalone表示本XML是否为自包含的,为false则表示需要引用外部的信息。这就像C++中包含头文件一样。 然后,我们调用reader.readNext(),将reader移动到下一个对象。下一个对象是XML文件的第一个元素:一个注释: 代码清单04-19-04 main.cpp
- case QXmlStreamReader::Comment: { // 注释
- QString strComment = reader.text().toString();
- break;
- }
复制代码 代码清单04-19-04中: 第2行的reader.text()得到的是一个QStringRef对象,它并不是一个真正的QString对象,因此需要继续转换,方法是调用toString()接口。在switch语句之后,我们调用reader.readNext()。 当继续读取到<doc>时,这是一个QXmlStreamReader::StartElement。与之对应的是</doc>,称之为EndElement.当读取到StartElement时我们需要验证是否读取到的是doc元素: 代码清单04-19-05 main.cpp
- case QXmlStreamReader::StartElement: { // 开始元素
- QString strElementName = reader.name().toString();
- if (QString::compare(strElementName, "doc") == 0) { // 根元素
- parseDoc(reader);
- }
- break;
- }
复制代码 代码清单04-19-05中: 第3行,用来判断是否为doc元素,如果是则调用parseDoc()接口继续解析。 现在我们来看一下parseDoc()接口。parseDoc()内部很简单,因为我们的XML文件里doc元素很简单,没有并列的兄弟元素,只有子元素。该接口内部也是遍历: 代码清单04-19-06 main.cpp:parseDoc()
- void parseDoc(QXmlStreamReader& reader) {
- QXmlStreamReader::TokenType nType = reader.readNext();
- while (!reader.atEnd()) {
- nType = reader.tokenType();
- switch (nType) {
- case QXmlStreamReader::StartElement: { // 开始元素
- QString strElementName = reader.name().toString();
- if (QString::compare(strElementName, "courses") == 0) {
- qDebug() << QString::fromLocal8Bit("============== 开始元素<courses> ==============");
- // 处理courses
- parseCourses(reader);
- }
- break;
- }
- case QXmlStreamReader::EndElement: { // 结束元素
- QString strElementName = reader.name().toString();
- qDebug() << QString::fromLocal8Bit("============= 结束元素<%1> =============").arg(strElementName);
- return;
- }
- default:
- break;
- }
- nType = reader.readNext();
- }
- }
复制代码 代码清单04-19-06中: 第2行,首先将reader指向下一个元素。然后,先判断XML文档是否已结束,否则遍历。 第3~24行,在遍历循环内部,此时reader指向doc的下一个元素:courses,其实它是doc元素的子元素。这时读取到的courses是一个StartElement。我们判断读取到的元素为"courses"之后,调用parseCourse(reader)继续对reader进行解析。 第25行,解析完毕后,在switch语句之后,循环体继续执行nType = reader.readNext(),此时将读取到doc的结束元素</doc>,请注意,我们这些元素的判断跟代码的执行过程有直接关系。读者需要根据设计来编写代码,比如,您希望在哪里处理对应的结束元素,就需要把代码写在哪里。我们这里是希望在parseCourse()接口内部处理完courses的结束元素,因此当调用parseCourse()接口结束后,进入上面代码的循环时,reader.readNext()将reader对象指向的就是doc的结束元素</doc>。 下面,我们看一下parseCourse()的实现。 代码清单04-19-07 main.cpp:parseCourses
- void parseCourses(QXmlStreamReader& reader) {
- // 将reader指向lesson子元素.
- QXmlStreamReader::TokenType nType = reader.readNext();
- while (!reader.atEnd()) {
- nType = reader.tokenType();
- switch (nType) {
- case QXmlStreamReader::StartElement: { // 开始元素
- QString strElementName = reader.name().toString();
- if (QString::compare(strElementName, "lesson") == 0) {
- // lesson元素
- qDebug() << QString::fromLocal8Bit("== 开始元素<lesson> ==");
- // 解析lesson
- parseLesson(reader);
- continue;
- }
- break;
- }
- case QXmlStreamReader::EndElement: { // 结束元素
- QString strElementName = reader.name().toString();
- qDebug() << QString::fromLocal8Bit("== 结束元素<%1>==")
- .arg(strElementName);
- if (QString::compare(strElementName, "courses") == 0) {
- // 结束元素
- return;
- }
- break;
- }
- default:
- break;
- }
- nType = reader.readNext();
- }
- }
复制代码 在代码清单04-19-07中, 该接口内部也是一个循环体。当首次进入该接口时,reader指向courses元素。因此,第3行处的代码得到的是courses下一个元素的类型,下一个元素就是courses的第一个子元素:lesson,这里读取到的是一个开始元素StartElement。 第9行,我们对该元素进行判断,如果是lesson则调用parseLesson()进行解析。 在18~27行,在switch内部,是处理结束元素的的代码。如果读取到的结束元素不是"courses"(,那么就是</doc>),就应该返回。 第31行,在switch语句之后,继续调用reader.readNext()指向下一个元素。
接着,我们看一下parseLesson()接口: 代码清单04-19-08 main.cpp:parseLesson - void parseLesson(QXmlStreamReader& reader) {
- QXmlStreamReader::TokenType nType;
- QString strElementName;
- QXmlStreamAttributes attributes;
- QXmlStreamAttributes::iterator iteAttribute;
- QString strText;
- while (!reader.atEnd()) {
- nType = reader.tokenType();
- switch (nType)
- {
- case QXmlStreamReader::StartElement:
- strElementName = reader.name().toString();
- attributes = reader.attributes();
- for (iteAttribute= attributes.begin();
- iteAttribute!= attributes.end();
- iteAttribute++) {
- qDebug() << (*iteAttribute).name()
- << "=" << (*iteAttribute).value();
- }
- break;
- case QXmlStreamReader::EndElement:
- strElementName = reader.name().toString();
- if (strElementName != "lesson") {
- return;
- }
- qDebug() << QString::fromLocal8Bit("== 结束元素:%1 ==")
- .arg(strElementName);
- break;
- case QXmlStreamReader::Characters:
- strText = reader.text().toString();
- qDebug() << QString("Characters:%1").arg(strText);
- break;
- default:
- break;
- }
- nType = reader.readNext();
- }
- }
复制代码 代码清单04-19-08中: parseLesson()接口内部也是循环体,通过第36行的reader.readNext()调整reader的游标。 当刚进入parseLesson()接口时,reader的游标指向的是lesson元素,因此,循环体内部直接取元素类型:reader.tokenType()。此时读取到的应该是lesson的开始元素StartElement,也就是<lesson>。 因此,第13行,可以通过reader.attributes()接口获取该元素的属性值的集合,得到的是一个QXmlStreamAttributes类型的对象。 第14~19行,使用迭代器可以遍历该集合: 通过迭代器对象访问属性名:(*iteAttribute).name()。 通过迭代器对象访问属性的值:(*iteAttribute).value()。 第36行,循环体继续执行:reader.readNext(),此时reader对象指向lesson的文本子元素,它的类型为:QXmlStreamReader::Characters。此时可以通过下列方法得到文本的内容: - QString strText = reader.text().toString();
复制代码 循环体继续执行reader.readNext(),则将reader指向lesson的结束元素</lesson>。当读取到最后一组lesson元素的</lesson>结束元素后,再次执行将读取到</courses>,此时退出parseLesson()接口,回到parseCourse()接口。而parseCourse()接口将读取到</courses>,退出parseCourse()接口,回到parseDoc()接口。 在parseDoc()接口中,继续循环将读取到作为EndElement对象的</doc>并退出parseDoc()接口。在最外层的解析接口中,读取到XML文档结尾,至此整个XML文档解析完毕。 结语 ---------------------------------------------------------------- 回顾一下本节的内容: 1. 首先要在pro中增加对xml模块的包含:
1. 流方式保存XML可以使用QXmlStreamWriter。 l 使用writeStartDocument()编写XML文档的头部。要编写配套的writeEndDocument()。 l 使用setCodec()设置XML文件的编码。 l 使用setAutoFormatting()设置自动缩进。 l 使用writeComment()添加注释。 l 使用writeStartElement()添加一个子元素,而且要编写配套的writeEndElement()。 l 使用writeAttribute()为元素设置属性,属性值的类型为字符串。 2. 流方式读取XML可以使用QXmlStreamReader。 l 使用readNext()将游标指向下一个对象。 l 使用while(!reader.atEnd())编写循环体。 l 使用tokenType()判断对象的类型。 l 使用name().toString()得到元素的名称。 l 使用text().toString()得到字符串类的对象对应的文本。 我们用流方式实现了XML文档的读写访问,使用这种方式将提高XML文档访问速度,并有效减少内存占用。XML文档中,还有很多其他对象我们没有涉及:比如DTD、命名空间等。随着朋友们对XML用法的深入,这些内容必然会接触到。但是我们要遵循循序渐进的原则,先把基本的理解了,再去研究更广泛的内容。
课程目录: 《Qt入门与提高-GUI产品开发》目录
|