C语言读取CSV至结构体数组异常及后续功能选型咨询
CSV解析异常及结构体数组适用性问题
一、CSV解析异常排查与修复
问题现象
尝试读取含3940条数据的CSV文件,使用strtok编写解析代码后,仅读取到1010条数据,且读取起始行为文件的第2931行,而非开头。
代码实现
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char location[100]; char city[100]; int price; int room; int bathroom; int carpark; char type[100]; char furnish[100]; } data; int main() { data house[4000]; FILE *file = fopen("data.csv", "r"); char buffer[1000]; fgets(buffer, 1000, file); int n = 0; while (fgets(buffer, 1000, file)) { char *token = strtok(buffer, ","); strcpy(house[n].location, token); token = strtok(NULL, ","); strcpy(house[n].city, token); token = strtok(NULL, ","); house[n].price = atoi(token); token = strtok(NULL, ","); house[n].room = atoi(token); token = strtok(NULL, ","); house[n].bathroom = atoi(token); token = strtok(NULL, ","); house[n].carpark = atoi(token); token = strtok(NULL, ","); strcpy(house[n].type, token); token = strtok(NULL, "\n"); strcpy(house[n].furnish, token); n++; } fclose(file); for (int i = 0; i < n; i++) { printf("%s,%s,%d,%d,%d,%d,%s,%s\n", house[i].location, house[i].city, house[i].price, house[i].room, house[i].bathroom, house[i].carpark, house[i].type, house[i].furnish); } return 0; }
问题原因分析
- 长行截断导致解析错位:
fgets(buffer, 1000, file)的缓冲区大小为1000,若CSV中某行数据长度超过1000,fgets会截断该行,剩余内容会被当作下一行处理,导致后续所有行的字段解析完全错位,最终触发未定义行为提前终止循环。 - 未处理带引号的CSV字段:标准CSV中,字段若包含逗号、换行符等特殊字符会用引号包裹,
strtok无法识别这种格式,会将引号内的逗号当作分隔符,破坏字段完整性。 - 空指针访问风险:未检查
strtok返回的token是否为NULL,若某行字段数量不足,strcpy或atoi会访问空指针,直接终止程序。 - 最后字段的换行处理不当:用
"\n"作为最后一个字段的分隔符,若行尾存在多余空格或字段本身包含换行(带引号场景),会导致字段内容错误。
修复方案
- 增加文件打开检查:避免文件打开失败后继续执行后续操作:
FILE *file = fopen("data.csv", "r"); if (!file) { perror("Failed to open file"); return 1; } - 处理长行与带引号字段:
- 增大缓冲区至合理大小(如4096),或实现动态读取逻辑;
- 替换
strtok为支持标准CSV格式的解析逻辑,例如手动处理引号包裹的字段:遇到引号时,跳过直到下一个引号,再寻找分隔符。
- 检查
token有效性:每次调用strtok后判断返回值,避免空指针访问:char *token = strtok(buffer, ","); if (!token) break; strcpy(house[n].location, token); // 后续每个token都需添加类似检查 - 修正最后字段的换行处理:用
",\n"作为分隔符,或手动去除字段末尾的换行符:token = strtok(NULL, ",\n"); if (!token) break; // 手动去除换行(可选) size_t len = strlen(token); if (len > 0 && token[len-1] == '\n') token[len-1] = '\0'; strcpy(house[n].furnish, token);
二、结构体数组的适用性及替代方案
当前方案的适用性
结构体数组完全适合实现显示、搜索、排序、导出这些功能:
- 显示:直接遍历数组输出即可;
- 搜索:遍历数组匹配目标条件;
- 排序:使用标准库
qsort函数,自定义比较函数,4000条数据的规模下效率足够; - 导出:遍历数组将数据写入文件。
优化替代方案
如果未来数据量大幅增长(如十万条以上),建议使用动态分配数组替代栈上固定大小数组,避免栈溢出:
// 初始分配4000个结构体的内存 data *house = malloc(4000 * sizeof(data)); if (!house) { perror("Failed to allocate memory"); fclose(file); return 1; } // 后续需要扩容时使用realloc size_t current_size = 4000; if (n >= current_size) { current_size *= 2; house = realloc(house, current_size * sizeof(data)); if (!house) { perror("Failed to reallocate memory"); free(house); fclose(file); return 1; } }
若需要频繁执行插入、删除操作,链表结构会更灵活,但你的需求中排序、搜索依赖随机访问特性,数组的效率远高于链表,因此结构体数组仍是最优选择。
内容的提问来源于stack exchange,提问作者Zain Firdaus
相关产品推荐
相关产品推荐

