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