文本文件操作实战:配置文件读写、CSV文件解析、日志文件生成

文本文件操作,说白了就是跟字符串打交道。我做了这么多年嵌入式,发现很多新手一上来就想着用二进制流,其实大部分场景下,文本文件反而更直观、更好调试。今天咱们就聊聊三个最常用的实战场景:配置文件、CSV解析、日志生成。

一、配置文件读写:INI风格的实现

嵌入式设备里,配置文件太常见了。WiFi密码、采样率、阈值……这些参数如果硬编码在代码里,每次改参数都得重新编译烧录,想想就头疼。我个人习惯用类似INI的格式:

# sensor_config.ini
[system]
sample_rate=100
log_level=3

[sensor]
threshold=45.5
calibration_offset=2

解析这种文件,核心就是逐行读取,然后判断是节(section)、键值对、还是注释。我贴一段我常用的解析函数:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAX_LINE 256
#define MAX_SECTION 64

typedef struct {
    char section[MAX_SECTION];
    char key[MAX_SECTION];
    char value[MAX_SECTION];
} ConfigItem;

int parse_config(const char *filename, ConfigItem *items, int max_items) {
    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)) {
        // 去掉换行符
        line[strcspn(line, "\n")] = 0;

        // 跳过空行和注释
        if (line[0] == '\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(items[count].section, current_section, MAX_SECTION - 1);
            strncpy(items[count].key, line, MAX_SECTION - 1);
            strncpy(items[count].value, eq + 1, MAX_SECTION - 1);
            count++;
            if (count >= max_items) break;
        }
    }

    fclose(fp);
    return count;
}
小技巧:解析时别忘了去掉键值对两端的空格。我曾经被一个"threshold= 45.5"(等号后面多了个空格)坑过,调试了半天才发现是字符串比较没做trim。

二、CSV文件解析:逗号分隔的艺术

CSV文件在数据采集、日志导出中非常常见。格式简单,但坑不少。比如字段里可能包含逗号,或者换行符。我一般用状态机来解析,这样更健壮。

先看一个典型的CSV数据:

timestamp,temperature,humidity
2024-01-15 10:30:00,25.3,60.1
2024-01-15 10:31:00,25.5,59.8

解析代码的核心逻辑:

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) {
        if (*p == '"') {
            in_quotes = !in_quotes;
        } else if (*p == ',' && !in_quotes) {
            *p = '\0';
            fields[count++] = start;
            start = p + 1;
            if (count >= max_fields) break;
        }
        p++;
    }
    // 最后一个字段
    if (count < max_fields) {
        fields[count++] = start;
    }

    return count;
}
注意:如果CSV字段里包含换行符(比如某些Excel导出的数据),用fgets逐行读取会出错。这种情况下,我建议用fread读取整个文件,然后自己处理换行逻辑。

三、日志文件生成:时间戳与轮转

日志文件是嵌入式系统的黑匣子。我习惯在每条日志前加上时间戳,并且做文件轮转——防止日志把Flash撑爆。

一个简单的日志写入函数:

void write_log(const char *msg) {
    FILE *fp = fopen("system.log", "a");
    if (!fp) return;

    time_t now = time(NULL);
    struct tm *t = localtime(&now);

    fprintf(fp, "[%04d-%02d-%02d %02d:%02d:%02d] %s\n",
            t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
            t->tm_hour, t->tm_min, t->tm_sec, msg);

    fclose(fp);
}

文件轮转的逻辑也不复杂:

void rotate_log(const char *filename, int max_size) {
    FILE *fp = fopen(filename, "r");
    if (!fp) return;

    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);

    if (size > max_size) {
        // 把当前日志重命名为 .old
        char oldname[256];
        snprintf(oldname, sizeof(oldname), "%s.old", filename);
        rename(filename, oldname);
    }
}
经验之谈:日志文件不要写得太频繁。我曾经在一个项目里每10ms写一次日志,结果SD卡一个月就挂了。后来改成每1秒聚合一次再写入,寿命延长了10倍。

四、知识体系总览

下面这张图总结了文本文件操作的三个核心场景,以及它们之间的关系:

文本文件操作实战 配置文件读写 INI格式解析 节/键值对提取 注释与空行处理 CSV文件解析 逗号分隔处理 引号内转义 字段类型转换 日志文件生成 时间戳格式化 文件轮转策略 写入频率控制 公共基础:fopen / fgets / fscanf / fprintf 文件打开模式、缓冲区管理、错误处理 常见陷阱 字符串截断 | 编码问题 | 文件锁冲突 | 缓冲区溢出

五、避坑指南

文本文件操作看着简单,但实际项目中踩过的坑真不少。我挑几个典型的说说:

  • 字符串截断:fgets读取时,如果一行超过缓冲区大小,会被截断。我曾经在解析一个长配置行时,后半段参数丢了,导致设备启动异常。解决办法是检测行尾是否有换行符,没有的话说明被截断了,需要继续读取。
  • 编码问题:Windows下生成的文本文件可能是GBK编码,而Linux下默认UTF-8。我遇到过中文日志乱码的问题,后来统一用UTF-8 with BOM格式,并在文件头写入BOM标记。
  • 文件锁冲突:在多线程或多进程环境下,同时写日志文件会导致内容交错。我一般用flock或者创建一个临时文件做原子写入。
核心原则:文本文件操作,永远假设输入是恶意的。长度检查、格式校验、错误恢复,一个都不能少。我见过太多因为没检查fgets返回值就直接用缓冲区内容导致崩溃的案例了。

好了,关于文本文件操作的三个实战场景就聊到这儿。配置文件、CSV解析、日志生成,这三个技能掌握了,大部分嵌入式项目的文件IO需求都能搞定。记住一点:代码写出来只是第一步,能稳定跑上几个月不出问题,那才是真本事。

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