24、配置文件解析:INI文件解析、CSV文件解析、JSON解析(cJSON库简介)
做嵌入式开发这些年,我处理过各种各样的配置文件。说实话,最头疼的不是写代码,而是跟各种格式的文件格式打交道。INI、CSV、JSON,这三种格式几乎覆盖了从简单到复杂的全部场景。今天我就把这三板斧的用法,掰开了揉碎了讲给你听。
24.1 INI文件解析——最朴素的配置格式
INI文件长什么样?你肯定见过:
; 这是一个注释
[network]
ip = 192.168.1.100
port = 8080
timeout = 30
[logging]
level = debug
file = /var/log/app.log
结构很简单:节(Section)、键(Key)、值(Value)。我在一个物联网网关项目里,就用INI文件来存设备参数。为什么选它?因为嵌入式工程师用记事本就能改,不需要任何额外工具。
24.1.1 手写一个INI解析器
其实不用第三方库,我们自己就能写一个轻量级的解析器。核心思路就三步:
- 按行读取文件
- 识别节名(用方括号括起来的)
- 解析键值对(用等号分隔)
来看代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_LINE 256
#define MAX_SECTION 64
#define MAX_KEY 64
#define MAX_VALUE 256
typedef struct {
char section[MAX_SECTION];
char key[MAX_KEY];
char value[MAX_VALUE];
} INIEntry;
int parse_ini(const char* filename, INIEntry* entries, int max_entries) {
FILE* fp = fopen(filename, "r");
if (!fp) return -1;
char line[MAX_LINE];
char current_section[MAX_SECTION] = "";
int count = 0;
while (fgets(line, sizeof(line), fp) && count < max_entries) {
// 去掉换行符
line[strcspn(line, "\n")] = 0;
// 跳过空行和注释
if (line[0] == '\0' || line[0] == ';' || line[0] == '#') continue;
// 检查是否是节
if (line[0] == '[') {
char* end = strchr(line, ']');
if (end) {
*end = '\0';
strncpy(current_section, line + 1, MAX_SECTION - 1);
}
continue;
}
// 解析键值对
char* eq = strchr(line, '=');
if (eq) {
*eq = '\0';
strncpy(entries[count].section, current_section, MAX_SECTION - 1);
strncpy(entries[count].key, line, MAX_KEY - 1);
strncpy(entries[count].value, eq + 1, MAX_VALUE - 1);
count++;
}
}
fclose(fp);
return count;
}
小技巧:我习惯在解析时去掉键值对两端的空格。用while(isspace(*p)) p++;这种手法,能避免很多因为手抖多打空格导致的bug。
24.2 CSV文件解析——表格数据的轻骑兵
CSV(Comma-Separated Values)说白了就是用逗号分隔的表格数据。我在做数据采集项目时,传感器日志就存成CSV格式,每行一条记录,用Excel直接能打开看,调试起来特别方便。
一个典型的CSV文件:
id,temperature,humidity,time
1,25.3,60.1,2024-01-15 10:00:00
2,26.1,58.7,2024-01-15 10:01:00
3,24.8,62.3,2024-01-15 10:02:00
24.2.1 解析CSV的坑
你以为CSV就是简单按逗号分割?太天真了。我曾经在一个项目里被坑惨了——某个字段里居然有逗号,比如地址字段"北京市,朝阳区"。直接按逗号分割,数据全乱了。
正确的做法是:
#include <stdio.h>
#include <string.h>
int parse_csv_line(char* line, char** fields, int max_fields) {
int count = 0;
char* p = line;
int in_quotes = 0;
char* start = p;
while (*p && count < max_fields) {
if (*p == '"') {
in_quotes = !in_quotes;
} else if (*p == ',' && !in_quotes) {
*p = '\0';
fields[count++] = start;
start = p + 1;
}
p++;
}
// 处理最后一个字段
if (count < max_fields) {
fields[count++] = start;
}
return count;
}
注意:CSV解析时,引号内的逗号不能作为分隔符。另外,如果字段本身包含引号,需要用两个引号转义("")。这些边界情况,写代码时一定要考虑到。
24.3 JSON解析——现代配置的标配
JSON现在几乎成了配置文件的通用语言。结构清晰,支持嵌套,比INI灵活,比CSV强大。在嵌入式领域,cJSON是最流行的轻量级JSON解析库,整个库就一个.c和一个.h文件,移植起来特别方便。
24.3.1 cJSON库简介
cJSON的核心数据结构就一个:
typedef struct cJSON {
struct cJSON *next, *prev; // 链表指针
struct cJSON *child; // 子节点(用于数组和对象)
int type; // 类型:对象、数组、字符串、数字等
char *valuestring; // 字符串值
int valueint; // 整数值
double valuedouble; // 浮点值
char *string; // 键名
} cJSON;
你看,它用链表来组织数据,每个节点可以指向子节点,天然支持嵌套结构。我当初第一次看到这个设计时,觉得挺巧妙的——既节省内存,又方便遍历。
24.3.2 实战:解析JSON配置文件
假设我们有这样一个JSON配置文件:
{
"device": {
"name": "sensor_node_01",
"type": "temperature",
"enabled": true
},
"network": {
"wifi": {
"ssid": "iot_network",
"password": "12345678"
},
"mqtt": {
"broker": "mqtt.example.com",
"port": 1883,
"topics": ["sensor/data", "sensor/status"]
}
},
"sampling": {
"interval": 5,
"units": "seconds"
}
}
用cJSON解析的代码:
#include "cJSON.h"
#include <stdio.h>
void parse_config(const char* json_str) {
cJSON* root = cJSON_Parse(json_str);
if (!root) {
printf("JSON解析失败: %s\n", cJSON_GetErrorPtr());
return;
}
// 读取设备名称
cJSON* device = cJSON_GetObjectItem(root, "device");
if (device) {
cJSON* name = cJSON_GetObjectItem(device, "name");
if (cJSON_IsString(name)) {
printf("设备名称: %s\n", name->valuestring);
}
}
// 读取采样间隔
cJSON* sampling = cJSON_GetObjectItem(root, "sampling");
if (sampling) {
cJSON* interval = cJSON_GetObjectItem(sampling, "interval");
if (cJSON_IsNumber(interval)) {
printf("采样间隔: %d %s\n",
interval->valueint,
cJSON_GetObjectItem(sampling, "units")->valuestring);
}
}
// 读取MQTT主题列表
cJSON* network = cJSON_GetObjectItem(root, "network");
if (network) {
cJSON* mqtt = cJSON_GetObjectItem(network, "mqtt");
if (mqtt) {
cJSON* topics = cJSON_GetObjectItem(mqtt, "topics");
if (cJSON_IsArray(topics)) {
printf("MQTT主题:\n");
cJSON* topic = NULL;
cJSON_ArrayForEach(topic, topics) {
if (cJSON_IsString(topic)) {
printf(" - %s\n", topic->valuestring);
}
}
}
}
}
cJSON_Delete(root);
}
核心要点:
- 解析前先检查
cJSON_Parse的返回值,NULL表示解析失败 - 用
cJSON_IsString、cJSON_IsNumber等函数做类型检查,别直接访问成员 - 用完记得调用
cJSON_Delete释放内存,否则会有内存泄漏 - 遍历数组用
cJSON_ArrayForEach宏,比手动遍历链表更安全
24.4 三种格式怎么选?
我个人的经验是这样的:
| 格式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| INI | 简单配置,少量参数 | 人类可读性好,解析简单 | 不支持嵌套,表达能力有限 |
| CSV | 表格数据,批量记录 | Excel可直接打开,结构紧凑 | 不支持复杂类型,转义规则麻烦 |
| JSON | 复杂配置,树形结构 | 表达能力强,生态完善 | 解析开销稍大,文件体积较大 |
嗯,这里要注意:在资源受限的MCU上,如果配置项不超过20个,我建议用INI。如果数据量很大且需要嵌套结构,那就上JSON。CSV嘛,主要用在数据导出和日志记录上。
避坑指南:我曾经在一个项目里,用JSON存了上千条传感器校准数据。结果MCU的堆空间只有8KB,cJSON解析到一半就崩了。后来改成CSV格式,逐行解析,问题就解决了。所以啊,选格式不光要看功能,还得看硬件资源。
好了,这三种格式的解析方法就讲到这里。实际项目中,你可能会遇到混合使用的情况——比如用JSON做主配置,用CSV存数据日志。灵活运用就好,没有银弹。