21、多文件合并与分割:文件合并工具实现、文件分割算法、大文件处理策略

做嵌入式开发这些年,我处理过不少文件操作的需求。说实话,文件合并与分割这个主题,看着简单,真上手了坑不少。你想想看,一个日志文件动不动几百兆,或者你要把固件包拆成小块通过串口发送——这时候,合并与分割的基本功就派上用场了。

我个人习惯把这类操作分成三个层次:小文件随便玩、大文件要讲究、超大文件得用策略。今天咱们就一层层聊透。

文件合并工具的实现

文件合并,说白了就是把多个文件的内容按顺序拼成一个文件。我在项目中遇到过最典型的场景:设备采集了一天数据,生成了几十个小的CSV文件,最后需要合并成一个做分析。

核心思路其实就一句话:逐个打开源文件,读一块写一块,直到读完。别小看这个“读一块写一块”,很多人一上来就用 fread 把整个文件读到内存,小文件还行,大文件直接崩。

合并工具的核心流程:

  1. 获取所有待合并文件的路径列表
  2. 以二进制追加模式打开目标文件
  3. 对每个源文件:以二进制读模式打开,循环读取缓冲区大小数据,写入目标文件
  4. 关闭所有文件句柄

这里我贴一段我常用的合并函数,你感受一下:

#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。这时候,上面那种一次性分配缓冲区的方式就不行了。

我总结了几条实战策略:

  • 流式处理:永远不要一次性把整个文件读进内存。用固定大小的缓冲区,边读边写。这是最核心的原则。
  • 使用 fseekftell 获取文件大小:但要注意,ftell 返回的是 long 类型,对于超过2GB的文件会溢出。在64位系统上可以用 fseekoftello
  • 分块索引:如果需要对大文件随机访问,可以预先建立一个索引表,记录每个数据块在文件中的偏移量和长度。这样就不用从头扫描了。

下面这张图展示了分块索引的核心逻辑:

大文件分块索引策略 原始大文件(例如:1GB 固件包) 块1 (256MB) 块2 (256MB) 块3 (256MB) 块4 (232MB) 索引表(记录每个块的偏移和大小) 块号 偏移量 大小 1 0 268435456 2 268435456 268435456 3 536870912 268435456

你看,索引表本质上就是一个结构体数组,每个元素记录块的偏移和大小。这样合并的时候,按索引顺序读取即可,不需要额外标记。

实战中的避坑指南

做文件合并分割,有几个坑我踩过,你注意避开:

  • 文件句柄泄漏:循环中打开文件,一旦出错直接 return,之前打开的文件就没关。我习惯用 goto 统一收尾,或者把每个文件操作封装成函数。
  • 缓冲区溢出:文件名拼接时,一定要检查长度。我见过有人用 sprintf 拼路径,结果文件名超长把栈给毁了。
  • 跨平台换行符:前面提过,二进制模式 vs 文本模式。在Linux上无所谓,Windows上必须用 "wb""rb"
  • 磁盘空间不足:分割大文件前,先检查剩余空间。可以用 statvfs(Linux)或 GetDiskFreeSpace(Windows)提前判断。

我的习惯:在嵌入式设备上做文件分割时,我会在每块文件头部加一个4字节的魔数(Magic Number)和2字节的校验和。合并时先校验魔数和校验和,能有效防止数据错乱。这个习惯帮我避免过好几次固件升级变砖的事故。

好了,文件合并与分割的核心内容就这些。说白了就是:缓冲区要小、循环要稳、模式要对、异常要处理。你把这些原则记牢了,不管多大的文件都能从容应对。


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