Qt使用流處理XML文件的示例代碼
本章開始我們將了解到如何使用 Qt 處理 XML 格式的文檔。
XML(eXtensible Markup Language)是一種通用的文本格式,被廣泛運用于數(shù)據(jù)交換和數(shù)據(jù)存儲(雖然近年來 JSON 盛行,大有取代 XML 的趨勢,但是對于一些已有系統(tǒng)和架構(gòu),比如 WebService,由于歷史原因,仍舊會繼續(xù)使用 XML)。XML 由 World Wide Web Consortium(W3C)發(fā)布,作為 SHML(Standard Generalized Markup Language)的一種輕量級方言。XML 語法類似于 HTML,與后者的主要區(qū)別在于 XML 的標簽不是固定的,而是可擴展的;其語法也比 HTML 更為嚴格。遵循 XML 規(guī)范的 HTML 則被稱為 XHTML(不過這一點有待商榷,感興趣的話可以詳見這里)。
我們說過,XML 類似一種元語言,基于 XML 可以定義出很多新語言,比如 SVG(Scalable Vector Graphics)和 MathML(Mathematical Markup Language)。SVG 是一種用于矢量繪圖的描述性語言,Qt 專門提供了 QtSVG 對其進行解釋;MathML 則是用于描述數(shù)學公式的語言,Qt Solutions 里面有一個 QtMmlWidget 模塊專門對其進行解釋。
另外一面,針對 XML 的通用處理,Qt4 提供了 QtXml 模塊;針對 XML 文檔的 Schema 驗證以及 XPath、XQuery 和 XSLT,Qt4 和 Qt5 則提供了 QtXmlPatterns 模塊。Qt 提供了三種讀取 XML 文檔的方法:
- QXmlStreamReader:一種快速的基于流的方式訪問良格式 XML 文檔,特別適合于實現(xiàn)一次解析器(所謂 “一次解析器”,可以理解成我們只需讀取文檔一次,然后像一個遍歷器從頭到尾一次性處理 XML 文檔,期間不會有反復的情況,也就是不會讀完第一個標簽,然后讀第二個,讀完第二個又返回去讀第一個,這是不允許的);
- DOM(Document Object Model):將整個 XML 文檔讀入內(nèi)存,構(gòu)建成一個樹結(jié)構(gòu),允許程序在樹結(jié)構(gòu)上向前向后移動導航,這是與另外兩種方式最大的區(qū)別,也就是允許實現(xiàn)多次解析器(對應(yīng)于前面所說的一次解析器)。DOM 方式帶來的問題是需要一次性將整個 XML 文檔讀入內(nèi)存,因此會占用很大內(nèi)存;
- SAX(Simple API for XML):提供大量虛函數(shù),以事件的形式處理 XML 文檔。這種解析辦法主要是由于歷史原因提出的,為了解決 DOM 的內(nèi)存占用提出的(在現(xiàn)代計算機上,這個一般已經(jīng)不是問題了)。
在 Qt4 中,這三種方式都位于 QtXml 模塊中。Qt5 則將 QXmlStreamReader/QXmlStreamWriter 移動到 QtCore 中,QtXml 則標記為 “不再維護”,這已經(jīng)充分表明了 Qt 的官方意向。
至于生成 XML 文檔,Qt 同樣提供了三種方式:
- QXmlStreamWriter,與 QXmlStreamReader 相對應(yīng);
- DOM 方式,首先在內(nèi)存中生成 DOM 樹,然后將 DOM 樹寫入文件。不過,除非我們程序的數(shù)據(jù)結(jié)構(gòu)中本來就維護著一個 DOM 樹,否則,臨時生成樹再寫入肯定比較麻煩;
- 純手工生成 XML 文檔,顯然,這是最復雜的一種方式。
使用 QXmlStreamReader 是 Qt 中最快最方便的讀取 XML 的方法。因為 QXmlStreamReader 使用了遞增式的解析器,適合于在整個 XML 文檔中查找給定的標簽、讀入無法放入內(nèi)存的大文件以及處理 XML 的自定義數(shù)據(jù)。
每次 QXmlStreamReader 的 readNext() 函數(shù)調(diào)用,解析器都會讀取下一個元素,按照下表中展示的類型進行處理。我們通過表中所列的有關(guān)函數(shù)即可獲得相應(yīng)的數(shù)據(jù)值:
類型 | 示例 | 有關(guān)函數(shù) |
StartDocument | – | documentVersion(),documentEncoding(),isStandaloneDocument() |
EndDocument | – | |
StartElement | <item> | namespaceUri(),name(),attributes(),namespaceDeclarations() |
EndElement | </item> | namespaceUri(),name() |
Characters | AT&T | text(),isWhitespace(),isCDATA() |
Comment | <!– fix –> | text() |
DTD | <!DOCTYPE …> | text(),notationDeclarations(),entityDeclarations(),dtdName(),dtdPublicId(),dtdSystemId() |
EntityReference | ™ | name(),text() |
ProcessingInstruction | <?alert?> | 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() 函數(shù)的循環(huán)調(diào)用,我們可以使用 isStartElement()、isCharacters() 這樣的函數(shù)檢查當前讀取的類型,當然也可以直接使用 state() 函數(shù)。
下面我們看一個完整的例子。在這個例子中,我們讀取一個 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 顯然就是我們的主窗口,其構(gòu)造函數(shù)也沒有什么好說的:
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 文檔的函數(shù),這正是我們關(guān)注的要點:
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() 函數(shù)用于打開給定文件。我們使用 QFile 打開文件,將其設(shè)置為 QXmlStreamReader 的設(shè)備。也就是說,此時 QXmlStreamReader 就可以從這個設(shè)備(QFile)中讀取內(nèi)容進行分析了。接下來便是一個 while 循環(huán),只要沒讀到文件末尾,就要一直循環(huán)處理。首先判斷是不是 StartElement,如果是的話,再去處理 bookindex 標簽。注意,因為我們的根標簽就是 bookindex,如果讀到的不是 bookindex,說明標簽不對,就要發(fā)起一個錯誤(raiseError())。
如果不是 StartElement(第一次進入循環(huán)的時候,由于沒有事先調(diào)用 readNext(),所以會進入這個分支),則調(diào)用 readNext()。為什么這里要用 while 循環(huán),XML 文檔不是只有一個根標簽嗎?直接調(diào)用一次 readNext() 函數(shù)不就好了?這是因為,XML 文檔在根標簽之前還有別的內(nèi)容,比如聲明,比如 DTD,我們不能確定第一個 readNext() 之后就是根標簽。正如我們提供的這個 XML 文檔,首先是 聲明,其次才是根標簽。如果你說,第二個不就是根標簽嗎?但是 XML 文檔還允許嵌入 DTD,還可以寫注釋,這就不確定數(shù)目了,所以為了通用起見,我們必須用 while 循環(huán)判斷。處理完之后就可以關(guān)閉文件,如果有錯誤則顯示錯誤。
接下來看 readBookindexElement() 函數(shù):
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(); } } }
注意第一行我們加了一個斷言。意思是,如果在進入函數(shù)的時候,reader 不是 StartElement 狀態(tài),或者說標簽不是 bookindex,就認為出錯。然后繼續(xù)調(diào)用 readNext(),獲取下面的數(shù)據(jù)。后面還是 while 循環(huán)。如果是 EndElement,退出,如果又是 StartElement,說明是 entry 標簽(注意我們的 XML 結(jié)構(gòu),bookindex 的子元素就是 entry),那么開始處理 entry,否則跳過。
那么下面來看 readEntryElement() 函數(shù):
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(); } } }
這個函數(shù)接受一個 QTreeWidgetItem 指針,作為根節(jié)點。這個節(jié)點被當做這個 entry 標簽在 QTreeWidget 中的根節(jié)點。我們設(shè)置其名字是 entry 的 term 屬性的值。然后繼續(xù)讀取下一個數(shù)據(jù)。同樣使用 while 循環(huán),如果是 EndElement 就繼續(xù)讀?。蝗绻?StartElement,則按需調(diào)用 readEntryElement() 或者 readPageElement()。由于 entry 標簽是可以嵌套的,所以這里有一個遞歸調(diào)用。如果既不是 entry 也不是 page,則跳過位置標簽。
然后是 readPageElement() 函數(shù):
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 是葉子節(jié)點,沒有子節(jié)點,所以不需要使用 while 循環(huán)讀取。我們只是遍歷了 entry 下所有的 page 標簽,將其拼接成合適的字符串。
最后 skipUnknownElement() 函數(shù):
void MainWindow::skipUnknownElement() { reader.readNext(); while (!reader.atEnd()) { if (reader.isEndElement()) { reader.readNext(); break; } if (reader.isStartElement()) { skipUnknownElement(); } else { reader.readNext(); } } }
我們沒辦法確定到底要跳過多少位置標簽,所以還是得用 while 循環(huán)讀取,注意位置標簽中所有子標簽都是未知的,因此只要是 StartElement,都直接跳過。
好了,這是我們的全部程序。只要在 main() 函數(shù)中調(diào)用一下即可:
MainWindow w; w.readFile("books.xml"); w.show();
然后就能看到運行結(jié)果:
以上就是Qt使用流處理XML文件的示例代碼的詳細內(nèi)容,更多關(guān)于Qt處理XML的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于linux下C開發(fā)中的幾點技術(shù)經(jīng)驗總結(jié)
本篇文章是對linux下C開發(fā)中的幾點技術(shù)經(jīng)驗總結(jié)進行了詳細的分析介紹,需要的朋友參考下2013-05-05C語言中判斷兩個IPv4地址是否屬于同一個子網(wǎng)的代碼
這篇文章主要介紹了C語言中判斷兩個IPv4地址是否屬于同一個子網(wǎng)的代碼,需要的朋友可以參考下2017-09-09