C++中并沒(méi)有操作XML文件的標準庫,因此大家需要使用各自熟悉的XML庫來(lái)解決XML文件的讀取與寫(xiě)入。XML的一個(gè)重要用途是作為程序的配置文件,存儲程序運行相關(guān)的各種數據。本文總結了使用libxml2庫來(lái)對XML配置文件進(jìn)行編程的一些經(jīng)驗。最后提供了一個(gè)封裝好的類(lèi)CXMLConfig,并詳細說(shuō)明了該類(lèi)的功能、使用方法和注意事項。
閱讀本文所需的技術(shù)背景:
l C/C++簡(jiǎn)單語(yǔ)法;
l XML技術(shù),XPATH技術(shù);
l C++編譯器知識;
本文的內容包括:
l 下載與安裝LIBXML2和ICONV;
l 第一個(gè)例子程序的編寫(xiě)、編譯鏈接和運行;
l 使用XPATH讀出多個(gè)配置項的值;
l XML的配置文件類(lèi)CXMLConfig;
l 將配置項寫(xiě)入XML文件;
l CXMLConfig類(lèi)使用小結;
閱讀本文之前最好先讀我的上一篇博客C++的XML編程經(jīng)驗――LIBXML2庫使用指南,那一篇專(zhuān)門(mén)介紹libxml2庫的使用方法。本文將不會(huì )再詳細介紹libxml2的使用,而是集中精力介紹如何存取XML中的數據。
本文的源代碼是一個(gè)VC6的工程,里面包含三個(gè)子工程。地址在http://www.blogjava.net/Files/wxb_nudt/XMLConfigFile.rar。
為了方便讀者,這一段原文照抄上一篇博客。
Libxml2是一個(gè)C語(yǔ)言的XML程序庫,可以簡(jiǎn)單方便的提供對XML文檔的各種操作,并且支持XPATH查詢(xún),以及部分的支持XSLT轉換等功能。Libxml2的下載地址是http://xmlsoft.org/,完全版的庫是開(kāi)源的,并且帶有例子程序和說(shuō)明文檔。最好將這個(gè)庫先下載下來(lái),因為這樣可以查看其中的文檔和例子。
windows版本的的下載地址是http://www.zlatkovic.com/libxml.en.html;這個(gè)版本只提供了頭文件、庫文件和dll,不包含源代碼、例子程序和文檔。在文本中,只需要下載libxml2庫、iconv庫和zlib庫就行了(注意,libxml2庫依賴(lài)iconv和zlib庫,本文中重點(diǎn)關(guān)注libxml2和iconv,zlib不介紹),我使用的版本是libxml2-2.6.30.win32.zip、zlib-1.2.3.win32.zip和iconv-1.9.2.win32.zip。
在編程的時(shí)候,我們使用windows版本的libxml2、zlib和iconv,將其解壓縮到指定文件夾,例如D:"libxml2-2.6.30.win32,D:"zlib-1.2.3.win32以及D:"iconv-1.9.2.win32。事實(shí)上,我們知道在windows下面使用頭文件、庫文件和dll是不需要安裝的,它又沒(méi)有使用任何需要注冊的組件或者數據庫,只需要告訴編譯器和鏈接器這些資源的位置就可以了。
注意:要在path變量中加上D:"iconv-1.9.2.win32"bin;D:"zlib-1.2.3.win32"bin;D:"libxml2-2.6.30.win32"bin這三個(gè)地址,否則在執行的時(shí)候就找不到?;蛘呤褂酶?jiǎn)單的方法,把其中的三個(gè)dll到拷貝到system32目錄中。
有兩種方法來(lái)編譯鏈接基于libxml2的程序,第一種是在VC環(huán)境中設置lib和include路徑,并在link設置中添加libxml2.lib和iconv.lib;第二種是用編譯器選項告訴編譯器cl.exe頭文件的位置,并用鏈接器選項告訴鏈接器link.exe庫文件的位置,同時(shí)在windows環(huán)境變量path中添加libxml2中bin文件夾的位置,以便于程序運行時(shí)可以找到dll(也可以將dll拷貝到system32目錄下)。
本節的源代碼位于項目HelloXml中,使用的xml文件是Helloxml.xml。
在安裝配置好libxml2和iconv庫之后,就可以寫(xiě)一個(gè)簡(jiǎn)單的程序來(lái)讀取XML中的數據了。該XML內容如下:
<?xml version="1.0" encoding="GB2312" ?>
<main>20080526</main>
使用libxml2庫讀取main節點(diǎn)包含的內容,代碼如下:
xmlChar* LoadConfigFile(const char* szConfigFilename, xmlChar* xszRel)
{
xmlDocPtr doc; //定義解析文檔指針
xmlNodePtr curNodePtr; //定義結點(diǎn)指針
doc = xmlReadFile(szConfigFilename,"GB2312",XML_PARSE_RECOVER); //解析文件
if (doc == NULL )
{
fprintf(stderr,"Document not parsed successfully. "n");
xmlFreeDoc(doc);
exit(1);
}
curNodePtr = xmlDocGetRootElement(doc); //確定文檔根元素
/*檢查確認當前文檔中包含內容*/
if (curNodePtr == NULL)
{
fprintf(stderr,"empty document"n");
xmlFreeDoc(doc);
exit(1);
}
//讀取xml文檔中的內容并賦值給對象屬性
xszRel = xmlNodeGetContent(curNodePtr);
xmlFreeDoc(doc);
return xszRel;
}
int main(int argc, char* argv[])
{
xmlChar* xszContent = NULL;
xszContent = LoadConfigFile("..""Debug""HelloXml.xml",xszContent);
if (xszContent != NULL)
{
cout<<"HELLO, XML CONFIG FILE. content = "<<xszContent<<endl;
xmlFree(xszContent);
}
return 0;
}
編譯代碼之前要注意:xml文檔存放的地點(diǎn)不是本項目文件夾,而是項目文件夾上層的Debug目錄,同時(shí)將編譯和鏈接的目的文件夾都設置為項目文件夾上層的Debug目錄。第二點(diǎn),在link選項中加入了libxml2.lib和iconv.lib。第三點(diǎn),在系統的Path變量中指明了libxml2.dll、iconv.dll和zlib1.dll的路徑(為了方便讀者,我將這三個(gè)dll都拷貝到了Debug目錄下面)。
編譯鏈接完畢后運行程序,得到如下結果:
HELLO, XML CONFIG FILE. content = 20080526
本節的源代碼位于項目XPathConfig中,使用的xml文件是XPathConfig.xml。
上面的例子中,為了理解的便利僅在根節點(diǎn)中存儲了一個(gè)值,而實(shí)際的配置文件往往是同時(shí)存放多個(gè)配置項的值。舉例如下:
<main>
<IP>127.0.0.1</IP>
<Port>80</Port>
</main>
Xml中存儲了一個(gè)IP地址和一個(gè)端口值。其XPATH地址分別是/main/IP/和/main/Port/。當然,更加復雜的XPATH值也可同樣處理。
為了方便的操作xml文檔,我寫(xiě)了一組xml函數,位于Code_Conv.h和Code_Conv.cpp中,其功能如下:
l openXmlFile,打開(kāi)Xml文檔,返回文檔指針;
l closeXmlFile,關(guān)閉Xml文檔;
l getXmlString,根據XPATH路徑讀取字符串;
l getXmlInt,根據XPATH路徑讀取整型值;
為了處理中文以及查詢(xún)Xpath節點(diǎn),我還寫(xiě)了四個(gè)被上述函數調用的函數:
l code_convert,從一種編碼轉為另一種編碼;
l u2g,從UTF-8轉換為GB2312編碼;
l g2u,從GB2312轉換為UTF-8編碼;
l get_nodeset,調用xpath查詢(xún)節點(diǎn)集合,成功則返回xpath的對象指針,失敗返回NULL。
然后,主程序便簡(jiǎn)化為:
int main(int argc, char* argv[])
{
xmlDocPtr doc = openXmlFile("..""Debug""XPathConfig.xml");
string strIP = getXmlString(doc,"/main/IP");
int iPort = getXmlInt(doc,"/main/Port");
cout<<"IP = "<<strIP.c_str()<<" Port = "<<iPort<<endl;
closeXmlFile(doc);
return 0;
}
運行結果為:
IP = 127.0.0.1 Port = 80
觀(guān)察上面的代碼可以發(fā)現,整個(gè)主程序幾乎與libxml2庫無(wú)關(guān)了,除了一個(gè)xmlDocPtr變量。再次觀(guān)察可以發(fā)現,這個(gè)變量幾乎出現在每個(gè)自定義函數中,它代表的是一種狀態(tài),或者可以稱(chēng)為屬性。而那些自定義函數可以稱(chēng)之為功能。因此,按照許多C++專(zhuān)著(zhù)的說(shuō)法,屬性+功能=對象?!?/span>C++沉思錄》中說(shuō)道,C和C++最大的不同在于,C++擁有一個(gè)最合適的存儲程序狀態(tài)的位置,即對象的屬性;而C則必須在許多函數中留出一個(gè)位置來(lái)保存這個(gè)狀態(tài)。這句話(huà),簡(jiǎn)直正確得可怕!
本節的源代碼位于項目UseClass中,使用的xml文件還是XPathConfig.xml。
于是有了下面的CXMLConfig類(lèi)定義:
class CXMLConfig
{
public:
CXMLConfig(const char* szXmlFilename);
~CXMLConfig();
//根據XPATH路徑讀取字符串
string getXmlString(const char *szXpath);
int getXmlInt(const char* szXpath);
private:
//代碼轉換:從一種編碼轉為另一種編碼
int code_convert(char* from_charset, char* to_charset, char* inbuf,int inlen, char* outbuf, int outlen);
//UNICODE碼轉為GB2312碼
//成功則返回一個(gè)動(dòng)態(tài)分配的char*變量,需要在使用完畢后手動(dòng)free,失敗返回NULL
char* u2g(char *inbuf);
//GB2312碼轉為UNICODE碼
//成功則返回一個(gè)動(dòng)態(tài)分配的char*變量,需要在使用完畢后手動(dòng)free,失敗返回NULL
char* g2u(char *inbuf);
//調用xpath查詢(xún)節點(diǎn)集合,成功則返回xpath的對象指針,失敗返回NULL
xmlXPathObjectPtr get_nodeset(const xmlChar *xpath);
private:
string m_strFilename;
xmlDocPtr m_doc;
};
使用這個(gè)類(lèi)來(lái)改寫(xiě)主程序,可以讓使用者完全脫離libxml2的庫環(huán)境,并且省略了打開(kāi)和關(guān)閉xml文件的步驟,因為這些工作在構造和析構函數中完成了。
int main(int argc, char* argv[])
{
CXMLConfig xmlConfig("..""Debug""XPathConfig.xml");
string strIP = xmlConfig.getXmlString("/main/IP");
int iPort = xmlConfig.getXmlInt("/main/Port");
cout<<"IP = "<<strIP.c_str()<<" Port = "<<iPort<<endl;
return 0;
}
運行結果為:
IP = 127.0.0.1 Port = 80
本節的源代碼位于項目UseClass中,使用的xml文件依然是XPathConfig.xml。
目前CXMLConfig類(lèi)已經(jīng)有了打開(kāi)xml文件,讀取數據以及關(guān)閉xml文件的功能。還缺少寫(xiě)入數據的功能。寫(xiě)入數據功能的算法也很簡(jiǎn)單:先將xml文件讀入內存,然后通過(guò)xpath找到相應節點(diǎn),并修改節點(diǎn)內容,最后將內存中的xml文件一次性寫(xiě)入硬盤(pán)。這里有一點(diǎn)要注意,如果在寫(xiě)入過(guò)程中硬盤(pán)斷電或者出現其他故障,則會(huì )造成無(wú)法恢復的錯誤,數據會(huì )全部丟失。為了防止這種情況,還應該在寫(xiě)入前進(jìn)行數據備份的工作。通盤(pán)考慮后,在CXMLConfig類(lèi)中加入如下函數:
writeXmlString:將字符串寫(xiě)入xml文檔相應節點(diǎn);
writeXmlInt:將整型寫(xiě)入xml文檔相應節點(diǎn);
saveConfigFile:將內存中的xml文檔寫(xiě)入硬盤(pán);
saveBakConfigFile:保存當前的xml文檔到bak文件(即xml文檔名加_BAK.XML)中;
loadBakConfigFile:將bak文件讀入內存;
注意,在調用saveConfigFile時(shí)會(huì )自動(dòng)調用saveBakConfigFile,將原有配置文件保存為備份文件。修改后的類(lèi)如下:
class CXMLConfig
{
public:
CXMLConfig(const char* szXmlFilename);
~CXMLConfig();
//根據XPATH路徑讀取字符串
string getXmlString(const char *szXpath);
int getXmlInt(const char* szXpath);
bool writeXmlString(const string strValue, const char* szXpath);
bool writeXmlInt(const int iValue, const char* szXpath);
bool saveConfigFile();
bool saveBakConfigFile();
bool loadBakConfigFile();
private:
//代碼轉換:從一種編碼轉為另一種編碼
int code_convert(char* from_charset, char* to_charset, char* inbuf,
int inlen, char* outbuf, int outlen);
//UNICODE碼轉為GB2312碼
char* u2g(char *inbuf);
//GB2312碼轉為UNICODE碼
char* g2u(char *inbuf);
//調用xpath查詢(xún)節點(diǎn)集合,成功則返回xpath的對象指針,失敗返回NULL
xmlXPathObjectPtr get_nodeset(const xmlChar *xpath);
// 禁止拷貝構造函數和"="操作
CXMLConfig(const CXMLConfig&);
CXMLConfig& operator=(const CXMLConfig&);
private:
string m_strFilename;
xmlDocPtr m_doc;
};
然后我們修改了主程序,其功能為讀出數據后修改了數據,然后存入了配置文件,主程序如下:
int main(int argc, char* argv[])
{
CXMLConfig xmlConfig("..""Debug""XPathConfig.xml");
string strIP = xmlConfig.getXmlString("/main/IP");
int iPort = xmlConfig.getXmlInt("/main/Port");
cout<<"IP = "<<strIP.c_str()<<" Port = "<<iPort<<endl;
strIP = "127.1.1.1";
iPort = 81;
xmlConfig.writeXmlString(strIP,"/main/IP");
xmlConfig.writeXmlInt(iPort,"/main/Port");
if(xmlConfig.saveConfigFile())
{
cout<<"Save Config file success!"<<endl;
}
return 0;
}
運行完以后會(huì )發(fā)現兩個(gè)結果,第一個(gè)是配置文件XPathConfig.xml中的內容已經(jīng)被修改,第二個(gè)是原配置文件內容備份在XPathConfig_bak.xml中。
目前為止,CXMLConfig類(lèi)提供了較為便利的讀取和保存XML配置文件的功能。那么使用CXMLConfig需要哪些步驟呢?
第一,正確安裝了libxml2和iconv庫,包括頭文件、lib文件和dll文件。注意頭文件主要是libxml2和iconv的頭文件,lib文件就是兩個(gè)libxml2.lib和iconv.lib,而dll有三個(gè),即libxml2.dll、iconv.dll和zlib1.dll。注意:如果你沒(méi)有正確安裝,那么無(wú)法正確編譯我的例子程序,但是可以運行,因為我已經(jīng)將dll都包含到運行目錄下。
第二,確信你弄懂了你的xml配置文件結構,并放在正確的地方;
第三,使用CXMLConfig xmlConfig("..""Debug""XPathConfig.xml")語(yǔ)句正確構造一個(gè)CXMLConfig對象,并調用相應的方法來(lái)操作xml文件。
CXMLConfig類(lèi)使用的注意事項:
第一,注意xml文件必須使用節點(diǎn)來(lái)存儲數據,而不是屬性。若使用屬性來(lái)保存數據,CXMLConfig類(lèi)不會(huì )正確讀出其數據,當然更不能正確寫(xiě)入。若有興趣,可以擴展CXMLConfig類(lèi)來(lái)實(shí)現對屬性數據的存取,事實(shí)上那非常簡(jiǎn)單。
第二,若有兩個(gè)節點(diǎn)的XPATH路徑相同,例如
<main>
<IP>127.0.0.1</IP>
<IP>127.0.0.2</IP>
<Port>80</Port>
</main>
那么使用getXmlString將只會(huì )得到第一個(gè)節點(diǎn)的內容。同理,寫(xiě)入時(shí)也只會(huì )寫(xiě)入第一個(gè)節點(diǎn)。
CXMLConfig類(lèi)的使用環(huán)境:
第一, 使用節點(diǎn)來(lái)存儲數據;
第二, 節點(diǎn)的XPATH路徑各不相同;
第三, XML文件最好不大于100M。
總之,若有更復雜的要求,請還是仔細研究libxml2或者任意一個(gè)開(kāi)源或商用XML庫。
事實(shí)上,按照原計劃這篇博客才剛剛開(kāi)頭,后面才是最精彩的部分。其內容是介紹如何將XML文件當作一個(gè)小型的數據庫,把多個(gè)XPATH路徑相同的鍵和值讀入一個(gè)std::map<std::string,std::string>中,然后在程序中方便的使用這個(gè)map來(lái)查找,存取某一類(lèi)數據。但是由于前面的部分寫(xiě)作時(shí)考慮得太詳細,而且CXMLConfig類(lèi)也介紹逐漸趨于完善,因此為了防止喧賓奪主,本文就到這里結束為好。作為一篇libxml2和C++的入門(mén)文章,恰到好處!
聯(lián)系客服