【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();

而後就能看到運行結果:

相關文章
相關標籤/搜索