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