24、CSV文件解析实战:读取并解析逗号分隔值文件

CSV文件,说白了就是逗号分隔值文件。你想想看,Excel表格导出来,或者数据库导出来,最常见的就是这种格式。我刚开始学C语言那会儿,觉得这玩意儿简单得很——不就是按逗号切分字符串嘛。结果真上手做项目,才发现坑多着呢。

今天咱们就手把手把这个CSV解析给啃下来。我会把我在实际项目中踩过的坑、总结的经验都抖出来。

CSV文件长什么样?

先看一个典型的CSV文件内容:

姓名,年龄,城市,职业
张三,28,北京,工程师
李四,35,上海,设计师
王五,22,广州,学生

嗯,就是这么简单。每行是一条记录,逗号是字段分隔符。但实际项目中,CSV可没这么乖。我遇到过的情况包括:

  • 字段里包含逗号(比如地址"北京市,朝阳区")
  • 字段里包含换行符
  • 字段用双引号包裹
  • 空字段(比如"张三,,北京")

所以,一个健壮的CSV解析器,必须能处理这些情况。

解析的核心思路

我个人习惯把CSV解析分成三个步骤:

  1. 按行读取——用fgets或fscanf逐行读
  2. 按逗号切分——遍历每个字符,遇到逗号就切一刀
  3. 处理特殊字符——双引号、转义符等

说白了,就是状态机。每读一个字符,判断当前是在普通字段里,还是在双引号字段里,还是在等待下一个字段。

核心原则:不要用strtok!不要用strtok!不要用strtok!

strtok会修改原字符串,而且不能处理空字段。我曾经在项目里吃过这个亏,后来老老实实自己写解析器。

代码实现:一个健壮的CSV解析器

下面这个函数,是我在实际项目中打磨过的版本。它能处理双引号、空字段、字段内逗号等情况。

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

#define MAX_LINE 1024
#define MAX_FIELDS 100

// 解析一行CSV,返回字段个数
int parse_csv_line(char *line, char *fields[], int max_fields) {
    int count = 0;
    char *p = line;
    char *start = line;
    int in_quotes = 0;

    while (*p) {
        if (*p == '"') {
            in_quotes = !in_quotes;  // 切换引号状态
        } else if (*p == ',' && !in_quotes) {
            // 遇到逗号且不在引号内,切分字段
            *p = '\0';
            fields[count] = start;
            count++;
            start = p + 1;
            if (count >= max_fields) break;
        }
        p++;
    }
    // 处理最后一个字段
    fields[count] = start;
    count++;

    // 去掉字段首尾的双引号
    for (int i = 0; i < count; i++) {
        char *f = fields[i];
        int len = strlen(f);
        if (len >= 2 && f[0] == '"' && f[len-1] == '"') {
            f[len-1] = '\0';
            fields[i] = f + 1;
        }
    }

    return count;
}

int main() {
    FILE *fp = fopen("data.csv", "r");
    if (!fp) {
        perror("打开文件失败");
        return 1;
    }

    char line[MAX_LINE];
    char *fields[MAX_FIELDS];
    int line_num = 0;

    while (fgets(line, sizeof(line), fp)) {
        // 去掉换行符
        line[strcspn(line, "\n")] = '\0';
        line_num++;

        int count = parse_csv_line(line, fields, MAX_FIELDS);
        printf("第%d行: %d个字段\n", line_num, count);
        for (int i = 0; i < count; i++) {
            printf("  字段%d: %s\n", i+1, fields[i]);
        }
    }

    fclose(fp);
    return 0;
}

小技巧:如果CSV文件很大(比如几百万行),建议用mmap或者分块读取,别一次性读到内存里。我之前处理一个2GB的CSV,差点把服务器搞崩了。

处理更复杂的情况

上面的代码已经能应付大部分场景了。但如果你遇到下面这些情况,还得再加强:

场景 处理方法
字段内包含双引号(如"他说""你好""") 连续两个双引号转义为一个
字段内包含换行符 需要跨行读取,直到找到匹配的引号
编码问题(UTF-8 BOM头) 读取第一行时检查前三个字节是否为EF BB BF
空行 跳过或返回空字段数组

我曾经在处理一个从Windows导出的CSV时,发现每行末尾都有\r\n,而Linux下只有\n。这个坑让我排查了半天。所以,跨平台解析时,一定要处理\r字符。

知识体系总览

下面这张图,把CSV解析的核心逻辑串起来了:

CSV文件解析核心流程 1. 读取文件 fgets / fread 2. 按行切分 去掉换行符 3. 状态机解析 逐字符判断 状态机内部逻辑 普通字段 引号字段 字段结束 遇到" 遇到" 换行/EOF 遇到, 输出:字段数组

避坑指南

我曾经犯过的错:

  • 直接用fscanf("%s")读CSV——遇到空格就停了,字段根本读不全
  • 用strtok切分——空字段直接跳过,数据对不齐
  • 没处理BOM头——第一行第一个字段前面多了三个不可见字符
  • 假设每行末尾都是\n——Windows的\r\n导致最后一个字段多了\r

嗯,这些坑我都踩过。现在写CSV解析器,我会先问清楚:文件是哪个系统生成的?字段里有没有特殊字符?要不要处理引号?

总结一下

CSV解析看着简单,但要做好、做稳,还是得下点功夫。核心就三点:

  • 逐字符解析,别偷懒用库函数
  • 状态机思维,处理引号和转义
  • 边界情况,空字段、空行、特殊字符都要考虑

你想想看,如果连CSV都解析不好,后面那些数据分析、报表生成的工作根本没法做。所以,把这个基础打扎实了,后面就顺了。


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