30、INI配置文件解析:键值对解析,节区管理,读写实现
INI文件,说白了就是最朴素的配置文件格式。你肯定见过——方括号括起来的节区名,等号连接的键值对。这东西在嵌入式系统里特别常见,因为简单、轻量、人类可读。我最早接触INI解析是在一个工控项目上,设备参数全存在一个config.ini里,当时我心想:这么简单的格式,手写解析不就完了?结果真写起来才发现,坑比想象的多。
INI文件的基本结构
先看一个典型的INI文件长什么样:
; 这是注释(分号开头)
[system]
baudrate = 115200
parity = none
stopbits = 1
[sensor]
type = temperature
range_min = -40
range_max = 85
unit = celsius
[network]
dhcp = true
ip = 192.168.1.100
mask = 255.255.255.0
结构其实就三层:
- 注释行:以分号或井号开头,解析时直接跳过
- 节区(Section):用方括号括起来的名称,表示一个配置分组
- 键值对(Key-Value):等号或冒号分隔,左右两侧可以有多余空格
嗯,这里要注意:等号两边可能有空格,也可能没有。我见过有些设备生成的INI文件,等号前后各加两个空格,看着整齐但解析时得处理。
解析器的核心设计
我个人习惯把INI解析器设计成状态机。为什么?因为逐行读取时,每一行属于哪个节区、是键值对还是注释,状态非常清晰。
核心数据结构:
// 键值对节点
typedef struct kv_node {
char key[64];
char value[128];
struct kv_node *next;
} kv_node_t;
// 节区节点
typedef struct section_node {
char name[64];
kv_node_t *kv_head; // 该节区下的键值对链表
struct section_node *next;
} section_node_t;
// 整个INI文件的管理结构
typedef struct {
section_node_t *section_head;
int section_count;
} ini_manager_t;
我用链表来管理节区和键值对,而不是数组。原因很简单:嵌入式环境里,你事先不知道配置项有多少条。链表可以动态增长,内存按需分配。我在一个项目里遇到过配置文件有200多个键值对,如果用固定数组,要么浪费内存,要么不够用。
解析流程:逐行处理
解析的核心逻辑其实就三步:
- 读一行,去掉首尾空白字符
- 判断类型:注释跳过,节区名创建新节区,键值对解析并挂到当前节区下
- 错误处理:格式不对的行,记录日志但不要崩
我曾经犯过一个错误:解析时遇到空行直接返回错误。结果客户反馈说他们的INI文件里习惯用空行分组,我的解析器直接罢工了。从那以后,我养成了一个习惯——对格式宽容,对逻辑严格。
int ini_parse_line(ini_manager_t *mgr, const char *line, section_node_t **cur_section) {
char buffer[256];
strncpy(buffer, line, sizeof(buffer) - 1);
// 去掉首尾空格
char *p = buffer;
while (*p == ' ' || *p == '\t') p++;
size_t len = strlen(p);
while (len > 0 && (p[len-1] == ' ' || p[len-1] == '\t' || p[len-1] == '\r' || p[len-1] == '\n')) {
p[--len] = '\0';
}
// 空行或注释,跳过
if (len == 0 || p[0] == ';' || p[0] == '#') {
return 0;
}
// 节区
if (p[0] == '[') {
char *end = strchr(p, ']');
if (end == NULL) return -1; // 格式错误
*end = '\0';
*cur_section = ini_add_section(mgr, p + 1);
return 0;
}
// 键值对
char *eq = strchr(p, '=');
if (eq == NULL) eq = strchr(p, ':'); // 也支持冒号分隔
if (eq == NULL) return -1;
*eq = '\0';
char *key = p;
char *value = eq + 1;
// 去掉key和value两边的空格
// ...(省略具体实现)
ini_add_kv(*cur_section, key, value);
return 0;
}
小技巧:解析时最好保留原始字符串的副本,不要直接修改文件缓冲区。这样如果后续需要写回文件,可以保留注释和格式。
节区管理:查找与遍历
节区管理说白了就是链表的增删改查。但有一个细节很多人会忽略:节区名大小写敏感吗?
我建议统一转成小写再比较。为什么?因为Windows和Linux下,INI文件的节区名大小写习惯不一样。Windows下经常是首字母大写,Linux下全是小写。如果你不做归一化,同一个配置文件在两个平台上解析结果可能不同。
section_node_t* ini_find_section(ini_manager_t *mgr, const char *name) {
section_node_t *cur = mgr->section_head;
char name_lower[64];
// 将name转成小写
str_to_lower(name_lower, name);
while (cur) {
char sec_lower[64];
str_to_lower(sec_lower, cur->name);
if (strcmp(sec_lower, name_lower) == 0) {
return cur;
}
cur = cur->next;
}
return NULL;
}
读写实现:序列化与反序列化
读(解析)我们已经讲了,写(序列化)其实更简单——遍历链表,按格式输出即可。
int ini_save_to_file(ini_manager_t *mgr, const char *filename) {
FILE *fp = fopen(filename, "w");
if (!fp) return -1;
section_node_t *sec = mgr->section_head;
while (sec) {
fprintf(fp, "[%s]\n", sec->name);
kv_node_t *kv = sec->kv_head;
while (kv) {
fprintf(fp, "%s = %s\n", kv->key, kv->value);
kv = kv->next;
}
fprintf(fp, "\n");
sec = sec->next;
}
fclose(fp);
return 0;
}
这里有个问题:写回文件时,原来的注释和空行全丢了。如果你需要保留这些,就得在解析时把每一行(包括注释)都存下来。我一般只在两种场景下用INI:
- 配置文件由程序自动生成,不需要保留注释
- 配置文件由人工编辑,但程序只读不写
如果既要人工编辑又要程序修改,我建议用JSON或YAML,它们有成熟的库支持。
避坑指南
我曾经踩过的坑:
- 值中包含等号:比如密码字段"password = abc=123",解析时只取第一个等号,后面的都算值的一部分
- 节区名重复:同一个节区出现两次,是合并还是覆盖?我建议合并,后出现的键值对覆盖前面的
- 内存泄漏:链表节点都是malloc出来的,解析失败或重新加载时一定要释放干净
- 编码问题:INI文件没有指定编码,如果配置项包含中文,建议统一用UTF-8 without BOM
知识体系总览
下面这张图概括了INI解析器的整体架构和核心流程:
实际项目中的取舍
说实话,INI格式在大型系统里已经不太够用了。但在嵌入式领域,它依然活得很好。原因有三:
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| MCU裸机或RTOS | INI | 解析器代码量小(几百行C),无依赖 |
| Linux嵌入式 | JSON/YAML | 有现成库,支持嵌套结构 |
| 需要人工频繁编辑 | INI | 人类可读性最好,语法最宽松 |
| 配置项超过500个 | SQLite/二进制 | INI的线性查找效率太低 |
我个人建议:如果你的项目里配置项不超过100个,且不需要嵌套结构,用INI完全够用。别为了用JSON而引入一个JSON库,那可能比你的业务代码还大。
一个实用建议:写INI解析器时,把错误处理做得友好一点。比如解析失败时,打印出是哪一行出了问题,而不是简单返回-1。我在调试一个传感器网络时,就因为配置文件里多了一个不可见字符,排查了整整一个下午。从那以后,我的解析器都会输出类似这样的信息:
[WARN] 第42行解析失败:'baudrate = 115200\x00' 包含非法字符
好了,INI解析器就聊到这儿。核心就三件事:逐行解析、链表管理、容错处理。你可以在自己的项目里实现一个轻量版,也可以直接用现成的库。但不管哪种方式,理解底层的解析逻辑,对你以后处理任何文本格式都有帮助。
公众号:蓝海资料掘金营,微信deep3321