5、XML序列化:XML格式简介、libxml2库的使用、XML序列化与反序列化
说到XML,很多嵌入式工程师第一反应是“太重量级了”。说实话,我早年也这么想。直到有一次做设备配置管理,JSON存不下复杂的嵌套结构,自定义二进制格式又不好调试——最后还是老老实实上了XML。
XML确实比JSON啰嗦,但它有两个杀手锏:自带Schema校验,支持注释。在工业设备、医疗仪器这些需要长期维护的场景里,XML的自我描述能力是无可替代的。今天我就带你把它拿下。
5.1 XML格式核心要点
XML说白了就是一棵树。根节点下面挂子节点,子节点还能挂子节点。每个节点可以有属性,也可以有文本内容。嗯,就这么简单。
一个典型的XML长这样:
<?xml version="1.0" encoding="UTF-8"?>
<device_config>
<sensor id="temp1">
<name>温度传感器A</name>
<unit>℃</unit>
<range min="-40" max="85"/>
</sensor>
<sensor id="hum1">
<name>湿度传感器B</name>
<unit>%RH</unit>
<range min="0" max="100"/>
</sensor>
</device_config>
这里有几个关键点:
- 声明行:
<?xml ...?>告诉解析器版本和编码。我建议一律用UTF-8,少给自己找麻烦。 - 根元素:有且只有一个。上面例子里是
device_config。 - 属性 vs 子元素:属性适合放“元数据”,比如ID、类型;子元素适合放“数据本体”,比如名字、数值。
- 自闭合标签:
<range .../>表示没有子内容,简洁明了。
5.2 libxml2库快速上手
Linux下做XML解析,libxml2是事实标准。我最早用它在ARM Linux板上解析设备树配置,跑了三年没出过内存泄漏。你想想看,这库的稳定性是经过实战检验的。
安装很简单:
sudo apt-get install libxml2-dev
# 编译时加 -lxml2
核心API就几个:
| 函数 | 作用 | 注意 |
|---|---|---|
xmlReadFile() |
从文件读取XML | 返回 xmlDocPtr |
xmlDocGetRootElement() |
获取根节点 | 返回 xmlNodePtr |
xmlNodeGetContent() |
获取节点文本 | 返回 xmlChar*,用完要free |
xmlGetProp() |
获取属性值 | 同样要free |
xmlNewChild() |
创建子节点 | 构建XML时用 |
xmlNewProp() |
创建属性 | 构建XML时用 |
xmlSaveFormatFile() |
保存到文件 | 可设置缩进格式 |
xmlChar*(本质是 unsigned char*),直接printf会乱码。记得强转成 (const char*)。另外,所有 xmlChar* 都要用 xmlFree() 释放,别用free()。
5.3 XML反序列化实战
好,我们直接写代码。假设要解析上面那个设备配置XML,提取传感器信息。
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <stdio.h>
#include <string.h>
typedef struct {
char id[16];
char name[64];
char unit[16];
float min_val;
float max_val;
} sensor_t;
int parse_sensor_config(const char *filename, sensor_t *sensors, int max_count) {
xmlDocPtr doc = xmlReadFile(filename, NULL, 0);
if (doc == NULL) {
fprintf(stderr, "无法解析XML文件: %s\n", filename);
return -1;
}
xmlNodePtr root = xmlDocGetRootElement(doc);
if (root == NULL || xmlStrcmp(root->name, (const xmlChar*)"device_config")) {
fprintf(stderr, "根节点错误\n");
xmlFreeDoc(doc);
return -1;
}
int count = 0;
for (xmlNodePtr cur = root->children; cur != NULL; cur = cur->next) {
if (cur->type != XML_ELEMENT_NODE) continue;
if (xmlStrcmp(cur->name, (const xmlChar*)"sensor")) continue;
if (count >= max_count) break;
// 读取id属性
xmlChar *id = xmlGetProp(cur, (const xmlChar*)"id");
if (id) {
strncpy(sensors[count].id, (const char*)id, sizeof(sensors[count].id)-1);
xmlFree(id);
}
// 遍历子节点
for (xmlNodePtr child = cur->children; child != NULL; child = child->next) {
if (child->type != XML_ELEMENT_NODE) continue;
if (!xmlStrcmp(child->name, (const xmlChar*)"name")) {
xmlChar *val = xmlNodeGetContent(child);
if (val) {
strncpy(sensors[count].name, (const char*)val, sizeof(sensors[count].name)-1);
xmlFree(val);
}
} else if (!xmlStrcmp(child->name, (const xmlChar*)"unit")) {
xmlChar *val = xmlNodeGetContent(child);
if (val) {
strncpy(sensors[count].unit, (const char*)val, sizeof(sensors[count].unit)-1);
xmlFree(val);
}
} else if (!xmlStrcmp(child->name, (const xmlChar*)"range")) {
xmlChar *min = xmlGetProp(child, (const xmlChar*)"min");
xmlChar *max = xmlGetProp(child, (const xmlChar*)"max");
if (min) { sensors[count].min_val = atof((const char*)min); xmlFree(min); }
if (max) { sensors[count].max_val = atof((const char*)max); xmlFree(max); }
}
}
count++;
}
xmlFreeDoc(doc);
return count;
}
这段代码我用了很多项目。核心逻辑就是:拿到根节点 → 遍历子节点 → 匹配标签名 → 提取属性或文本。注意每次拿到 xmlChar* 后立即拷贝到自己的结构体里,然后释放——这是防止内存泄漏的铁律。
5.4 XML序列化实战
反序列化搞定了,序列化就是反过来。把结构体数据写回XML文件。
int serialize_sensor_config(const char *filename, sensor_t *sensors, int count) {
xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0");
xmlNodePtr root = xmlNewNode(NULL, BAD_CAST "device_config");
xmlDocSetRootElement(doc, root);
for (int i = 0; i < count; i++) {
xmlNodePtr sensor = xmlNewChild(root, NULL, BAD_CAST "sensor", NULL);
xmlNewProp(sensor, BAD_CAST "id", BAD_CAST sensors[i].id);
xmlNewChild(sensor, NULL, BAD_CAST "name", BAD_CAST sensors[i].name);
xmlNewChild(sensor, NULL, BAD_CAST "unit", BAD_CAST sensors[i].unit);
char min_str[32], max_str[32];
snprintf(min_str, sizeof(min_str), "%.1f", sensors[i].min_val);
snprintf(max_str, sizeof(max_str), "%.1f", sensors[i].max_val);
xmlNodePtr range = xmlNewChild(sensor, NULL, BAD_CAST "range", NULL);
xmlNewProp(range, BAD_CAST "min", BAD_CAST min_str);
xmlNewProp(range, BAD_CAST "max", BAD_CAST max_str);
}
int ret = xmlSaveFormatFile(filename, doc, 1); // 1表示缩进
xmlFreeDoc(doc);
return ret;
}
你看,xmlNewChild 最后一个参数传NULL表示创建空节点,然后手动加属性。对于纯文本节点,直接传字符串就行。最后 xmlSaveFormatFile 的第二个参数传1,输出的XML会自动缩进,可读性很好。
snprintf 控制小数位数,避免出现0.999999这种尴尬情况。
5.5 知识体系总览
下面这张图总结了XML序列化/反序列化的完整流程。你写代码时照着这个思路走,不会乱。
5.6 避坑指南
最后分享几个我实战中遇到的坑,你遇到了能少走弯路:
- 编码问题:XML声明里写UTF-8,但文件实际是GBK——解析直接崩。我后来统一在代码里用
xmlReadFile()的第二个参数强制指定编码。 - 空白节点:libxml2会把换行和空格也当成文本节点。遍历时一定要加
if (cur->type != XML_ELEMENT_NODE) continue;过滤掉。 - 内存泄漏:每次
xmlGetProp或xmlNodeGetContent后,记得xmlFree。我曾经在循环里漏了一次,跑了一周后设备内存耗尽重启。 - 大文件性能:如果XML文件超过10MB,别用DOM解析(一次性加载到内存)。改用
xmlTextReader流式解析,内存占用能降90%。
xmlParseFile() 配合 xmlGetLastError() 看错误信息。大部分情况是标签不匹配或特殊字符没转义。
XML序列化其实不复杂。记住“解析用遍历,生成用创建”这八个字,再配合libxml2那七八个核心函数,大部分场景都能搞定。嗯,今天就到这里。
公众号:蓝海资料掘金营,微信deep3321