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

KS04-20 用流方式读写XML

0
回复
3031
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2019-8-30 14:43:35 显示全部楼层 |阅读模式

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

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

x
本帖最后由 baizy77 于 2019-9-2 16:19 编辑

----------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白
转载请注明出处!
----------------------------------------------------------------


----------------------------------------------------------------
引言
----------------------------------------------------------------
    前面的章节中我们介绍了使用DOM方式读写XML文件。但是使用DOM方式有一个明显的缺陷,它在读取XML文件过程中需要在内存中建立DOM树结构。也就是要把DOM节点在内存中创建出来,这导致内存占用非常大,如果解析一个稍微大些的XML文件,内存占用可能非常惊人,而且性能要差些。本节我们为朋友们介绍使用流方式访问XML文件的方法。

学习建议
----------------------------------------------------------------
    对照本节配套的源代码进行学习。

正文
----------------------------------------------------------------
    使用流方式读取XML需要用到QXmlStreamReader;保存XML文件用QXmlStreamWriter。
    QXmlStreamReader解析XML文件的特点是,只能从前向后解析XML文件,不能像前面章节用DOM方式那样,得到整个节点树或者某个节点的属性列表进行任意顺序访问。
    我们仍然用两个例子来展示读写XML文件,为了进行对比,我们写的XML文件内容同前面用DOM方式保存的文件基本一致。

  1. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  2. <!--软件特攻队-->
  3. <doc>
  4.     <courses count="4" institution="软件特攻队" teacher="女儿叫老白">
  5.         <lesson url="https://study.163.com/instructor/1143174933.htm" id="1" fee="免费">C++老鸟日记</lesson>
  6.         <lesson url="https://study.163.com/instructor/1143174933.htm" id="2" fee="免费">C++跨平台开发中的编译错误</lesson>
  7.         <lesson url="https://study.163.com/instructor/1143174933.htm" id="3">Qt入门与提高-GUI产品开发</lesson>
  8.         <lesson url="sorry, not ready" id="4">C++跨平台服务模块开发</lesson>
  9.     </courses>
  10. </doc>
复制代码
    现在进入开发环节。
    同样的,首先要在pro中增加对xml模块的包含:

  1. QT                        += xml
复制代码
保存XML文件-QXmlStreamWriter
    先设置XML文件的保存路径,并自动添加路径最后的斜杠。
代码清单04-19-01
main.cpp

  1. void example01() {
  2. QString strPath =
  3.                ns_train::getPath("$TRAINDEVHOME/test/chapter04/ks04_20/");
  4.     if (!strPath.endsWith("/")) {
  5.             strPath += "/";
  6. }
  7. //......
  8. }
复制代码
    下列保存xml的代码均位于example01接口内。
    然后,用如下代码自动创建该目录。

  1. QDir dir(strPath);
  2. dir.mkpath(strPath); // mkpath会自动创建strPath的整个目录层次。
复制代码
   然后,组织文件名,并创建文件对象:

  1. QString strFileName = strPath + "test04_20.xml"; // 程序运行目录下的xml文件
  2. QFile file(strFileName);
  3. if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate))        {
  4. // QFile::Truncate,需要清空原来的内容
  5.                 return ;
  6. }
复制代码
下面开始创建XML流对象并设置编码、版本:

  1. QXmlStreamWriter writer(&file);
  2. writer.setCodec("UTF-8");       // XML 编码设置为UTF-8
  3. writer.setAutoFormatting(true); // 自动格式化
  4. writer.writeStartDocument("1.0", true);  // 开始文档(XML 声明)
复制代码
    读者可以将自动格式化设置为false,然后看看保存的XML文件有何不同。
    然后我们添加一行注释:

  1. writer.writeComment(QString::fromLocal8Bit("软件特攻队"));  // 注释
复制代码
开始写入根元素:

  1. writer.writeStartElement("doc");  // 开始根元素 <doc>
复制代码
开始写入doc的第一个子元素<courses>:

  1. writer.writeStartElement("courses");  // 开始子元素 <courses>
复制代码
为courses元素设置属性:

  1. writer.writeAttribute("count", "4");
  2. writer.writeAttribute("institution", QString::fromLocal8Bit("软件特攻队"));
  3. writer.writeAttribute("teacher", QString::fromLocal8Bit("女儿叫老白"));
复制代码
开始写入courses元素的第一个子元素:

  1. // lesson
  2. writer.writeStartElement("lesson");  // 开始子元素 <lesson>
  3. writer.writeAttribute("url", "https://study.163.com/instructor/1143174933.htm");
  4. writer.writeAttribute("id", "1");
  5. writer.writeAttribute("fee", QString::fromLocal8Bit("免费"));
复制代码
然后写入lesson的文本子元素:
  1. writer.writeCharacters(QString::fromLocal8Bit("C++老鸟日记"));
复制代码
    接着,完成第一个lesson子元素:

  1. writer.writeEndElement();  // 结束子元素 </lesson>
复制代码
    从上面的代码可以看出,writeEndElement()接口并未提供任何参数。因此,该接口内部实现是根据之前的代码产生的element堆栈(队列)自动添加结束元素。这就像括号嵌套一样,这里相当于添加一个右括号,它对应的是跟他配对的左括号。比如,之前最后一个元素为lesson,那么此时调用writeEndElement()接口就表示结束lesson元素(即</lesson>标签)。
然后,我们循环写入另外几个lesson。在此不再赘述。

    写入courses元素的结束标签,需要使用如下语句:

  1. writer.writeEndElement();  // 结束子元素 </courses>
复制代码
    这里也是调用writeEndElement()接口。如前所述,它结束的元素是跟他配对的startElement,也就是courses。
    最后,我们写入doc的结束元素:

  1. writer.writeEndElement();  // 结束子元素 </doc>
复制代码
    当所有元素结束后,我们写入文档的结束标志并关闭文档:
  1. writer.writeEndDocument();  // 结束文档
  2. file.close();
复制代码

读取XML文件-QXmlStreamReader
    读取XML文件时,也需要构造文件对象:
代码清单04-19-02
mian.cpp
  1. void example02() {
  2. QString strPath =
  3.               ns_train::getPath("$TRAINDEVHOME/test/chapter04/ks04_20/");
  4.     if (!strPath.endsWith("/")) {
  5.             strPath += "/";
  6.     }
  7.     // 程序运行目录下的xml文件
  8.     QString strFileName = strPath + "test04_20.xml";
  9.     QFile file(strFileName);
  10.     if (!file.open(QFile::ReadOnly | QFile::Text))        {
  11.             return;
  12. }
  13. //......
  14. }
复制代码
    代码清单04-19-02的代码跟保存XML文件时的代码类似,不再详述。
    然后构建读取XML文件用的流对象:
  1. QXmlStreamReader reader(&file);
复制代码
    接着,进入主循环体进行遍历:
代码清单04-19-03
main.cpp

  1. QXmlStreamReader::TokenType nType = reader.readNext();
  2. while (!reader.atEnd()) {
  3.        // 读取下一个元素
  4.         nType = reader.tokenType();
  5.         switch (nType) {
  6.         //......
  7.         default:
  8.             break;
  9.         }
  10.         reader.readNext();
  11.     }
  12. }
复制代码
    代码清单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

  1. case QXmlStreamReader::Comment: {  // 注释
  2.                 QString strComment = reader.text().toString();
  3.                 break;
  4. }
复制代码
    代码清单04-19-04中:
    第2行的reader.text()得到的是一个QStringRef对象,它并不是一个真正的QString对象,因此需要继续转换,方法是调用toString()接口。在switch语句之后,我们调用reader.readNext()。
    当继续读取到<doc>时,这是一个QXmlStreamReader::StartElement。与之对应的是</doc>,称之为EndElement.当读取到StartElement时我们需要验证是否读取到的是doc元素:
代码清单04-19-05
main.cpp

  1. case QXmlStreamReader::StartElement: {  // 开始元素
  2.         QString strElementName = reader.name().toString();
  3.         if (QString::compare(strElementName, "doc") == 0) {  // 根元素
  4.                 parseDoc(reader);
  5.         }
  6.         break;
  7. }
复制代码
    代码清单04-19-05中:
    第3行,用来判断是否为doc元素,如果是则调用parseDoc()接口继续解析。
    现在我们来看一下parseDoc()接口。parseDoc()内部很简单,因为我们的XML文件里doc元素很简单,没有并列的兄弟元素,只有子元素。该接口内部也是遍历:
代码清单04-19-06
main.cpp:parseDoc()

  1. void parseDoc(QXmlStreamReader& reader) {
  2.     QXmlStreamReader::TokenType nType = reader.readNext();
  3.     while (!reader.atEnd()) {
  4.         nType = reader.tokenType();
  5.         switch (nType) {
  6.         case QXmlStreamReader::StartElement: { // 开始元素
  7.             QString strElementName = reader.name().toString();
  8.             if (QString::compare(strElementName, "courses") == 0) {
  9.                 qDebug() << QString::fromLocal8Bit("============== 开始元素<courses> ==============");
  10.                 // 处理courses
  11.                 parseCourses(reader);
  12.             }
  13.             break;
  14.         }
  15.         case QXmlStreamReader::EndElement: { // 结束元素
  16.             QString strElementName = reader.name().toString();
  17.             qDebug() << QString::fromLocal8Bit("============= 结束元素<%1> =============").arg(strElementName);
  18.             return;
  19.         }
  20.         default:
  21.             break;
  22.         }
  23.         nType = reader.readNext();
  24.     }
  25. }
复制代码
    代码清单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

  1. void    parseCourses(QXmlStreamReader& reader) {
  2.     // 将reader指向lesson子元素.
  3.     QXmlStreamReader::TokenType nType = reader.readNext();
  4.     while (!reader.atEnd()) {
  5.         nType = reader.tokenType();
  6.         switch (nType) {
  7.         case QXmlStreamReader::StartElement: { // 开始元素
  8.             QString strElementName = reader.name().toString();
  9.             if (QString::compare(strElementName, "lesson") == 0) {
  10.                 // lesson元素
  11.                 qDebug() << QString::fromLocal8Bit("== 开始元素<lesson> ==");
  12.                 // 解析lesson
  13.                 parseLesson(reader);
  14.                 continue;
  15.             }
  16.             break;
  17.         }
  18.         case QXmlStreamReader::EndElement: { // 结束元素
  19.             QString strElementName = reader.name().toString();
  20.             qDebug() << QString::fromLocal8Bit("== 结束元素<%1>==")
  21.                        .arg(strElementName);
  22.             if (QString::compare(strElementName, "courses") == 0) {
  23.                 // 结束元素
  24.                 return;
  25.             }
  26.             break;
  27.         }
  28.         default:
  29.             break;
  30.         }  
  31.         nType = reader.readNext();
  32.     }
  33. }
复制代码
    在代码清单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
  1. void parseLesson(QXmlStreamReader& reader) {
  2.     QXmlStreamReader::TokenType nType;
  3.     QString strElementName;
  4.     QXmlStreamAttributes attributes;
  5.     QXmlStreamAttributes::iterator iteAttribute;
  6.     QString strText;
  7.     while (!reader.atEnd()) {
  8.         nType = reader.tokenType();
  9.         switch (nType)
  10.         {
  11.         case QXmlStreamReader::StartElement:
  12.             strElementName = reader.name().toString();
  13.             attributes = reader.attributes();
  14.             for (iteAttribute= attributes.begin();
  15.                  iteAttribute!= attributes.end();
  16.                  iteAttribute++) {
  17.                 qDebug() << (*iteAttribute).name()
  18.                          << "=" << (*iteAttribute).value();
  19.             }
  20.             break;
  21.         case QXmlStreamReader::EndElement:
  22.             strElementName = reader.name().toString();   
  23.             if (strElementName != "lesson") {
  24.                 return;
  25.             }
  26.             qDebug() << QString::fromLocal8Bit("== 结束元素:%1 ==")
  27.                        .arg(strElementName);
  28.             break;
  29.         case QXmlStreamReader::Characters:
  30.             strText = reader.text().toString();
  31.             qDebug() << QString("Characters:%1").arg(strText);
  32.             break;
  33.         default:
  34.             break;
  35.         }  
  36.         nType = reader.readNext();
  37.     }
  38. }
复制代码
    代码清单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。此时可以通过下列方法得到文本的内容:
  1. 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. QT                        += 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产品开发》目录






回复

使用道具 举报

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

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