21、多文件合并与分割:文件合并工具实现、文件分割算法、大文件处理策略
做嵌入式开发这些年,我处理过不少文件操作的需求。说实话,文件合并与分割这个主题,看着简单,真上手了坑不少。你想想看,一个日志文件动不动几百兆,或者你要把固件包拆成小块通过串口发送——这时候,合并与分割的基本功就派上用场了。
我个人习惯把这类操作分成三个层次:小文件随便玩、大文件要讲究、超大文件得用策略。今天咱们就一层层聊透。
文件合并工具的实现
文件合并,说白了就是把多个文件的内容按顺序拼成一个文件。我在项目中遇到过最典型的场景:设备采集了一天数据,生成了几十个小的CSV文件,最后需要合并成一个做分析。
核心思路其实就一句话:逐个打开源文件,读一块写一块,直到读完。别小看这个“读一块写一块”,很多人一上来就用 fread 把整个文件读到内存,小文件还行,大文件直接崩。
合并工具的核心流程:
- 获取所有待合并文件的路径列表
- 以二进制追加模式打开目标文件
- 对每个源文件:以二进制读模式打开,循环读取缓冲区大小数据,写入目标文件
- 关闭所有文件句柄
这里我贴一段我常用的合并函数,你感受一下:
#define BUF_SIZE 8192 // 8KB缓冲区,嵌入式环境够用
int merge_files(const char *output, const char **inputs, int count) {
FILE *fp_out = fopen(output, "wb");
if (!fp_out) return -1;
char *buf = (char*)malloc(BUF_SIZE);
if (!buf) { fclose(fp_out); return -1; }
for (int i = 0; i < count; i++) {
FILE *fp_in = fopen(inputs[i], "rb");
if (!fp_in) {
fprintf(stderr, "无法打开: %s\n", inputs[i]);
continue; // 我习惯跳过错误文件,不中断整个流程
}
size_t bytes;
while ((bytes = fread(buf, 1, BUF_SIZE, fp_in)) > 0) {
fwrite(buf, 1, bytes, fp_out);
}
fclose(fp_in);
}
free(buf);
fclose(fp_out);
return 0;
}
我的小技巧:缓冲区大小选 4KB 到 64KB 之间比较合适。太小的缓冲区导致频繁IO,太大又浪费内存。8KB 是我在ARM Cortex-M上常用的值,兼顾了速度和资源。
文件分割算法
文件分割正好反过来——把一个文件切成若干小块。为什么要切?我遇到过的情况:通过蓝牙传固件,一次只能发 1KB;或者要把大文件存到FAT32的U盘上,单个文件不能超过4GB。
分割策略有两种主流方式:
- 按大小分割:每块固定字节数,最后一块可能小一些
- 按数量分割:指定切几块,每块大小均分(有余数时最后一块补齐或截断)
我个人更常用按大小分割,因为接收端处理起来简单。来看实现:
int split_file(const char *src, size_t chunk_size) {
FILE *fp_src = fopen(src, "rb");
if (!fp_src) return -1;
char *buf = (char*)malloc(chunk_size);
if (!buf) { fclose(fp_src); return -1; }
char out_name[256];
int part = 0;
size_t bytes;
while ((bytes = fread(buf, 1, chunk_size, fp_src)) > 0) {
snprintf(out_name, sizeof(out_name), "%s.part%d", src, part++);
FILE *fp_out = fopen(out_name, "wb");
if (!fp_out) break;
fwrite(buf, 1, bytes, fp_out);
fclose(fp_out);
}
free(buf);
fclose(fp_src);
return part; // 返回分割出的文件数量
}
注意:分割时一定要用二进制模式打开文件!文本模式下,Windows系统会把 0x0A 自动转成 0x0D 0x0A,导致分割后的文件合并回去时数据错乱。我曾经在这个问题上折腾了一下午……
大文件处理策略
说到大文件,得先定义一下什么叫“大”。在嵌入式系统里,可能 10MB 就算大了——毕竟很多MCU的RAM才几百KB。这时候,上面那种一次性分配缓冲区的方式就不行了。
我总结了几条实战策略:
- 流式处理:永远不要一次性把整个文件读进内存。用固定大小的缓冲区,边读边写。这是最核心的原则。
- 使用
fseek和ftell获取文件大小:但要注意,ftell返回的是long类型,对于超过2GB的文件会溢出。在64位系统上可以用fseeko和ftello。 - 分块索引:如果需要对大文件随机访问,可以预先建立一个索引表,记录每个数据块在文件中的偏移量和长度。这样就不用从头扫描了。
下面这张图展示了分块索引的核心逻辑:
你看,索引表本质上就是一个结构体数组,每个元素记录块的偏移和大小。这样合并的时候,按索引顺序读取即可,不需要额外标记。
实战中的避坑指南
做文件合并分割,有几个坑我踩过,你注意避开:
- 文件句柄泄漏:循环中打开文件,一旦出错直接
return,之前打开的文件就没关。我习惯用goto统一收尾,或者把每个文件操作封装成函数。 - 缓冲区溢出:文件名拼接时,一定要检查长度。我见过有人用
sprintf拼路径,结果文件名超长把栈给毁了。 - 跨平台换行符:前面提过,二进制模式 vs 文本模式。在Linux上无所谓,Windows上必须用
"wb"和"rb"。 - 磁盘空间不足:分割大文件前,先检查剩余空间。可以用
statvfs(Linux)或GetDiskFreeSpace(Windows)提前判断。
我的习惯:在嵌入式设备上做文件分割时,我会在每块文件头部加一个4字节的魔数(Magic Number)和2字节的校验和。合并时先校验魔数和校验和,能有效防止数据错乱。这个习惯帮我避免过好几次固件升级变砖的事故。
好了,文件合并与分割的核心内容就这些。说白了就是:缓冲区要小、循环要稳、模式要对、异常要处理。你把这些原则记牢了,不管多大的文件都能从容应对。
公众号:蓝海资料掘金营,微信deep3321