【问题标题】:Tokenize string by space and assign multiple of tokenized of them into one string in C用空格标记字符串,并将它们的多个标记化分配到 C 中的一个字符串中
【发布时间】:2021-03-08 23:55:57
【问题描述】:

我在.csv 文件中有一些数据,每一行都属于一个驱动程序。我想要做的是读取 CSV 文件的每一行,然后将其拆分为单词。通常每个部分的一行数据由 ; 彼此分隔。但每行的第一部分是司机的名字和姓氏,可以有不同的格式。我在阅读和拆分时应该遵守的规则是将此部分分成两个部分。如果有三个单词,则应将最后一个单词指定为姓氏,将另外两个单词指定为名字。我使用struct 将每一行的数据存储为驱动程序,下面是我编写的用于读取所有文件行并为每一行创建一个结构对象的代码。

void convert_file_data_to_struct(struct driver *driversList, int *driverCounter) {

    FILE *mfile;

    int errCnt;

    char line[100];

    char *tok = NULL;

    mfile = fopen(FILE_NAME, "r");

    if(!mfile) {

        printf("File wasn't read properly!\n");

        exit_program();

    }

    while(feof(mfile) == 0) {

        errCnt = 0;

        fgets(line, 99, mfile);

        int i, count;

        for (i=0, count=0; line[i]; i++)

            count += (line[i] == ' ');

        tok = strtok(line, " ");

        if (count > 1) {

            int spaceCounter = 0;

            char name[50];

            while (tok != NULL) {   

                if(spaceCounter <= count){

                    strcat(name, tok);

                    strcat(name, " ");

                    spaceCounter++;

                }

                if (spaceCounter <= count) {

                    tok = strtok(NULL, " "); 

                }  

            }            

        }

        strcpy((driversList + *driverCounter)->firstName, tok);

        while(tok != NULL) {

            ((errCnt+1) > 10) ? exit_program(): errCnt++;

            tok = strtok(NULL, ";");

            if(errCnt == 1) {

                strcpy((driversList + *driverCounter)->lastName, tok);

            } else {

                if(errCnt == 2) {

                    if(tok[0] == 'm' || tok[0] == 'f') {

                        strcpy((driversList + *driverCounter)->gender, tok);

                    } else {

                        exit_program();;

                    }

                } else if (errCnt == 3) {

                    if(atoi(tok)) {

                        (driversList + *driverCounter)->birthYear = atoi(tok);

                    }

                    else {

                        exit_program();

                    }

                }else if (errCnt == 4) {

                    strcpy((driversList + *driverCounter)->automobil, tok);

                } else if (errCnt == 5) {

                    if(atof(tok)) {

                        (driversList + *driverCounter)->firstRecord = atof(tok);

                    } else {

                        exit_program();

                    }

                } else if (errCnt == 6) {

                    if(atof(tok)) {

                        (driversList + *driverCounter)->secondRecord = atof(tok);

                    } else {

                        exit_program();

                    }

                } else if (errCnt == 7) {

                    if(atof(tok)) {

                        (driversList + *driverCounter)->thirdRecord = atof(tok);

                    } else {

                        exit_program();

                    }

                }else if (errCnt == 8){

                    if(atof(tok)) {

                        (driversList + *driverCounter)->fourthRecord = atof(tok);

                    } else {

                        exit_program();

                    }

                } else if (errCnt == 9) {

                    if(atof(tok)) {

                        (driversList + *driverCounter)->fifthRecord = atof(tok);

                    } else {

                        exit_program();

                    }

                }

            }

        }

        (*driverCounter)++;

    }

    fclose(mfile);

}

下面是csv文件的每一行数据。

 Francoise Test Hardy-Test;f;1982;ferrari;72.643;71.987;70.221;79.002;73.737

strtok() 函数的行为真的很模棱两可,我无法按照我之前提到的方式获取人的名字。我想知道C 中是否还有其他方法可以拆分字符串,并且该方法会返回基于索引的拆分字符串。

【问题讨论】:

标签: c string split


【解决方案1】:

好吧,让我们看看能不能解决这个问题。从评论继续,您现在已经阅读并理解使用while(feof(mfile) == 0) 将失败,因为尝试读取比文件中存在的多行的行。始终通过返回读取函数本身来控制您的读取循环。 (注意:在您编辑之前,我将您的结构称为 car_tp(汽车类型的缩写)——所以我们将使用它)例如,如果您有一个 car_tp 数组,其中最大为 MAXN 元素,你可以这样做:

#define MAXC   1024     /* if you need a constant, #define one (or more) */
#define MAXN     64
#define MAXY      8
#define DELIM ";\n"
...
int main (int argc, char **argv) {
    
    char buf[MAXC];                                     /* buffer to hold each line */
    car_tp cars[MAXN] = {{ .first = "" }};              /* array of MAXN struct */
    size_t ncars = 0;
    ...
    /* while array not full, read each line */
    while (ncars < MAXN && fgets (buf, MAXC, fp)) {
        ...

这会将每一行读入buf,同时保护您的结构数组边界。

现在开始标记化。你有基本的想法。您将拥有一个令牌计数器,并且您将跟踪您正在处理的令牌并将该令牌分配给适当的结构成员。唯一真正的挑战是您的第一个标记,您可以在其中有多个空格分隔的名称标记,您只想将姓氏标记分配给姓氏,其余的分配给名字。

而不是计算空格(这是有效的,但尴尬且容易出错,除非您还处理多个空格并将其视为单个分隔符),只需使用 strrchr() 查找标记中的最后一个空格。您知道下一个字符将指向姓氏的开头,请将其保存到您的姓氏成员。然后朝着开始循环,直到找到下一个非空格。你知道这将是名字的结尾。因此,您需要做的就是从令牌的开头复制到该点到您的名字,然后 nul-terminate 复制名字。

我更喜欢使用switch() 语句来确定在给定一系列值的情况下要采用多个分支中的哪一个,而不是if ... else if ... else 的长菊花链等等。你可以这样做:

    /* while array not full, read each line */
    while (ncars < MAXN && fgets (buf, MAXC, fp)) {
        size_t ntok = 0;    /* token count */
        /* tokenize line */
        for (char *p = strtok (buf, DELIM); p; p = strtok (NULL, DELIM)) {
            switch (ntok) { /* switch to fill struct members */
            case 0: {       /* handle name (note brace enclosed block) */
                char *endp = strrchr (p, ' ');          /* find last space in token */
                if (!endp) {    /* validate */
                    fputs ("error: invalid field1 format.\n", stderr);
                    continue;
                }
                if (strlen (endp+1) >= MAXN) {          /* check last name fits */
                    fputs ("error: last too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].last, endp+1);      /* copy last name to struct */
                /* loop toward start, position endp to space after first name */
                while (endp != p && isspace (*(endp-1)))
                    endp--;
                if (endp - p >= MAXN) {                 /* check first name fits */
                    fputs ("error: first too long.\n", stderr);
                    continue;
                }
                memcpy (cars[ncars].first, p, endp - p);    /* copy first to struct */
                cars[ncars].first[endp - p] = 0;            /* nul-terminate */
                break;
            }

(注意:在循环回到token的开头之后,你应该检查endp == p来捕捉整个名字只有1部分的情况并处理错误——这是留给你的)

这完全符合上面段落中的描述,并添加了对名称每个部分长度的验证,以确保它适合MAXN 可用字符数。根据我对您的结构的猜测,其余的字符串值可以处理为:

            case 1:     /* handle model */
                if (strlen (p) >= MAXN) {               /* check model fits */
                    fputs ("error: model too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].model, p);          /* copy to struct */
                break;
            ...
            case 3:
                if (strlen (p) >= MAXN) {               /* check manufacturer fits */
                    fputs ("error: manufacturer too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].manuf, p);          /* copy to struct */
                break;

对于您的浮点值,您永远不想在实践中使用atof()。它有零错误报告,并且很乐意为您提供的任何非数字字符串返回0,例如atof ("my cow")。在实践中始终使用 strtof()strtod() 验证转换的两个数字,并且未设置 errno 表示转换期间发生下溢/溢出。由于您将进行多次转换,因此创建一个简单的函数来处理验证可以节省为每个转换的浮点值重复代码。要转换为double,您可以这样做:

/* convert string 'nptr' to double stored at `dbl` with error-check,
 * returns 1 on success, 0 otherwise.
 */ 
int todouble (const char *nptr, double *dbl)
{
    char *endptr = (char*)nptr;         /* endptr to use with strtod() */
    errno = 0;                          /* zero errno */
    
    *dbl = strtod (nptr, &endptr);      /* convert to double, assign */
    if (endptr == nptr) {   /* validate digits converted */
        fputs ("error: todouble() no digits converted.\n", stderr);
        return 0;       /* return failure */
    }
    else if (errno) {   /* validate no under/overflow */
        fputs ("error: todouble() under/overflow detected.\n", stderr);
        return 0;       /* return failure */
    }
    
    return 1;   /* return success */
}

注意:提供最小验证以确保有效转换)

然后要转换和分配每个浮点值,您可以这样做:

            case 4: {       /* handle double values (note brace enclosed block) */
                double d;
                if (!todouble (p, &d))                  /* attempt conversion */
                    continue;
                cars[ncars].d1 = d;                     /* assign on success */
                break;
            }

(注意: 为了在case 语句中声明变量,您必须通过为该case 提供包含代码的大括号来创建单独的代码块,因为switch() @ 987654346@ 语句本身并不是一个单独的程序块(范围))

这基本上就是您所需要的,除了跟踪结构数组中元素数量的计数器。举一个简短的例子,你可以这样做:

#include <stdio.h>
#include <stdlib.h>     /* for strtod() */
#include <string.h>     /* for strtok(), strrchr(), strlen() */
#include <ctype.h>      /* for isspace() */
#include <errno.h>      /* for errno */

#define MAXC   1024     /* if you need a constant, #define one (or more) */
#define MAXN     64
#define MAXY      8
#define DELIM ";\n"

typedef struct {        /* struct to fit data */
    char first[MAXN], last[MAXN], model[MAXN], year[MAXY], manuf[MAXN];
    double d1, d2, d3, d4, d5;
} car_tp;

/* convert string 'nptr' to double stored at `dbl` with error-check,
 * returns 1 on success, 0 otherwise.
 */ 
int todouble (const char *nptr, double *dbl)
{
    char *endptr = (char*)nptr;         /* endptr to use with strtod() */
    errno = 0;                          /* zero errno */
    
    *dbl = strtod (nptr, &endptr);      /* convert to double, assign */
    if (endptr == nptr) {   /* validate digits converted */
        fputs ("error: todouble() no digits converted.\n", stderr);
        return 0;       /* return failure */
    }
    else if (errno) {   /* validate no under/overflow */
        fputs ("error: todouble() under/overflow detected.\n", stderr);
        return 0;       /* return failure */
    }
    
    return 1;   /* return success */
}

/* simple print car function taking pointer to struct */
void prncar (car_tp *car)
{
    printf ("\nfirst : %s\n"
            "last  : %s\n"
            "model : %s\n"
            "year  : %s\n"
            "manuf : %s\n"
            "d1    : %lf\n"
            "d2    : %lf\n"
            "d3    : %lf\n"
            "d4    : %lf\n"
            "d5    : %lf\n", 
            car->first, car->last, car->model, car->year, car->manuf,
            car->d1, car->d2, car->d3, car->d4, car->d5);
}

int main (int argc, char **argv) {
    
    char buf[MAXC];                                     /* buffer to hold each line */
    car_tp cars[MAXN] = {{ .first = "" }};              /* array of MAXN struct */
    size_t ncars = 0;
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
    
    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }
    
    /* while array not full, read each line */
    while (ncars < MAXN && fgets (buf, MAXC, fp)) {
        size_t ntok = 0;    /* token count */
        /* tokenize line */
        for (char *p = strtok (buf, DELIM); p; p = strtok (NULL, DELIM)) {
            switch (ntok) { /* switch to fill struct members */
            case 0: {       /* handle name (note brace enclosed block) */
                char *endp = strrchr (p, ' ');          /* find last space in token */
                if (!endp) {    /* validate */
                    fputs ("error: invalid field1 format.\n", stderr);
                    continue;
                }
                if (strlen (endp+1) >= MAXN) {          /* check last name fits */
                    fputs ("error: last too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].last, endp+1);      /* copy last name to struct */
                /* loop toward start, position endp to space after first name */
                while (endp != p && isspace (*(endp-1)))
                    endp--;
                if (endp - p >= MAXN) {                 /* check first name fits */
                    fputs ("error: first too long.\n", stderr);
                    continue;
                }
                memcpy (cars[ncars].first, p, endp - p);    /* copy first to struct */
                cars[ncars].first[endp - p] = 0;            /* nul-terminate */
                break;
            }
            case 1:     /* handle model */
                if (strlen (p) >= MAXN) {               /* check model fits */
                    fputs ("error: model too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].model, p);          /* copy to struct */
                break;
            case 2:
                if (strlen (p) >= MAXY) {               /* check year fits */
                    fputs ("error: year too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].year, p);           /* copy to struct */
                break;
            case 3:
                if (strlen (p) >= MAXN) {               /* check manufacturer fits */
                    fputs ("error: manufacturer too long.\n", stderr);
                    continue;
                }
                strcpy (cars[ncars].manuf, p);          /* copy to struct */
                break;
            case 4: {       /* handle double values (note brace enclosed block) */
                double d;
                if (!todouble (p, &d))                  /* attempt conversion */
                    continue;
                cars[ncars].d1 = d;                     /* assign on success */
                break;
            }
            case 5: {                           /* ditto */
                double d;
                if (!todouble (p, &d))
                    continue;
                cars[ncars].d2 = d;
                break;
            }
            case 6: {                           /* ditto */
                double d;
                if (!todouble (p, &d))
                    continue;
                cars[ncars].d3 = d;
                break;
            }
            case 7: {                           /* ditto */
                double d;
                if (!todouble (p, &d))
                    continue;
                cars[ncars].d4 = d;
                break;
            }
            case 8: {                           /* ditto */
                double d;
                if (!todouble (p, &d))
                    continue;
                cars[ncars].d5 = d;
                break;
            }   /* provide default case to notify on too many tokens */
            default: fputs ("error: tokens exceed struct members.\n", stderr);
                break;
            }
            ntok += 1;                                  /* update token count */
        }
        ncars += 1;                                     /* update car count */
    }
    
    if (fp != stdin)   /* close file if not stdin */
        fclose (fp);
    
    for (size_t i = 0; i < ncars; i++)                  /* output results */
        prncar (&cars[i]);
    
    return 0;
}

输入文件示例

为了测试代码,我从您的示例输入创建了一个文件,添加了另一条记录来练习简单的名字/姓氏大小写,例如

$ cat dat/ferrari.txt
Francoise Test Hardy-Test;f;1982;ferrari;72.643;71.987;70.221;79.002;73.737
Other Guys;Dino;1980;ferrari;72.643;71.987;70.221;79.002;73.737

使用/输出示例

包含prncar() 函数后,它将产生以下输出:

$ ./bin/cartok dat/ferrari.txt

first : Francoise Test
last  : Hardy-Test
model : f
year  : 1982
manuf : ferrari
d1    : 72.643000
d2    : 71.987000
d3    : 70.221000
d4    : 79.002000
d5    : 73.737000

first : Other
last  : Guys
model : Dino
year  : 1980
manuf : ferrari
d1    : 72.643000
d2    : 71.987000
d3    : 70.221000
d4    : 79.002000
d5    : 73.737000

有很多东西需要消化,所以花点时间理解每一行代码。如果您有任何问题,请在下方发表评论,我很乐意为您提供进一步帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-02
    • 1970-01-01
    • 2011-02-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多