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 .../> 表示没有子内容,简洁明了。
我的习惯:属性里只放不可变标识,可变数据全放子元素。这样后期扩展时不用改Schema,兼容性好。

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() 保存到文件 可设置缩进格式
曾经踩过的坑:libxml2返回的字符串是 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序列化/反序列化的完整流程。你写代码时照着这个思路走,不会乱。

XML序列化/反序列化核心流程 反序列化(解析) xmlReadFile() → xmlDocPtr xmlDocGetRootElement() → 根节点 遍历子节点 → 提取属性/文本 填充C结构体 → 释放xmlChar* 序列化(生成) xmlNewDoc() → 创建文档 xmlNewChild() → 构建节点树 xmlNewProp() → 添加属性 xmlSaveFormatFile() → 写入文件 关键:xmlChar* 用完即释放,数值转字符串注意精度

5.6 避坑指南

最后分享几个我实战中遇到的坑,你遇到了能少走弯路:

  • 编码问题:XML声明里写UTF-8,但文件实际是GBK——解析直接崩。我后来统一在代码里用 xmlReadFile() 的第二个参数强制指定编码。
  • 空白节点:libxml2会把换行和空格也当成文本节点。遍历时一定要加 if (cur->type != XML_ELEMENT_NODE) continue; 过滤掉。
  • 内存泄漏:每次 xmlGetPropxmlNodeGetContent 后,记得 xmlFree。我曾经在循环里漏了一次,跑了一周后设备内存耗尽重启。
  • 大文件性能:如果XML文件超过10MB,别用DOM解析(一次性加载到内存)。改用 xmlTextReader 流式解析,内存占用能降90%。
调试小技巧:解析出问题时,先用 xmlParseFile() 配合 xmlGetLastError() 看错误信息。大部分情况是标签不匹配或特殊字符没转义。

XML序列化其实不复杂。记住“解析用遍历,生成用创建”这八个字,再配合libxml2那七八个核心函数,大部分场景都能搞定。嗯,今天就到这里。


公众号:蓝海资料掘金营,微信deep3321