在 C 中读取具有不同数据类型的多行

Reading multiple lines with different data types in C

我有一个很奇怪的问题,我正在尝试用 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 个字符,包括白色 space 之间。

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

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

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

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

%s
%s
%d %d

(主要通过您使用 fgets,一个 line-oriented 输入函数来证明,它读取一行输入(最多 including the '\n') each time it is called.) 如果不是这种情况,则以下不适用(并且可以大大简化)

由于您正在将多个值读取到结构数组中的单个元素中,您可能会发现在开始将信息复制到您的结构成员本身。这允许您 (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 (参见相关 link:Why is “while ( !feof (file) )” always wrong?))


使用 fgets

防止短读

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

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

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

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

除了从输入中删除结尾的换行符之外,您还可以将上面的 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 检查组成的每一行的结果 title, artist,时间组。)

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

 #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

示例Use/Output

$ ./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

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

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

您不能指望设计 C 语言的人会以与您相同的方式设计它。正如 iharob 所说,您需要检查 return 值。

这还不是全部。您需要阅读(并相对理解)整个 scanf 手册(OpenGroup 编写的手册没问题)。这样你就知道如何使用该函数(包括格式字符串的所有细微差别)以及如何处理 return 值。

作为程序员,你需要阅读。好好记住吧。