【问题标题】:fscanf with whitespaces as separators - what format should I use?以空格作为分隔符的 fscanf - 我应该使用什么格式?
【发布时间】:2017-06-30 18:46:43
【问题描述】:

我有一个txt文件,它的行如下

[7 chars string][whitespace][5 chars string][whitespace][integer]

我想使用 fscanf() 将所有这些读入内存,但我对应该使用什么格式感到困惑。

这是这样一行的一个例子:

hello   box   94324

注意每个字符串中的填充空格,除了分隔空格。

编辑:我知道首先使用 fgets() 的建议,我不能在这里使用它。

编辑:这是我的代码

typedef struct Product {
    char* id;   //Product ID number. This is the key of the search tree.
    char* productName;  //Name of the product.
    int currentQuantity;    //How many items are there in stock, currently. 
} Product;

int main()
{
    FILE *initial_inventory_file = NULL;
    Product product = { NULL, NULL, 0 };

    //open file 
    initial_inventory_file = fopen(INITIAL_INVENTORY_FILE_NAME, "r");

    product.id = malloc(sizeof(char) * 10); //- Product ID: 9 digits exactly. (10 for null character)
    product.productName = malloc(sizeof(char) * 11); //- Product name: 10 chars exactly.

    //go through each line in inital inventory
    while (fscanf(initial_inventory_file, "%9c %10c %i", product.id, product.productName, &product.currentQuantity) != EOF)
    {
        printf("%9c %10c %i\n", product.id, product.productName, product.currentQuantity);
    }

    //cleanup...
    ...
}

这是一个文件示例:(实际上是 10 个字符、9 个字符和 int)

022456789 box-large  1234
023356789 cart-small 1234
023456789 box        1234
985477321 dog food   2
987644421 cat food   5555
987654320 snaks      4444
987654321 crate      9999
987654322 pillows    44

【问题讨论】:

  • @BLUEPIXY 有一个很好的comment - 一定要检查fscanf() 的返回值。
  • 当你说你必须使用 f/scanf 而你不能使用 fgets 时,我认为这是你正在上课的一个作业。如果是这样,请帮自己一个忙,一旦结束就忘记这一课。在现实世界的 C 编程中,基本上 nobody 使用scanffscanf 做任何事情。它们实际上没用。我会说学习它们完全是浪费时间,但我无法影响你的导师。
  • 关于fgets() 的一个关键问题,fscanf() 的问题以及此类帖子是错误处理之一。当输入unexpected时,代码应该怎么做?这篇文章的中心是如何读取预期的输入,但没有解决输入是否可能可以使用"hello \n box 94324\n""hello (many space) box 94324\n""hello \n box\n""hello \n box XYZ\n"。祝你好运。
  • @DavidBowling 旁白:“值得学习 fscanf(),如果只是能够使用 sscanf()”不如“学习有用的函数 sscanf()”那么直接也许 i> 学习fscanf() 或跳过后者。 (f)scanf() 不需要被视为 sscanf() 的先决条件。
  • 在调用任何scanf() 系列函数时,始终检查返回值(而不是参数值)以确保操作成功。在当前场景下,返回值必须为3,否则会出错。

标签: c


【解决方案1】:

假设您的输入文件格式正确,这是最直接的版本:

char str1[8] = {0};
char str2[6] = {0};
int  val;
...
int result = fscanf( input, "%7s %5s %d", str1, str2, &val );

如果result 等于 3,则您成功读取了所有三个输入。如果它小于 3 但不是 EOF,那么您的一个或多个输入出现匹配失败。如果是EOF,你要么到达文件末尾要么输入错误;此时使用feof( input ) 测试EOF。

如果您不能保证您的输入文件格式正确(我们大多数人不能),那么您最好将整行作为文本读取并自己解析。你说你不能使用fgets,但是有一种方法可以使用fscanf

char buffer[128]; // or whatever size you think would be appropriate to read a line at a time

/**
 * " %127[^\n]" tells scanf to skip over leading whitespace, then read
 * up to 127 characters or until it sees a newline character, whichever
 * comes first; the newline character is left in the input stream.
 */
if ( fscanf( input, " %127[^\n]", buffer ) == 1 )
{
  // process buffer
}

然后您可以使用sscanf 解析输入缓冲区:

int result = sscanf( buffer, "%7s %5s %d", str1, str2, &val );
if ( result == 3 )
{
  // process inputs
}
else
{
  // handle input error
}

或通过其他方法。

编辑

需要注意的边缘情况:

  1. 每行缺少一个或多个输入
  2. 格式错误的输入(例如整数字段中的非数字文本)
  3. 每行不止一组输入
  4. 长度超过 7 或 5 个字符的字符串
  5. 值太大,无法存储在 int

编辑 2

我们大多数人不推荐fscanf 的原因是因为它有时会使错误检测和恢复变得困难。例如,假设您有输入记录

foo     bar    123r4
blurga  blah   5678

然后您使用fscanf( input, "%7s %5s %d", str1, str2, &val ); 阅读它。 fscanf 将读取123 并将其分配给val,将r4 留在输入流中。在下一次调用中,r4 将分配给 str1blurga 将分配给 str2,您将在 blah 上遇到匹配失败。理想情况下,您想拒绝整个第一条记录,但是当您知道有问题时,为时已晚。

如果你把它读成一个字符串first,你可以解析和检查每个字段,如果其中任何一个是坏的,你可以拒绝整个事情。

【讨论】:

  • 有时,我想将这样的答案加倍紫外线。当然有赏金的东西....(唯一的弱点是" %127[^\n]" 可以消耗超过 1 行并将 \n 留在流中 - 但是对于不合理的fgets()-less 代码要求来说是一个很好的快速修复。)
  • @chux:是的,这是个问题。问题是,前一个调用将换行符留在流中,因此必须以某种方式处理它。我想我可以添加一个%c 来消耗换行符本身。但这只是进一步说明scanf 是错误的工具。
【解决方案2】:

假设输入是

<LWS>* <first> <LWS>+ <second> <LWS>+ <integer>

其中&lt;LWS&gt; 是任何空白字符,包括换行符; &lt;first&gt; 有 1 到 7 个非空白字符; &lt;second&gt; 有 1 到 5 个非空白字符; &lt;integer&gt; 是一个可选的有符号整数(如果以0x0X 开头则为十六进制,如果以0 开头则为八进制,否则为十进制); * 表示零个或多个前面的元素;而+ 表示前面的一个或多个元素。

假设你有一个结构,

struct record {
    char first[8];  /* 7 characters + end-of-string '\0' */
    char second[6]; /* 5 characters + end-of-string '\0' */
    int  number;
};

然后您可以将流in 中的下一条记录读取到调用者指向的结构中,例如

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

/* Read a record from stream 'in' into *'rec'.
   Returns: 0 if success
           -1 if invalid parameters
           -2 if read error
           -3 if non-conforming format
           -4 if bug in function
           +1 if end of stream (and no data read)
*/
int read_record(FILE *in, struct record *rec)
{
    int rc;

    /* Invalid parameters? */
    if (!in || !rec)
        return -1;

    /* Try scanning the record. */
    rc = fscanf(in, " %7s %5s %d", rec->first, rec->second, &(rec->number));

    /* All three fields converted correctly? */
    if (rc == 3)
        return 0; /* Success! */

    /* Only partially converted? */
    if (rc > 0)
        return -3;

    /* Read error? */
    if (ferror(in))
        return -2;

    /* End of input encountered? */
    if (feof(in))
        return +1;

    /* Must be a bug somewhere above. */
    return -4;
}

转换说明符%7s 最多转换七个非空白字符,%5s 最多转换五个;数组(或 char 指针)必须有空间容纳额外的字符串结尾 nul 字节 '\0'scanf() family of functions 会自动添加。

如果不指定长度限制,并使用%s,输入可能会超出指定的缓冲区。这是常见buffer overflow 错误的常见原因。

scanf() 系列函数的返回值是成功转换的次数(可能是0),如果发生错误,则返回EOF。上面,我们需要三个转换来完全扫描一条记录。如果我们只扫描 1 或 2 个,我们就有部分记录。否则,我们通过检查ferror() 来检查是否发生了流错误。 (注意你要在feof()之前检查ferror(),因为错误条件也可能设置feof()。)如果没有,我们检查扫描函数是否在转换之前遇到流尾,使用@987654346 @。

如果上述情况均未满足,则扫描函数返回零或负数,ferror()feof() 均未返回 true。因为扫描模式以(空格和)转换说明符开头,所以它永远不应该返回零。 scanf() 系列函数中唯一的非正返回值是EOF,这应该会导致feof() 返回true。因此,如果上述情况均未满足,则代码中一定存在错误,由输入中的一些奇怪的极端情况触发。

从某个流中读取结构到动态分配的缓冲区的程序通常实现以下伪代码:

Set ptr = NULL  # Dynamically allocated array
Set num = 0     # Number of entries in array
Set max = 0     # Number of entries allocated for in array

Loop:

    If (num >= max):
        Calculate new max; num + 1 or larger
        Reallocate ptr
        If reallocation failed:
            Report out of memory
            Abort program
        End if
    End if

    rc = read_record(stream, ptr + num)
    If rc == 1:
        Break out of loop
    Else if rc != 0:
        Report error (based on rc)
        Abort program
    End if
End Loop

【讨论】:

    【解决方案3】:

    使用"%9c ..." 格式的代码中的问题是%9c 没有写入字符串终止字符。因此,您的字符串可能充满了垃圾并且根本没有终止,这会在使用printf 打印出来时导致未定义的行为。

    如果在第一次扫描之前将字符串的完整内容设置为0,它应该可以按预期工作。为此,您可以使用calloc 而不是malloc;这将使用0 初始化内存。

    请注意,代码还必须以某种方式消耗换行符,这可以通过附加的fscanf(f,"%*c")-statement 来解决(* 表示该值已被消耗,但未存储到变量中)。只有在最后一位数字和换行符之间没有其他空格时才有效:

    int main()
    {
        FILE *initial_inventory_file = NULL;
        Product product = { NULL, NULL, 0 };
    
        //open file
        initial_inventory_file = fopen(INITIAL_INVENTORY_FILE_NAME, "r");
    
        product.id = calloc(sizeof(char), 10); //- Product ID: 9 digits exactly. (10 for null character)
        product.productName = calloc(sizeof(char), 11); //- Product name: 10 chars exactly.
    
        //go through each line in inital inventory
        while (fscanf(initial_inventory_file, "%9c %10c %i", product.id, product.productName, &product.currentQuantity) == 3)
        {
            printf("%9s %10s %i\n", product.id, product.productName, product.currentQuantity);
            fscanf(initial_inventory_file,"%*c");
        }
    
        //cleanup...
    }
    

    【讨论】:

    • 为什么不在fscanf() 格式字符串中使用前导空格而不是尾随空格?尾随空格在scanf()(阻塞输入)中不会按预期工作,并且可能会使学习者感到困惑,而前导空格对两者都有效。
    • @David Bowling:因为我假设像" llo .... " 这样的字符串应该读作"__llo",而不是"llo"。这就是为什么我不能使用领先的 WS。
    • 嗯。我理解你的意思,这是我在有效的格式字符串中看到尾随空格的少数几次之一;然而 OP 代码有一条评论说://- Product ID: 9 digits exactly. 我认为这个规定是不必要的,除非我错过了问题或 cmets 中的其他内容。
    • 不是有人说他们讨厌scanf() 函数吗?
    • @David Bowling:我试过"%i%*c",它有效(也适用于文件的最后一行,没有换行)
    【解决方案4】:

    您是否尝试过格式说明符?

    char seven[8] = {0};
    char five[6] = {0};
    int myInt = 0;
    
    // loop here
    fscanf(fp, "%s %s %d", seven, five, &myInt);
    // save to structure / do whatever you want
    

    如果您确定格式和字符串始终是固定长度,您还可以逐个字符地迭代输入(使用 fgetc() 之类的东西并手动处理它。上面的示例可能会导致分段错误,如果字符串在文件中超过 5 或 7 个字符。

    编辑手动扫描循环:

    char seven[8] = {0};
    char five[6] = {0};
    int myInt = 0;
    
    // loop this part
    for (int i = 0; i < 7; i++) {
        seven[i] = fgetc(fp);
    }
    assert(fgetc(fp) == ' '); // consume space (could also use without assert)
    for (int i = 0; i < 5; i++) {
        five[i] = fgetc(fp);
    }
    assert(fgetc(fp) == ' '); // consume space (could also use without assert)
    fscanf(fp, "%d", &myInt);
    

    【讨论】:

    猜你喜欢
    • 2019-11-27
    • 1970-01-01
    • 2016-10-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-07
    • 2015-01-06
    相关资源
    最近更新 更多