1 libexpat简介
- Expat 是一个用 C 语言编写的开源 XML 解析库,以其高性能和小巧的体积著称。Expat 兼容多种操作系统平台,包括但不限于 Windows、Linux、macOS 等。由于其跨平台特性和简单易用的API,Expat 成为了许多C/C++程序员解析XML文档的首选工具之一。
- 主要特性:
- 面向流的解析器:Expat 不像 DOM 解析器那样把整个XML文档加载到内存中形成树状结构,而是采用逐行解析的方式处理XML数据。这意味着它适合处理大型或者无限流式的XML数据输入,因为它不需要一次性加载整个文档到内存。
- 事件驱动解析:Expat 使用回调函数机制来报告解析过程中的事件,如元素开始、元素结束、字符数据块等。开发者需要提供这些回调函数,并通过 Expat API 注册,以便在解析过程中接收并处理这些事件。
- 轻量级和高效:Expat 因其简洁的设计和快速的解析速度而受到青睐,尤其对于资源受限的环境或者对性能要求较高的应用来说是一个理想的选择。
2 环境部署
2.1 Windows平台编译
- Winodws平台编译需要安装Visual Studio,推荐使用2015及以上版本。
- 下载源码后解压进入代码根目录下的expat目录中,创建一个build_x84文件夹,在build_x86文件夹中执行以下命令
sh
复制代码
cmake -G "Visual Studio 14 2015" ..
cmake --build ./ --config Release
- 编译成功后,会在expat\build_x86\Release目录下生成对应的静态库和动态库
- 还需要用到3个头文件,expat\build_x86目录下会生成一个expat_config.h头文件,expat\lib目录下有expat.h和expat_external.h这两个头文件。
- 将对应的库文件和这三个头文件拷贝到我们的工程中。
2.2 Linux平台编译
- Linux平台推荐使用Centos7编译
- 同样解压后进入代码根目录下的expat目录中,创建一个build_x64文件夹,在build_x64文件夹中依次执行以下命令
sh
复制代码
./buildconf.sh # 执行后会生成configure文件
./configure --prefix=${PWD}/_install
sudo make #编译
sudo make install # 安装,会安装到执行configure时--prefix参数指定目录下,不指定会安装到默认目录下
- 执行完以上命令在expat/_install目录下会生成头文件、库文件和可执行程序等。
- 将头文件和库文件拷贝到我们的工程目录下。
3 接口介绍
- 介绍下常用的几个API接口,有几个函数可能不好理解,在4章节的demo中会结合实例说明。
3.1 创建XML解析器实例
c
复制代码
/*
* encoding: 规定输出编码,填NULL默认为UTF-8,支持ISO-8859-1, UTF-8, US-ASCII 这三种编码方式
* 返回值: 创建成功返回一个XML解析器实例,创建失败返回NULL
*/
XML_Parser XML_ParserCreate(const XML_Char *encoding);
3.2 设置用户自定义的数据
c
复制代码
/*
* parser: XML解析器实例
* userData: 指向任意类型数据的指针。可以指向用户自定义的数据结构,通常是为了在解析过程中传递上下文信息或者存储解析结果
*/
void XML_SetUserData(XML_Parser parser, void *userData);
3.3 注册处理XML数据开始和结束事件的回调函数
c
复制代码
/*
* parser: XML解析器实例
* start: 处理元素开始事件的回调函数,可查看3.7
* end: 处理元素结束事件的回调函数,可查看3.8
*/
void XML_SetElementHandler(XML_Parser parser, XML_StartElementHandler start, XML_EndElementHandler end);
3.4 注册处理XML文本内容事件的回调函数
c
复制代码
/*
* parser: XML解析器实例
* handler: 处理XML数据中的文本内容的回调函数,可查看3.9
*/
void XML_SetCharacterDataHandler(XML_Parser parser, XML_CharacterDataHandler handler);
3.6 解析缓冲区中的XML数据
c
复制代码
/*
* parser: XML解析器实例
* buffer: XML数据的缓冲区
* isFinal: 指示本次调用是否代表了整个XML输入的结束
* 返回值: 成功返回 XML_STATUS_OK
*/
XML_Status XML_Parse(XML_Parser parser, const char *s, int len, int isFinal) ;
3.7 处理XML数据字段开始的回调函数
c
复制代码
/*
* 说明: 首先通过3.3接口注册这个回调函数,然后执行3.6接口开始解析,每碰到一个新字段这个函数就被回调一次
* userData: 通过3.2接口传递进来的参数,可通过这个值将解析出来的数据返回出去
* name: 开始字段名称
* atts: 指向NULL结尾的XML_Char指针数组, 每两个连续的元素构成一个键值对,分别表示元素的属性名和属性值
*/
void(XMLCALL *XML_StartElementHandler)(void *userData, const XML_Char *name, const XML_Char **atts);
3.8 处理XML数据字段结束的回调函数
c
复制代码
/*
* 说明: 首先通过3.3接口注册这个回调函数,然后执行3.6接口开始解析,每碰到一个字段结束这个函数就被回调一次
* userData: 通过3.2接口传递进来的参数,可通过这个值将解析出来的数据返回出去
* name: 开始字段名称
*/
void(XMLCALL *XML_EndElementHandler)(void *userData, const XML_Char *name);
3.9 处理XML数据文本内容的回调函数
c
复制代码
/*
* 说明: 首先通过3.4接口注册这个回调函数,然后执行3.6接口开始解析,每碰到文本内容这个函数就被回调一次
* userData: 通过3.2接口传递进来的参数,可通过这个值将解析出来的数据返回出去
* s: 文本内容
* len: 文本内容长度
*/
void(XMLCALL *XML_CharacterDataHandler)(void *userData, const XML_Char *s, int len);
4 实例演示
xml
复制代码
<?xml version="1.0"?>
<data>
<header hattr="http">
<type>Post</type>
<host>127.0.0.1</host>
</header>
<body battr="base64">
<data1>aGVsbG8=</data1>
<data2>ZXhwYXQ=</data2>
</body>
</data>
c
复制代码
#include <stdio.h>
#include <expat.h>
#include <iostream>
#include <vector>
#include <map>
#ifndef _WIN32
#include <string.h>
#endif
// 定义一个结构,保存字段名和字段值,这里为了演示简洁属性值就不保存了
typedef struct USERDATA {
std::string strName; //字段名
std::string strValue; // 字段值
}StUserData;
// 调用 XML_Parse 开始解析数据后,只要碰到字段名,这个函数就会被调用
// 比如碰到data开始时,该函数会被回调一次,碰到header开始时,会再次被回调
void startElement(void *userData, const XML_Char *name, const XML_Char **atts)
{
// 将字段名保存
std::vector<StUserData> *vecData = (std::vector<StUserData>*)userData;
StUserData stData;
stData.strName.assign(name);
vecData->insert(vecData->end(), stData);
// 打印字段名
printf("startElement name : %s\n", name);
// 打印属性
for (int i = 0; atts[i]; i += 2) {
// 属性名和属性值
printf("%s:%s\n", atts[i], atts[i + 1]);
}
}
// 调用 XML_Parse 开始解析数据后,只要碰到字段名结束,这个回调函数就会被调用
// 比如碰到header结束时,该函数会被回调一次
void endElement(void *userData, const XML_Char *name)
{
printf("endElement name : %s\n", name);
}
// 调用 XML_Parse 开始解析数据后,只要碰到文本,这个函数就会被回调
// 比如碰到data和header时,并没有文本内容,下一层还有数据,因此不会被调用
// 碰到type时,有文本内容了,是Post,因此该函数会被调用
void characterData(void *userData, const XML_Char *s, int len) {
// startElement 被调用后,只要对应的字段名有值,这个函数就会被调用
// 所以文本值保存到最后一个数据中,保证字段名和文本内容对应
std::vector<StUserData> *vecData = (std::vector<StUserData>*)userData;
StUserData stData;
stData.strName = vecData->at(vecData->size() - 1).strName;
stData.strValue.assign(s, len);
vecData->at(vecData->size() - 1) = stData;
// 打印文本内容
printf("value : ");
for (int i = 0; i < len; i++) {
printf("%c", s[i]);
}
printf("\n");
}
int main(int argc, const char *argv[])
{
std::vector<StUserData> vecData;
XML_Parser parser = XML_ParserCreate(NULL);
if (parser == NULL) {
return -1;
}
// 设置用户自定义的数据
XML_SetUserData(parser, &vecData);
// 注册两个回调函数,分别处理元素的开始和结束事件
XML_SetElementHandler(parser, startElement, endElement);
// 注册一个回调函数来处理 XML 文档中元素内的文本内容
XML_SetCharacterDataHandler(parser, characterData);
// 开始解析数据
const char* xmlData = "<?xml version=\"1.0\"?><data><header hattr=\"http\"><type>Post</type><host>127.0.0.1</host></header><body battr=\"base64\"><data1>aGVsbG8=</data1><data2>ZXhwYXQ=</data2></body></data>";
if(!XML_Parse(parser, xmlData, strlen(xmlData), false)){
printf("XML_Parse failed : %s at line %lu\n", XML_ErrorString(XML_GetErrorCode(parser)), XML_GetCurrentLineNumber(parser));
system("pause");
return -1;
}
printf("==================================================\n");
// 打印我们在自己定义的数据结构中保存的数据
for (int i = 0; i < vecData.size(); i++) {
// 没有文本内容时只打印字段值
if (vecData.at(i).strValue.empty()) {
std::cout << vecData.at(i).strName.c_str() << std::endl;
}
else {
std::cout <<" "<< vecData.at(i).strName.c_str() << " : " << vecData.at(i).strValue.c_str() << std::endl;
}
}
// 释放xml解析器
XML_ParserFree(parser);
system("pause");
return 0;
}
- 输出结果