【问题标题】:C- reading a multi-line file with fgets or sscanf?C- 使用 fgets 或 sscanf 读取多行文件?
【发布时间】:2020-06-11 17:08:07
【问题描述】:

比如说我有一个文本文件

person1
25
500
male
person2
..
..
..
person3
..

不管有多少人,我想将文件的 4 行读入文件中每个人的结构。

我该怎么做?我试过只使用多个 fget,但我不知道如何在一次读取四行的同时循环到文件末尾

谢谢

【问题讨论】:

  • 请你展示你的尝试。您将能够从fgets 的返回值判断是否已到达文件末尾。
  • 这是唯一不使用fgets()/sscanf() 而是使用scanf() 的地方。 scanf() 将跳过空格。所以你可以简单地做if (scanf ("%[^\n]%d%d%[^\n]", ... vars ...) == 4) { /* store in struct */ }。您的阅读将是脆弱的,并且依赖于 4 行重复保持 - 但它提供了对多行的简单阅读。 (您还应该在每次字符串转换之前包含一个 field-width 修饰符以保护您的存储范围,例如 "%63[^\n]" 将读取限制为 63 个字符('\0' 的 +1)对于 64 -char 数组用作存储)

标签: c file-io


【解决方案1】:

继续上面的评论,当您在数据文件中有一组固定的重复行需要读入struct 时,这是您应该考虑scanf()/fscanf() 而不是推荐的唯一例外之一fgets()/sscanf() 每行。

为什么?

scanf() 是一个格式化输入函数(相比之下fgets() 是一个面向行的输入函数)。如果您有跨多行的格式化输入,scanf()/fscanf() 忽略 whitespace'\n' 字符是 whitespace)并允许您将多行作为单个输入使用(使用精心制作的格式字符串

当使用scanf()/fscanf()将数据读入字符串(或数组)时,你必须使用field-width修饰符来限制读入数组的值的数量,以避免写入超出结尾如果输入超出您的数组边界,您的数组会调用 Undefined Behavior。这适用于您使用scanf()/fscanf()/sscanf()(整个家庭)的任何时候。不使用 field-width 修饰符 来读取数组数据并不比使用 gets() 好。

那么如何制作你的格式字符串?让我们看一个具有 4 个成员的示例结构,类似于您在问题中显示的内容,例如

...
#define MAXG 8      /* if you need a constant, #define one (or more) */
#define MAXP 32
#define MAXN 128

typedef struct {    /* struct with typedef */
    char name[MAXN], gender[MAXG];
    int iq, weight;
} person;
...

如果您的数据如图所示,name 的声明为 128 字符,gender 的声明为 8 字符,其余两个成员为 int 类型,您可以执行类似的操作以下:

    int rtn;                                /* fscanf return */
    size_t n = 0;                           /* number of struct filled */
    person ppl[MAXP] = {{ .name = "" }};    /* array of person */
    ...
    while (n < MAXP &&  /* protect struct array bound, and each array bound below */
            (rtn = fscanf (fp, " %127[^\n]%d%d %7[^\n]", /* validate each read */
                    ppl[n].name, &ppl[n].iq, &ppl[n].weight, ppl[n].gender)) == 4)
        n++;            /* increment array index */

具体看格式字符串,你有:

    " %127[^\n]%d%d %7[^\n]"

其中" %127[^\n]",凭借前导' ',使用任何前导空格,然后最多读取127 个字符(您不能使用变量或宏来指定字段宽度 ),字符是行中不是'\n' 字符的任何字符(允许您将空格作为名称的一部分读取,例如"Mickey Mouse")。

注意"%[...] 是一个字符串转换,它将读取字符列表[...] 中的任何字符作为字符串。使用抑扬符'^' 作为列表的第一个字符否定匹配导致"%[^\n]" 将不包括'\n' 的所有字符读入字符串。

" %[^\n]" 之前的空格是必需的,因为 "%[...]""%c" 是唯一不消耗 前导空格转换说明符,因此您提供通过在格式字符串中的转换之前包含一个空格。 int 的另外两个转换说明符,例如"%d" 将自行消耗前导空格,从而导致总转换:

    " %127[^\n]%d%d %7[^\n]"

总而言之,将:

  • 使用任何前导空格('\n' 来自先前读取的 stdingender 用于数组中的前一个结构);
  • 使用%127[^\n] 将最多127 个字符的行读入name 成员;
  • 将包含第一个整数值的行用%d 读入iq(使用前导空格);
  • %d(同上)将包含第二个整数值的行读入weight
  • ' ' 消耗weight 读取后留下的'\n';最后
  • 使用 %7[^\n] 将最多 7 个字符的行读入 gender 成员(根据需要调整以保留最长的性别字符串)

使用这种方法,您只需调用fscanf() 就可以将4 行输入消耗到数组中的每个结构中。您应该在循环退出时检查rtn,以确保在从文件中读取所有值后循环在EOF 上退出。一个简单的检查将涵盖所需的最低验证,例如

    if (rtn != EOF) /* if loop exited on other than EOF, issue warning */
        fputs ("warning: error in file format or array full.\n", stderr);

注意:您还可以检查n == MAXP是否是由于单独的数组已满导致循环退出的原因)。

总而言之,你可以这样做:

#include <stdio.h>

#define MAXG 8      /* if you need a constant, #define one (or more) */
#define MAXP 32
#define MAXN 128

typedef struct {    /* struct with typedef */
    char name[MAXN], gender[MAXG];
    int iq, weight;
} person;

int main (int argc, char **argv) {

    int rtn;                                /* fscanf return */
    size_t n = 0;                           /* number of struct filled */
    person ppl[MAXP] = {{ .name = "" }};    /* array of person */
    /* 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 (n < MAXP &&  /* protect struct array bound, and each array bound below */
            (rtn = fscanf (fp, " %127[^\n]%d%d %7[^\n]", /* validate each read */
                    ppl[n].name, &ppl[n].iq, &ppl[n].weight, ppl[n].gender)) == 4)
        n++;            /* increment array index */

    if (rtn != EOF) /* if loop exited on other than EOF, issue warning */
        fputs ("warning: error in file format or array full.\n", stderr);

    for (size_t i = 0; i < n; i++)  /* output results */
        printf ("\nname   : %s\niq     : %d\nweight : %d\ngender : %s\n",
                ppl[i].name, ppl[i].iq, ppl[i].weight, ppl[i].gender);

    if (fp != stdin)   /* close file if not stdin */
        fclose (fp);
}

注意:您也可以使用全局enum 来定义您的常量)

输入文件示例

$ cat dat/ppl.txt
person1
25
500
male
person2
128
128
female
Mickey Mouse
56
2
male
Minnie Mouse
96
1
female

使用/输出示例

$ ./bin/readppl dat/ppl.txt

name   : person1
iq     : 25
weight : 500
gender : male

name   : person2
iq     : 128
weight : 128
gender : female

name   : Mickey Mouse
iq     : 56
weight : 2
gender : male

name   : Minnie Mouse
iq     : 96
weight : 1
gender : female

您还可以使用行计数器或多行读取方法使用fgets() 读取每一行,但这更多是关于为工作选择合适的工具。使用fgets() 然后多次调用sscanf() 进行整数值或两次调用strtol() 进行转换没有任何问题,但是对于大型输入文件,1-函数调用fscanf() 相比4-对fgets() 的单独调用加上对sscanf()strtol() 的2 次单独调用以及用于处理行计数器或多缓冲区逻辑的额外逻辑将开始累加。

查看一下,如果您还有其他问题,请告诉我。

【讨论】:

    【解决方案2】:

    一些示例行。我会让你提供程序的其余部分。

    #define MAX 1000
    
      ...
    
      FILE *f;
      char line1[MAX], line2[MAX], line3[MAX], line4[MAX];
    
      ...
    
      while(fgets(line1, MAX, f) != NULL)
        {
          if (fgets(line2, MAX, f) == NULL ||
              fgets(line3, MAX, f) == NULL ||
              fgets(line4, MAX, f) == NULL)
            {
             /* insert code here to handle end of file in unexpected place */
              break;
            }
          /* insert code here to do your sscanf and anything else you want */
        }
    
        ....
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-25
      • 1970-01-01
      • 2016-08-14
      • 1970-01-01
      • 1970-01-01
      • 2015-09-06
      相关资源
      最近更新 更多