【问题标题】:Reading from a CSV file and separating the fields to store in a struct in C从 CSV 文件中读取并分离字段以存储在 C 中的结构中
【发布时间】:2020-12-13 01:11:32
【问题描述】:

我正在尝试从 CSV 文件中读取数据并将每个字段存储到结构内的变量中。我正在使用 fgets 和 strtok 来分隔每个字段。但是,我无法处理字段内包含逗号的特殊字段。

typedef struct {
    char name[20+1];
    char surname[20+1];
    char uniqueId[10+1];
    char address[150+1];
} employee_t;

void readFile(FILE *fp, employee_t *employees[]){
    int i=0;
    char buffer[205];
    char *tmp;
    
    while (fgets(buffer,205,fp) != NULL) {
        employee_t *new = (employee_t *)malloc(sizeof(*new));
        
        tmp = strtok(buffer,",");
        strcpy(new->name,tmp);
        
        tmp = strtok(buffer,",");
        strcpy(new->surname,tmp);
        
        tmp = strtok(buffer,",");
        strcpy(new->uniqueId,tmp);

        tmp = strtok(buffer,",");
        strcpy(new->address,tmp);

        employees[i++] = new;
        free(new);
    }
}

输入如下:

Jim,Hunter,9239234245,"8/1 Hill Street, New Hampshire"
Jay,Rooney,92364434245,"122 McKay Street, Old Town"
Ray,Bundy,923912345,NOT SPECIFIED

我尝试使用此代码打印令牌,结果如下:

Jim 
Hunter 
9239234245
"8/1 Hill Street
 New Hampshire"

我不确定如何处理地址字段,因为其中一些可能包含逗号。我尝试逐个字符地阅读,但不确定如何使用单个循环将字符串插入结构中。有人可以帮我解决这个问题吗?

【问题讨论】:

  • 没有直接关系,但这很糟糕:employees[i++] = new; free(new); 这将在数组中留下无效的指针。当内存仍在使用时,不要在那里调用free。
  • 至于实际问题,一种方法是删除最后一个 strtok 并更改 strcpy 以复制第三个逗号之后的所有内容:strcpy(new->address,tmp+strlen(tmp)+1)。当然,应该添加错误检查以确保 src 地址在初始字符串内。

标签: c string struct fgets strtok


【解决方案1】:

strcspn 可用于查找双引号或双引号加逗号。
原始字符串未修改,因此可以使用字符串文字。
双引号的位置并不重要。他们可以在任何领域。

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

int main( void) {

    char *string[] = {
        "Jim,Hunter,9239234245,\"8/1 Hill Street, New Hampshire\""
        , "Jay,Rooney,92364434245,\"122 McKay Street, Old Town\""
        , "Ray,Bundy,923912345,NOT SPECIFIED"
        , "Ray,Bundy,\" double quote here\",NOT SPECIFIED"
    };

    for ( int each = 0; each < 4; ++each) {
        char *token = string[each];
        char *p = string[each];

        while ( *p) {
            if ( '\"' == *p) {//at a double quote
                p += strcspn ( p + 1, "\"");//advance to next double quote
                p += 2;//to include the opening and closing double quotes
            }
            else {
                p += strcspn ( p, ",\"");//advance to a comma or double quote
            }
            int span = ( int)( p - token);
            if ( span) {
                printf ( "token:%.*s\n", span, token);//print span characters

                //copy to another array
            }
            if ( *p) {//not at terminating zero
                ++p;//do not skip consecutive delimiters

                token = p;//start of next token
            }
        }
    }
    return 0;
}

编辑:复制到变量
计数器可用于在处理字段时跟踪它们。

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

#define SIZENAME 21
#define SIZEID 11
#define SIZEADDR 151

typedef struct {
    char name[SIZENAME];
    char surname[SIZENAME];
    char uniqueId[SIZEID];
    char address[SIZEADDR];
} employee_t;

int main( void) {

    char *string[] = {
        "Jim,Hunter,9239234245,\"8/1 Hill Street, New Hampshire\""
        , "Jay,Rooney,92364434245,\"122 McKay Street, Old Town\""
        , "Ray,Bundy,923912345,NOT SPECIFIED"
        , "Ray,Bundy,\"quote\",NOT SPECIFIED"
    };
    employee_t *employees = malloc ( sizeof *employees * 4);
    if ( ! employees) {
        fprintf ( stderr, "problem malloc\n");
        return 1;
    }

    for ( int each = 0; each < 4; ++each) {
        char *token = string[each];
        char *p = string[each];
        int field = 0;

        while ( *p) {
            if ( '\"' == *p) {
                p += strcspn ( p + 1, "\"");//advance to a delimiter
                p += 2;//to include the opening and closing double quotes
            }
            else {
                p += strcspn ( p, ",\"");//advance to a delimiter
            }
            int span = ( int)( p - token);
            if ( span) {
                ++field;
                if ( 1 == field) {
                    if ( span < SIZENAME) {
                        strncpy ( employees[each].name, token, span);
                        employees[each].name[span] = 0;
                        printf ( "copied:%s\n", employees[each].name);//print span characters
                    }
                }
                if ( 2 == field) {
                    if ( span < SIZENAME) {
                        strncpy ( employees[each].surname, token, span);
                        employees[each].surname[span] = 0;
                        printf ( "copied:%s\n", employees[each].surname);//print span characters
                    }
                }
                if ( 3 == field) {
                    if ( span < SIZEID) {
                        strncpy ( employees[each].uniqueId, token, span);
                        employees[each].uniqueId[span] = 0;
                        printf ( "copied:%s\n", employees[each].uniqueId);//print span characters
                    }
                }
                if ( 4 == field) {
                    if ( span < SIZEADDR) {
                        strncpy ( employees[each].address, token, span);
                        employees[each].address[span] = 0;
                        printf ( "copied:%s\n", employees[each].address);//print span characters
                    }
                }
            }
            if ( *p) {//not at terminating zero
                ++p;//do not skip consceutive delimiters

                token = p;//start of next token
            }
        }
    }
    free ( employees);
    return 0;
}

【讨论】:

  • 如何在此处的变量中插入标记?
  • 我明白了,但是下一个插入呢?像插入到 new->surname 和其他人时一样?
  • 我认为,如果有两个引号并排,则意味着这不是最后一个引号。
  • 我的错,我只是交叉检查了 csv 文件,请忽略第一个示例。
  • 是的,会的!如果需要,我可以稍后格式化字符串
【解决方案2】:

在我看来,这类问题需要一个“适当的”标记器,可能基于有限状态机 (FSM)。在这种情况下,您将逐个字符地扫描输入字符串,将每个字符分配给一个类。分词器将从特定状态开始,根据读取的字符类别,它可能保持相同状态,或移动到新状态。也就是说,状态转换是由当前状态和所考虑的角色的组合控制的。

例如,如果您在开始状态下读取双引号,则会转换到“在带引号的字符串中”状态。在那种状态下,逗号不会导致转换到新状态——它只会被添加到您正在构建的令牌中。在任何其他状态下,逗号都具有特殊的意义,表示令牌的结束。您必须弄清楚何时需要在标记之间吞下额外的空格,是否有一些“转义”允许在其他标记中使用双引号,是否可以转义行尾以使更长的行,等等。

重要的一点是,如果您实现这是一个 FSM(或另一个真正的分词器),您实际上可以考虑所有这些事情,并根据需要实现它们。如果您使用 strtok() 和字符串搜索的临时应用程序,则不能——无论如何,不​​能以一种优雅、可维护的方式。

如果有一天,您最终需要使用宽字符来完成整个工作,这很容易 - 只需将输入转换为宽字符并一次迭代一个宽字符(而不是字节)。

使用状态转换图很容易记录 FSM 解析器的行为 - 至少比尝试通过在文本中记录代码来解释它更容易。

我的经验是,有人第一次实现 FSM 标记器,这太可怕了。在那之后,这很容易。当您知道方法时,您可以使用相同的技术来解析更复杂的输入。

【讨论】:

    猜你喜欢
    • 2013-07-22
    • 1970-01-01
    • 2016-10-30
    • 2021-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-05
    • 1970-01-01
    相关资源
    最近更新 更多