【问题标题】:Reading multiple lines with different data types in C在C中读取具有不同数据类型的多行
【发布时间】:2016-05-18 20:13:14
【问题描述】:

我有一个很奇怪的问题,我正在尝试用 C 读取一个 .txt 文件,数据结构如下: %s %s %d %d 因为我必须阅读字符串all the way to \n,所以我是这样阅读的:

while(!feof(file)){
        fgets(s[i].title,MAX_TITLE,file);
        fgets(s[i].artist,MAX_ARTIST,file);
        char a[10];
        fgets(a,10,file);
        sscanf(a,"%d %d",&s[i].time.min,&s[i++].time.sec);
    }

但是,我在s.time.min 中读到的very first 整数显示了一个随机的大数。

我现在正在使用 sscanf,因为有几个人遇到了类似的问题,但它没有帮助。

谢谢!

编辑:整数代表时间,它们的组合永远不会超过 5 个字符,包括中间的空格。

【问题讨论】:

标签: c file io fgets


【解决方案1】:

注意,我认为您的帖子是从 3 个不同的行读取值,例如:

%s
%s
%d %d

(主要体现在您使用了fgets,这是一个面向行的输入函数,它读取一行输入(直到并包括'\n' ) 每次调用。) 如果不是这样,则以下内容不适用(并且可以大大简化)

由于您正在将多个值读取到结构数组中的单个元素中,您可能会发现在开始将信息复制到结构成员之前读取每个值并使用临时值验证每个值会更好(并且更健壮)他们自己。这允许您 (1) 验证所有值的读取,以及 (2) 在将成员存储在结构中并增加数组索引之前验证所有必需值的解析或转换。

此外,您需要从titleartist 中删除尾部'\n',以防止嵌入的换行符悬垂在字符串的末尾(这将导致搜索titleartist)。例如,将它们放在一起,您可以执行以下操作:

void rmlf (char *s);
....
char title[MAX_TITLE] = "";
char artist[MAX_ARTIST = "";
char a[10] = "";
int min, sec;
...
while (fgets (title, MAX_TITLE, file) &&     /* validate read of values */
       fgets (artist, MAX_ARTIST, file) &&
       fgets (a, 10, file)) {

    if (sscanf (a, "%d %d", &min, &sec) != 2) {  /* validate conversion */
        fprintf (stderr, "error: failed to parse 'min' 'sec'.\n");
        continue;  /* skip line - tailor to your needs */
    }

    rmlf (title);   /* remove trailing newline */
    rmlf (artist);

    s[i].time.min = min;    /* copy to struct members & increment index */
    s[i].time.sec = sec;
    strncpy (s[i].title, title, MAX_TITLE);
    strncpy (s[i++].artist, artist, MAX_ARTIST);
}

/** remove tailing newline from 's'. */
void rmlf (char *s)
{
    if (!s || !*s) return;
    for (; *s && *s != '\n'; s++) {}
    *s = 0;
}

注意:这也将读取所有值,直到遇到EOF没有使用feof(参见相关链接:Why is “while ( !feof (file) )” always wrong?))


使用fgets防止短读

继 Jonathan 的评论之后,在使用 fgets 时,您应该真正检查以确保您确实阅读了整行,并且没有遇到您提供的最大字符值不是 short read足以读取整行(例如,短读,因为该行中的字符仍未读取)

如果发生短读,这将完全破坏您从文件中读取任何其他行的能力,除非您正确处理故障。这是因为下一次读取尝试不会从您认为正在读取的行开始读取,而是尝试读取 short read 发生的行的剩余字符。

您可以通过验证读入缓冲区的最后一个字符实际上是'\n' 字符来验证fgets 的读取。 (如果该行长于您指定的最大值,则 nul-terminating 字符之前的最后一个字符将是普通字符。)如果遇到 short read,然后,您必须读取并丢弃长行中的剩余字符,然后再继续下一次读取。 (除非您使用的是动态分配的缓冲区,您可以根据需要简单地 realloc 读取行的其余部分和您的数据结构)

您的情况使验证复杂化,因为每个结构元素都需要输入文件中的 3 行数据。在读取循环的每次迭代期间,您必须始终保持 3 行读取同步读取所有 3 行(即使发生短读取)。这意味着您必须验证是否已读取所有 3 行并且没有发生短读,以便在不退出输入循环的情况下处理任何一个 短读。 (如果您只想终止任何一个短读的输入,您可以单独验证每个,但这会导致输入例程非常不灵活。

除了从输入中删除尾随换行符之外,您还可以将上面的 rmlf 函数调整为验证 fgets 每次读取的函数。我在下面的一个名为shortread 的函数中完成了这项工作。对原始函数和读取循环的调整可以这样编码:

int shortread (char *s, FILE *fp);
...
    for (idx = 0; idx < MAX_SONGS;) {

        int t, a, b;
        t = a = b = 0;

        /* validate fgets read of complete line */
        if (!fgets (title, MAX_TITLE, fp)) break;
        t = shortread (title, fp);

        if (!fgets (artist, MAX_ARTIST, fp)) break;
        a = shortread (artist, fp);

        if (!fgets (buf, MAX_MINSEC, fp)) break;
        b = shortread (buf, fp);

        if (t || a || b) continue;  /* if any shortread, skip */

        if (sscanf (buf, "%d %d", &min, &sec) != 2) { /* validate conversion */
            fprintf (stderr, "error: failed to parse 'min' 'sec'.\n");
            continue;  /* skip line - tailor to your needs */
        }

        s[idx].time.min = min;   /* copy to struct members & increment index */
        s[idx].time.sec = sec;
        strncpy (s[idx].title, title, MAX_TITLE);
        strncpy (s[idx].artist, artist, MAX_ARTIST);
        idx++;
    }
...
/** validate complete line read, remove tailing newline from 's'.
 *  returns 1 on shortread, 0 - valid read, -1 invalid/empty string.
 *  if shortread, read/discard remainder of long line.
 */
int shortread (char *s, FILE *fp)
{
    if (!s || !*s) return -1;
    for (; *s && *s != '\n'; s++) {}
    if (*s != '\n') {
        int c;
        while ((c = fgetc (fp)) != '\n' && c != EOF) {}
        return 1;
    }
    *s = 0;
    return 0;
}

注意:在上面的示例中,shortread 检查构成标题、艺术家、时间组的每一行的结果。)

为了验证该方法,我整理了一个简短的示例,有助于将所有内容放在上下文中。查看示例,如果您有任何其他问题,请告诉我。

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

/* constant definitions */
enum { MAX_MINSEC = 10, MAX_ARTIST = 32, MAX_TITLE = 48, MAX_SONGS = 64 };

typedef struct {
    int min;
    int sec;
} stime;

typedef struct {
    char title[MAX_TITLE];
    char artist[MAX_ARTIST];
    stime time;
} songs;

int shortread (char *s, FILE *fp);

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

    char title[MAX_TITLE] = "";
    char artist[MAX_ARTIST] = "";
    char buf[MAX_MINSEC] = "";
    int  i, idx, min, sec;
    songs s[MAX_SONGS] = {{ .title = "", .artist = "" }};
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        fprintf (stderr, "error: file open failed '%s'.\n", argv[1]);
        return 1;
    }

    for (idx = 0; idx < MAX_SONGS;) {

        int t, a, b;
        t = a = b = 0;

        /* validate fgets read of complete line */
        if (!fgets (title, MAX_TITLE, fp)) break;
        t = shortread (title, fp);

        if (!fgets (artist, MAX_ARTIST, fp)) break;
        a = shortread (artist, fp);

        if (!fgets (buf, MAX_MINSEC, fp)) break;
        b = shortread (buf, fp);

        if (t || a || b) continue;  /* if any shortread, skip */

        if (sscanf (buf, "%d %d", &min, &sec) != 2) { /* validate conversion */
            fprintf (stderr, "error: failed to parse 'min' 'sec'.\n");
            continue;  /* skip line - tailor to your needs */
        }

        s[idx].time.min = min;   /* copy to struct members & increment index */
        s[idx].time.sec = sec;
        strncpy (s[idx].title, title, MAX_TITLE);
        strncpy (s[idx].artist, artist, MAX_ARTIST);
        idx++;
    }
    if (fp != stdin) fclose (fp);   /* close file if not stdin */

    for (i = 0; i < idx; i++)
        printf (" %2d:%2d  %-32s  %s\n", s[i].time.min, s[i].time.sec, 
                s[i].artist, s[i].title);

    return 0;
}

/** validate complete line read, remove tailing newline from 's'.
 *  returns 1 on shortread, 0 - valid read, -1 invalid/empty string.
 *  if shortread, read/discard remainder of long line.
 */
int shortread (char *s, FILE *fp)
{
    if (!s || !*s) return -1;
    for (; *s && *s != '\n'; s++) {}
    if (*s != '\n') {
        int c;
        while ((c = fgetc (fp)) != '\n' && c != EOF) {}
        return 1;
    }
    *s = 0;
    return 0;
}

示例输入

$ cat ../dat/titleartist.txt
First Title I Like
First Artist I Like
3 40
Second Title That Is Way Way Too Long To Fit In MAX_TITLE Characters
Second Artist is Fine
12 43
Third Title is Fine
Third Artist is Way Way Too Long To Fit in MAX_ARTIST
3 23
Fourth Title is Good
Fourth Artist is Good
32274 558212 (too long for MAX_MINSEC)
Fifth Title is Good
Fifth Artist is Good
4 27

使用/输出示例

$ ./bin/titleartist <../dat/titleartist.txt
  3:40  First Artist I Like               First Title I Like
  4:27  Fifth Artist is Good              Fifth Title is Good

【讨论】:

  • 代码是否应该检查前两个(全部三个?)fgets() 调用是否读取了换行符?如果存在,则删除尾随字符,但您似乎不能确保它们存在(如果它们不存在,同步将全部错误)。除此之外,您的代码与我生成的代码非常相似。
  • 是的,您在这一点上是 100% 正确的。可以进行额外验证以确保titleartist 中的最后一个字符都是'\n',以确保所有字符都适合MAX_TITLEMAX_ARTIST,如果不是,则处理该错误。我要去现场检查,回来后会更新。
  • @JonathanLeffler,今天花费的时间比预期的要长...无论如何,我已经调整了输入例程以防止短读取,同时保持 3 行读取的同步。这样做显着改变了验证检查所需的内容,我欢迎您对结果提出想法。
  • 对我来说看起来不错 — 抱歉,这本不应该如此苛刻。而且我已经给出了唯一允许我投的赞成票。还有一些其他选项可以考虑/提及。一种是使用 POSIX getline(),它总是读取整行,除非内存不足。另一个是围绕fgets() 的专用包装器,它使用本地长输入缓冲区(例如char buffer[4096];)来调用fgets(),找到换行符(如果没有,则使用getc() 读取换行符一),然后复制并截断数据以适合传递的数组。
  • 这样做的一个小好处是您可以将所有空间用于标题和艺术家;就目前而言,必须将换行符读入标题/艺术家的空间,然后将其删除,因此您实际上会丢失(浪费?)一个字符。
【解决方案2】:

我会使用 strtok() 和 atoi(),而不是 sscanf()。

只是好奇,为什么两个整数只有 10 个字节?你确定它们总是那么小吗?

顺便说一句,对于这么简短的回答,我深表歉意。我确信有一种方法可以让 sscanf() 为你工作,但根据我的经验 sscanf() 可能相当挑剔,所以我不是一个大粉丝。当用 C 解析输入时,我发现它更有效(就编写和调试代码需要多长时间而言)只需使用 strtok() 标记输入并使用各种 ato 单独转换每个部分?函数(atoi、atof、atol、strtod 等;参见 stdlib.h)。它使事情变得更简单,因为每条输入都是单独处理的,这使得调试任何问题(如果它们出现)变得更加容易。最后,与过去尝试使用 sscanf() 时相比,我通常花费更少的时间让此类代码可靠地工作。

【讨论】:

  • 看起来问题是(除了 while(!feof(file)) 的不良做法),我正在用 sscanf 解析并在函数调用中增加 i 。当我将i++ 放在下一行时,效果很好。
【解决方案3】:

使用"%*s %*s %d %d" 作为您的格式字符串,而不是...

您似乎期望sscanf 自动跳过导致十进制数字字段的两个标记。它不会这样做,除非你明确告诉它(因此是一对%*s)。

你不能指望设计 C 的人会像你一样设计它。正如 iharob 所说,您需要检查返回值。

这还不是全部。您需要阅读(并充分理解)整个scanf 手册(OpenGroup 编写的那本可以)。这样您就知道如何使用函数(包括格式字符串的所有细微差别)以及如何处理返回值。

作为一名程序员,您需要阅读。记住这一点。

【讨论】:

  • 我明白你在说什么,但我认为这个帖子的意思是他正在阅读 3 个不同的行(这也会对我产生影响)
  • 可能。在这种情况下,他需要了解 %s 的真正含义,这样他才能避免在生产代码中犯那个错误...
  • 我完全阅读了前 2 行,如您所见,我没想到 sscanf 会神奇地跳过这些行,因为我阅读了 char a[] 中的 2 个整数并使用 sscanf 来解析里面的字符。感谢您的回复!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-25
  • 2017-02-18
  • 2019-11-29
  • 2021-09-20
  • 2012-08-18
相关资源
最近更新 更多