【Qt笔记】使用流处理 XML

本章开始咱们将了解到如何使用 Qt 处理 XML 格式的文档。html

XML(eXtensible Markup Language)是一种通用的文本格式,被普遍运用于数据交换和数据存储(虽然近年来 JSON 盛行,大有取代 XML 的趋势,可是对于一些已有系统和架构,好比 WebService,因为历史缘由,仍旧会继续使用 XML)。XML 由 World Wide Web Consortium(W3C)发布,做为 SHML(Standard Generalized Markup Language)的一种轻量级方言。XML 语法相似于 HTML,与后者的主要区别在于 XML 的标签不是固定的,而是可扩展的;其语法也比 HTML 更为严格。遵循 XML 规范的 HTML 则被称为 XHTML(不过这一点有待商榷,感兴趣的话能够详见这里)。html5

 

咱们说过,XML 相似一种元语言,基于 XML 能够定义出不少新语言,好比 SVG(Scalable Vector Graphics)和 MathML(Mathematical Markup Language)。SVG 是一种用于矢量绘图的描述性语言,Qt 专门提供了 QtSVG 对其进行解释;MathML 则是用于描述数学公式的语言,Qt Solutions 里面有一个 QtMmlWidget 模块专门对其进行解释。数据结构

另一面,针对 XML 的通用处理,Qt4 提供了 QtXml 模块;针对 XML 文档的 Schema 验证以及 XPath、XQuery 和 XSLT,Qt4 和 Qt5 则提供了 QtXmlPatterns 模块。Qt 提供了三种读取 XML 文档的方法:架构

  • QXmlStreamReader:一种快速的基于流的方式访问良格式 XML 文档,特别适合于实现一次解析器(所谓“一次解析器”,能够理解成咱们只需读取文档一次,而后像一个遍历器从头至尾一次性处理 XML 文档,期间不会有反复的状况,也就是不会读完第一个标签,而后读第二个,读完第二个又返回去读第一个,这是不容许的);
  • DOM(Document Object Model):将整个 XML 文档读入内存,构建成一个树结构,容许程序在树结构上向前向后移动导航,这是与另外两种方式最大的区别,也就是容许实现屡次解析器(对应于前面所说的一次解析器)。DOM 方式带来的问题是须要一次性将整个 XML 文档读入内存,所以会占用很大内存;
  • SAX(Simple API for XML):提供大量虚函数,以事件的形式处理 XML 文档。这种解析办法主要是因为历史缘由提出的,为了解决 DOM 的内存占用提出的(在现代计算机上,这个通常已经不是问题了)。

在 Qt4 中,这三种方式都位于 QtXml 模块中。Qt5 则将QXmlStreamReader/QXmlStreamWriter 移动到 QtCore 中,QtXml 则标记为“再也不维护”,这已经充分代表了 Qt 的官方意向。ide

至于生成 XML 文档,Qt 一样提供了三种方式:函数

  • QXmlStreamWriter,与QXmlStreamReader相对应;
  • DOM 方式,首先在内存中生成 DOM 树,而后将 DOM 树写入文件。不过,除非咱们程序的数据结构中原本就维护着一个 DOM 树,不然,临时生成树再写入确定比较麻烦;
  • 纯手工生成 XML 文档,显然,这是最复杂的一种方式。

使用QXmlStreamReader是 Qt 中最快最方便的读取 XML 的方法。由于QXmlStreamReader使用了递增式的解析器,适合于在整个 XML 文档中查找给定的标签、读入没法放入内存的大文件以及处理 XML 的自定义数据。this

每次QXmlStreamReaderreadNext()函数调用,解析器都会读取下一个元素,按照下表中展现的类型进行处理。咱们经过表中所列的有关函数便可得到相应的数据值:spa

类型 示例 有关函数
StartDocument documentVersion(),documentEncoding(),isStandaloneDocument()
EndDocument  
StartElement <item> namespaceUri(),name(),attributes(),namespaceDeclarations()
EndElement </item> namespaceUri(),name()
Characters AT&amp;T text(),isWhitespace(),isCDATA()
Comment <!– fix –> text()
DTD <!DOCTYPE …> text(),notationDeclarations(),entityDeclarations(),dtdName(),dtdPublicId(),dtdSystemId()
EntityReference &trade; name(),text()
ProcessingInstruction <?alert?> processingInstructionTarget(),processingInstructionData()
Invalid >&<! error()errorString()

考虑以下 XML 片断:.net

<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 文档,而后使用一个QTreeWidget显示出来。咱们的 XML 文档以下:

<bookindex>
    <entry term="sidebearings">
        <page>10</page>
        <page>34-35</page>
        <page>307-308</page>
    </entry>
    <entry term="subtraction">
        <entry term="of pictures">
            <page>115</page>
            <page>244</page>
        </entry>
        <entry term="of vectors">
            <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;
};

MainWindow显然就是咱们的主窗口,其构造函数也没有什么好说的:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    setWindowTitle(tr("XML Reader"));

    treeWidget = new QTreeWidget(this);
    QStringList headers;
    headers << "Items" << "Pages";
    treeWidget->setHeaderLabels(headers);
    setCentralWidget(treeWidget);
}

MainWindow::~MainWindow()
{
}

接下来看几个处理 XML 文档的函数,这正是咱们关注的要点:

bool MainWindow::readFile(const QString &fileName)
{
    QFile file(fileName);
    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()) {
            if (reader.name() == "bookindex") {
                readBookindexElement();
            } else {
                reader.raiseError(tr("Not a valid book file"));
            }
        } else {
            reader.readNext();
        }
    }
    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 循环判断。处理完以后就能够关闭文件,若是有错误则显示错误。

接下来看readBookindexElement()函数:

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()) {
        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,都直接跳过。

好了,这是咱们的所有程序。只要在main()函数中调用一下便可:

MainWindow w;
w.readFile("books.xml");
w.show();

而后就能看到运行结果:

相关文章
相关标签/搜索