#(59):使用流處理 XML
本章開始我們將了解到如何使用 Qt 處理 XML 格式的文檔。
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(不過這一點有待商榷,感興趣的話可以詳見[這里](http://www.devbean.net/2011/04/dive_into_html5_2_3/))。
我們說過,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`/`QXmlStreamWrite`r 移動到 QtCore 中,QtXml 則標記為“不再維護”,這已經充分表明了 Qt 的官方意向。
至于生成 XML 文檔,Qt 同樣提供了三種方式:
* `QXmlStreamWriter`,與`QXmlStreamReader`相對應;
* DOM 方式,首先在內存中生成 DOM 樹,然后將 DOM 樹寫入文件。不過,除非我們程序的數據結構中本來就維護著一個 DOM 樹,否則,臨時生成樹再寫入肯定比較麻煩;
* 純手工生成 XML 文檔,顯然,這是最復雜的一種方式。
使用`QXmlStreamReader`是 Qt 中最快最方便的讀取 XML 的方法。因為`QXmlStreamReader`使用了遞增式的解析器,適合于在整個 XML 文檔中查找給定的標簽、讀入無法放入內存的大文件以及處理 XML 的自定義數據。
每次`QXmlStreamReader`的`readNext()`函數調用,解析器都會讀取下一個元素,按照下表中展示的類型進行處理。我們通過表中所列的有關函數即可獲得相應的數據值:
| 類型 | 示例 | 有關函數 |
| -- || -- || -- |
| `StartDocument` | – | `documentVersion()`,`documentEncoding()`,`isStandaloneDocument()` |
| `EndDocument` | – | |
| `StartElement` | | `namespaceUri()`,`name()`,`attributes()`,`namespaceDeclarations()` |
| `EndElement` | | `namespaceUri()`,`name()` |
| `Characters` | AT&T | `text()`,`isWhitespace()`,`isCDATA()` |
| `Comment` | | `text()` |
| `DTD` | | `text()`,`notationDeclarations()`,`entityDeclarations()`,`dtdName()`,`dtdPublicId()`,`dtdSystemId()` |
| `EntityReference` | ™ | `name()`,`text()` |
| `ProcessingInstruction` | | `processingInstructionTarget()`,`processingInstructionData()` |
| `Invalid` | >&<! | `error()`,?`errorString()` |
考慮如下 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 文檔,然后使用一個`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();
~~~
然后就能看到運行結果:
[](http://files.devbean.net/images/2013/07/xml-reader-demo.png)
值得一提的是,雖然我們的代碼比較復雜,但是思路很清晰,一層一層地處理,這正是遞歸下降算法的有一個示例。我們曾在前面講解[布爾表達式的樹模型](http://www.devbean.net/2013/05/qt-study-road-2-bool-tree-model/)章節使用過這個思想。
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)