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://www.blogjava.net/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;
}
运行结果为:
IP = 127.0.0.1 Port = 80
观察上面的代码可以发现,整个主程序几乎与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;
}
运行结果为:
IP = 127.0.0.1 Port = 80
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++的入门文章,恰到好处!