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解析器

其实不用第三方库,我们自己就能写一个轻量级的解析器。核心思路就三步:

  1. 按行读取文件
  2. 识别节名(用方括号括起来的)
  3. 解析键值对(用等号分隔)

来看代码:

#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_IsStringcJSON_IsNumber等函数做类型检查,别直接访问成员
  • 用完记得调用cJSON_Delete释放内存,否则会有内存泄漏
  • 遍历数组用cJSON_ArrayForEach宏,比手动遍历链表更安全

24.4 三种格式怎么选?

我个人的经验是这样的:

格式 适用场景 优点 缺点
INI 简单配置,少量参数 人类可读性好,解析简单 不支持嵌套,表达能力有限
CSV 表格数据,批量记录 Excel可直接打开,结构紧凑 不支持复杂类型,转义规则麻烦
JSON 复杂配置,树形结构 表达能力强,生态完善 解析开销稍大,文件体积较大

嗯,这里要注意:在资源受限的MCU上,如果配置项不超过20个,我建议用INI。如果数据量很大且需要嵌套结构,那就上JSON。CSV嘛,主要用在数据导出和日志记录上。

避坑指南:我曾经在一个项目里,用JSON存了上千条传感器校准数据。结果MCU的堆空间只有8KB,cJSON解析到一半就崩了。后来改成CSV格式,逐行解析,问题就解决了。所以啊,选格式不光要看功能,还得看硬件资源。

配置文件解析知识体系 配置文件解析 INI 格式 CSV 格式 JSON 格式 节(Section) 键值对(Key=Value) 逗号分隔 引号转义 逐行解析 cJSON库 嵌套结构 类型检查 选择建议:简单用INI,表格用CSV,复杂用JSON 注意:资源受限场景优先考虑INI或CSV,避免JSON解析带来的内存开销

好了,这三种格式的解析方法就讲到这里。实际项目中,你可能会遇到混合使用的情况——比如用JSON做主配置,用CSV存数据日志。灵活运用就好,没有银弹。

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