使用fscanf处理含SQL NULL值的制表符分隔CSV文件的问题
处理带空字段的制表符分隔CSV文件
你的问题核心是fscanf不适合解析带空字段的分隔文件——它的格式符(比如%u、%s)会自动跳过所有空白字符(包括制表符、空格),遇到SQL NULL对应的空字段(连续制表符)时,会直接跳过这个位置,把下一个非空字段错误地填充到当前结构体成员中,导致整个记录错位。
下面是更可靠的解决方案,核心思路是先整行读取,再逐字段分割处理,精准控制每个空字段的解析逻辑:
步骤1:整行读取+逐字段解析的实现
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> struct Path { unsigned int objectId, parentObjectId, changeId, prevId, nextId; unsigned short regionCode, areaCode, cityCode, placeCode, planCode, streetCode; char updateDate[10], startDate[10], endDate[10]; bool isActive; char path[300]; }; // 辅助函数:提取行中指定索引的制表符分隔字段 char* get_tab_field(char* line, int field_index) { static char field_buf[1024]; int current_field = 0; char* ptr = line; while (*ptr != '\0') { // 遇到制表符,判断是否是目标空字段 if (*ptr == '\t') { if (current_field == field_index) { field_buf[0] = '\0'; return field_buf; } current_field++; ptr++; continue; } // 定位当前字段的结束位置 char* field_end = ptr; while (*field_end != '\0' && *field_end != '\t') { field_end++; } // 匹配到目标字段,复制内容并返回 if (current_field == field_index) { int field_len = field_end - ptr; strncpy(field_buf, ptr, field_len); field_buf[field_len] = '\0'; return field_buf; } // 跳到下一个字段起始位置 current_field++; ptr = field_end + 1; } // 字段索引超出实际数量,返回空字符串 field_buf[0] = '\0'; return field_buf; } int main() { FILE* ah = fopen("your_file.csv", "r"); if (!ah) { perror("Failed to open file"); return 1; } char line[2048]; // 根据实际行长度调整,或动态分配内存 while (fgets(line, sizeof(line), ah) != NULL) { // 移除换行符(兼容Windows/Linux换行格式) line[strcspn(line, "\n\r")] = '\0'; // 初始化结构体,空字段默认用0/空字符串 struct Path path = {0}; char* field; // 逐个处理每个字段 // 1. objectId field = get_tab_field(line, 0); if (strlen(field) > 0) sscanf(field, "%u", &path.objectId); // 2. parentObjectId field = get_tab_field(line, 1); if (strlen(field) > 0) sscanf(field, "%u", &path.parentObjectId); // 3. changeId field = get_tab_field(line, 2); if (strlen(field) > 0) sscanf(field, "%u", &path.changeId); // 4. regionCode(先转成unsigned int再强转,避免溢出) field = get_tab_field(line, 3); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.regionCode = (unsigned short)val; } // 5. areaCode field = get_tab_field(line, 4); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.areaCode = (unsigned short)val; } // 6. cityCode field = get_tab_field(line, 5); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.cityCode = (unsigned short)val; } // 7. placeCode field = get_tab_field(line, 6); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.placeCode = (unsigned short)val; } // 8. planCode field = get_tab_field(line, 7); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.planCode = (unsigned short)val; } // 9. streetCode field = get_tab_field(line, 8); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.streetCode = (unsigned short)val; } // 10. prevId field = get_tab_field(line, 9); if (strlen(field) > 0) sscanf(field, "%u", &path.prevId); // 11. nextId field = get_tab_field(line, 10); if (strlen(field) > 0) sscanf(field, "%u", &path.nextId); // 12. updateDate(限制长度,避免缓冲区溢出) field = get_tab_field(line, 11); if (strlen(field) > 0) { strncpy(path.updateDate, field, sizeof(path.updateDate)-1); path.updateDate[sizeof(path.updateDate)-1] = '\0'; } // 13. startDate field = get_tab_field(line, 12); if (strlen(field) > 0) { strncpy(path.startDate, field, sizeof(path.startDate)-1); path.startDate[sizeof(path.startDate)-1] = '\0'; } // 14. endDate field = get_tab_field(line, 13); if (strlen(field) > 0) { strncpy(path.endDate, field, sizeof(path.endDate)-1); path.endDate[sizeof(path.endDate)-1] = '\0'; } // 15. isActive(空字段默认设为false) field = get_tab_field(line, 14); if (strlen(field) > 0) { unsigned int val; sscanf(field, "%u", &val); path.isActive = (val != 0); } // 16. path field = get_tab_field(line, 15); if (strlen(field) > 0) { strncpy(path.path, field, sizeof(path.path)-1); path.path[sizeof(path.path)-1] = '\0'; } // 这里添加你的业务逻辑,比如将path存入链表或进行后续处理 // ... } fclose(ah); return 0; }
关键细节说明
- 抛弃
feof循环:原代码的while (!feof(ah))存在逻辑错误——feof只有在读取到文件末尾后才会置位,会导致最后一行被重复处理。改用fgets作为循环条件更可靠。 - 空字段精准处理:每个字段先判断是否为空,为空则保持结构体初始化的默认值(0或空字符串),非空再用
sscanf转换类型,彻底避免字段错位。 - 类型安全与缓冲区安全:
- 对于短整型字段,先读取到
unsigned int再强制转换,避免直接写入短整型可能的溢出问题。 - 字符串字段用
strncpy并手动添加终止符,防止输入过长导致缓冲区溢出。
- 对于短整型字段,先读取到
- 字段提取函数:
get_tab_field能完美处理连续制表符(空字段),即使字段索引超出实际内容也会返回空字符串,避免越界访问。
进阶方案:使用成熟CSV库
如果需要处理更复杂的CSV场景(比如字段包含制表符、换行符,需要转义),可以考虑使用C语言的成熟CSV库(如libcsv),但对于你的简单场景,手动分割已经足够优雅可靠。
内容的提问来源于stack exchange,提问作者Александр Попов
相关产品推荐
相关产品推荐

