文本文件操作实战:配置文件读写、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倍。
四、知识体系总览
下面这张图总结了文本文件操作的三个核心场景,以及它们之间的关系:
五、避坑指南
文本文件操作看着简单,但实际项目中踩过的坑真不少。我挑几个典型的说说:
- 字符串截断:fgets读取时,如果一行超过缓冲区大小,会被截断。我曾经在解析一个长配置行时,后半段参数丢了,导致设备启动异常。解决办法是检测行尾是否有换行符,没有的话说明被截断了,需要继续读取。
- 编码问题:Windows下生成的文本文件可能是GBK编码,而Linux下默认UTF-8。我遇到过中文日志乱码的问题,后来统一用UTF-8 with BOM格式,并在文件头写入BOM标记。
- 文件锁冲突:在多线程或多进程环境下,同时写日志文件会导致内容交错。我一般用flock或者创建一个临时文件做原子写入。
核心原则:文本文件操作,永远假设输入是恶意的。长度检查、格式校验、错误恢复,一个都不能少。我见过太多因为没检查fgets返回值就直接用缓冲区内容导致崩溃的案例了。
好了,关于文本文件操作的三个实战场景就聊到这儿。配置文件、CSV解析、日志生成,这三个技能掌握了,大部分嵌入式项目的文件IO需求都能搞定。记住一点:代码写出来只是第一步,能稳定跑上几个月不出问题,那才是真本事。