正确使用sscanf

5
我需要得到一个任意以下格式的输入行:
  • 单词1和单词2之间必须有空格。
  • 单词2和单词3之间必须有逗号。
  • 单词2和单词3之间可以没有空格,但是可能有任意数量的空格。
如何分离1、2和3个单词的情况,并将数据放入正确的变量中?
word1
word1 word2 
word1 word2 , word3
word1 word2,word3

我想到了以下内容:

我想到了一些类似的东西:

sscanf("string", "%s %s,%s", word1, word2, word3);

但它似乎无法运行。

我使用严格的C89规范。


输入行是一个单独的字符串吗?所以你需要从字符串中提取标记(分隔符为空格和逗号),对吗? - vulkanino
3个字符串,第一个分隔符是空格,第二个分隔符是逗号。 - Nahum
6
使用sscanf(以及所有scanf系列函数)的最恰当方式是不使用它们。当然,有时候scanf的奇怪行为确实可以完美地满足你的需求,但通常情况下,你会不得不编写一些骇客代码来绕过其行为,这种情况下最好还是从头开始编写自己干净的解析器。 - R.. GitHub STOP HELPING ICE
4个回答

25
int n = sscanf("string", "%s %[^, ]%*[, ]%s", word1, word2, word3);

n 的返回值告诉你成功分配了多少个值。 %[^, ] 是一个反向字符类匹配,可以找到不包括逗号或空格(如果需要,也可以添加制表符)的单词。 %*[, ] 是一个匹配逗号或空格但不执行分配的匹配项。

我不确定在实践中是否要使用它,但应该可以工作。但是,它未经测试。


也许更严格的规范是:

int n = sscanf("string", "%s %[^, ]%*[,]%s", word1, word2, word3);
区别在于不赋值的字符类仅接受逗号。`sscanf()`在`word2`之后停止于任何空格(或EOS,即字符串结尾),并跳过分配给`word3`之前的空格。先前版本允许第二个和第三个单词之间有一个空格代替逗号,但问题没有严格允许。
正如pmg在评论中建议的那样,应该给出赋值转换规范的长度以防止缓冲区溢出。注意,长度不包括空终止符,因此格式字符串中的值必须比数组大小少1字节。还要注意,虽然`printf()`允许您使用`*`动态指定大小,但`sscanf()`等则使用`*`来禁止赋值。这意味着您必须为手头的任务创建特定的字符串。
char word1[20], word2[32], word3[64];
int n = sscanf("string", "%19s %31[^, ]%*[,]%63s", word1, word2, word3);
(Kernighan和Pike在他们的(极好的)书籍"The Practice of Programming"或Amazon The Practice of Programming 1999中建议动态地格式化格式字符串。)

刚发现一个问题:给定"word1 word2, word3",它不会读取word3。有解决方法吗?

是的,有解决方法,而且实际上很简单。在不分配、逗号匹配的转换规范之前,在格式字符串中加入一个空格。因此:

#include <stdio.h>

static void tester(const char *data)
{
    char word1[20], word2[32], word3[64];
    int n = sscanf(data, "%19s %31[^, ] %*[,]%63s", word1, word2, word3);
    printf("Test data: <<%s>>\n", data);
    printf("n = %d; w1 = <<%s>>, w2 = <<%s>>, w3 = <<%s>>\n", n, word1, word2, word3);
}

int main(void)
{
    const char *data[] =
    {
        "word1 word2 , word3",
        "word1 word2 ,word3",
        "word1 word2, word3",
        "word1 word2,word3",
        "word1 word2       ,       word3",
    };
    enum { DATA_SIZE = sizeof(data)/sizeof(data[0]) };
    size_t i;
    for (i = 0; i < DATA_SIZE; i++)
        tester(data[i]);
    return(0);
}

示例输出:

Test data: <<word1 word2 , word3>>
n = 3; w1 = <<word1>>, w2 = <<word2>>, w3 = <<word3>>
Test data: <<word1 word2 ,word3>>
n = 3; w1 = <<word1>>, w2 = <<word2>>, w3 = <<word3>>
Test data: <<word1 word2, word3>>
n = 3; w1 = <<word1>>, w2 = <<word2>>, w3 = <<word3>>
Test data: <<word1 word2,word3>>
n = 3; w1 = <<word1>>, w2 = <<word2>>, w3 = <<word3>>
Test data: <<word1 word2       ,       word3>>
n = 3; w1 = <<word1>>, w2 = <<word2>>, w3 = <<word3>>

如果“非赋值字符类”只接受逗号,那么您可以在格式字符串中将其缩写为文字逗号:

int n = sscanf(data, "%19s %31[^, ] , %63s", word1, word2, word3);

将其插入测试工具产生与之前相同的结果。请注意,所有代码都受益于审查;即使在其正常工作之后,它通常(基本上总是)可以改进。


2
我只会在输入中添加一个限制:...scanf("%99s")... 或者 ...scanf("%99[^, ]")... 适用于类型为 char[100] 的数组。 - pmg
@NahumLitvin:是的,这是C89以及C99(我猜测还包括C2011)。 - Jonathan Leffler
刚刚发现一个问题。情况是:"word1 word2,word3" 它没有读取word3。 - Nahum
@Jonathan Leffler,你太棒了!我爱你! - Nahum
@JonathanLeffler 我发现另一个有问题的案例: n = sscanf(".entry 418:34 ‎09/‎03/‎2012sex\n","%s %[^, ] %*[,]%s", _command, _operand1, _operand2); 返回 n =2 _command = .entry _operand1=418:34 _operand2= 0xcccccccccc - Nahum
有什么问题吗?你说“第2和第3字段之间必须有逗号”。但是在你的样本片段中第2和第3字段之间没有逗号,因此sccanf()正确地告诉了你,指出它只能正确转换其中的两个字段。如果你有其他规范,你需要重新设计格式字符串以匹配另一种规范。(听起来好像你在想“第二个和第三个字段可以用逗号或空格分隔,但如果有逗号,则不需要有空格。”) - Jonathan Leffler

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

int main ()
{
  char str[] ="word1 word2,word3";
  char* pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);

  pch = strtok(str," ,");
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");
  }
  return 0;
}

这是否允许在前两个单词之间使用逗号作为分隔符,而这是不被允许的? - hmjd
是的,但通过从第一个对 strtok() 的调用中删除逗号可以轻松解决这个问题。您可以随意更改每个调用中的分隔符集;您不受限于在每个调用中使用单个分隔符集。 - Jonathan Leffler

4

摘要: 答案分为三部分。第一部分回答了“正确使用sscanf”的一般问题,描述了使用sscanf的好处以及何时最好使用sscanf。第二部分回答了具体问题的特定部分。第三部分对于一般问题和特定问题都非常重要,尽可能详细、简单地描述了sscanf的内部工作。

第一部分 使用sscanf的优点:使用sscanf可以将一个大问题(原始输入行)一次性分解为较小的问题(输出标记)。

如果行规则定义得很好(例如问题中的行规则已经定义得很好:单词1和单词之间必须有空格,单词2和单词3之间必须有逗号。单词2和单词3之间不一定需要空格,但可能有任意数量的空格),那么sscanf可以给出一个“Yes/No”答案来回答“当前读取的行是否符合行规则?”(而不是试图分析和理解输入文件中键入的内容或者打算在那里键入的内容),并且它也可以立即给出行的输出标记。

为了实现将输入字符串分解为标记的目的,使用%c是方便的。我们应该记住,按默认情况,sscanf跳过空白字符(空格、制表符和换行符),但在%c的情况下不会跳过,其中sscanf读取空格并将其分配为相应字符变量的值。

使用strtok是更加通用和灵活的,但它没有一次性读取整行和使用丰富的词法分析(即%d、%f、%c*、^和所有sscanf的词汇)的优点。如果行规则定义得很好,并且对于问题“当前读取的行是否符合行规则?”的“Yes/No”答案已经足够,那么可以使用这些优点。

第二部分 回答具体问题:这里有一个似乎有效的sscanf代码行,下面是代码行的解释。(假设数字100大于最大输入行大小。)

调用:

n = sscanf("  sssfdf wret      ,   123  fdsgs fdgsdfg",
"%100[^ ]%c%100[^,] %c %100[^\0]", s1, &ch1, s2, &ch2, s3);

会导致:
s1 = ""sssfdf";
ch1=' ';
s2=""wret      ";
ch2=',';
s3=""123  fdsgs fdgsdfg";
  1. 将前100个字符或者第一个空格前的所有字符读入s1。(记住条件是第一个单词和第二个单词之间只有一个空格)。

  2. 将下一个字符读入ch1(稍后我们可以检查ch1是否为一个空格)。

  3. 将前100个字符或者第一个逗号前的所有字符读入s2,s2中可能包含需要删除的空格。(第二个单词和第三个单词之间应该有一个逗号,逗号前后可以有可选的空格)。

请注意,%100[^ ]%c%100[^,]没有空格,因为在%c前的空格会导致ch1后面的字符被删除,在%100[^,]前的空格会使第一个单词和第二个单词之间有多个空格。

  1. 将下一个字符读入ch2(稍后我们可以检查ch2是否为一个逗号)。

  2. 将剩余的输入字符串读入s3(从第一个非空格字符到字符串终止符之间的所有字符)。

现在需要检查s1、s2和s3的有效性(并测试ch1和ch2的值是否为一个空格和逗号)。

第三部分sscanf的内部工作:sscanf()函数会逐个字符读取格式字符串。这个字符有三种可能的值,空格、'%'或其他字符。

  1. 如果下一个字符不是空格,也不是“%”,那么它将开始读取输入字符串 1.1 如果输入字符串中的下一个字符不是格式字符串中的字符,则sscanf停止其工作,并返回到调用程序,其中包括迄今为止读取的参数数量。 示例:

    n = sscanf(" 2 22.456","2%f",&FloatArg); /* n is 0 */

    1.2 如果输入字符串中的下一个字符是格式字符串中的字符,则sscanf继续从格式字符串读取下一个字符。

    n = sscanf("2 22.456","2%f",&FloatArg); // n is 1 FloatArg=22.456

  2. 如果格式字符串中的下一个字符是“%”,则sscanf跳过空格并等待以“%”格式读取字符串。例如对于%f,它等待以以下格式读取和输入: [+/-][IntDigiT1]...[IntDigiTn]<....>. 示例:31.25,32.,3 2.1 如果sscanf没有找到该格式,则它将返回迄今为止读取的参数数。 示例:

    n = sscanf("aaa","%f",&FloatArg); // n = 0

    2.2 如果sscanf读取了至少一个数字或跟随'.'的一系列数字,则在遇到一个非数字时,它会得出已达到浮点数的结论。 sscanf()将nondigit放回输入,并将读取的值分配给浮点变量。 示例1:

    n = sscanf("2 22.456","2%f",&FloatArg); // FloatArg is 22.456

    示例2:

    n = sscanf("22.456","2%f",&FloatArg); // FloatArg is 2.456

  3. 如果格式字符串中的下一个字符为空格,则意味着跳过下一个输入字符之前的任何空格。

A. 读取字符(%c):如果下一个输入字符是空格(例如空格),则将空格分配给指定变量。

B. 读取字符串(%s):除空格外的任何字符均可接受, 因此scanf()跳过空格到第一个非空格字符,然后保存非空格字符,直到再次遇到空格。 sscanf在被分配的字符串变量末尾添加'\0',即字符串终止符。

C. 答案不符合格式 % 变化。[=%[*][width][modifiers]type=] 这部分的详细描述可以在以下链接中找到:http://docs.roxen.com/(en)/pike/7.0/tutorial/strings/sscanf.xml请注意,上述链接中的%[characters]用于私人问题的答案,并可实现字符串的灵活操作。
D. 以上是我在互联网搜索和在Dev-C++ 5.11中测试各种字符串时找到的结果,不能保证完整性。欢迎提出建设性意见,这将有助于我改进答案。

0

说实话,这已经超出了scanf和相关函数的范围;除了“编写自己的简单解析器”的答案之外,您还可以投资于yacc来解析语法(词法分析器留给读者作为练习):

line: oneword | twowords | threewords;
oneword: word;
twowords: word word;
threewords: word word word;
word: STRING;

这可能对你来说有些过度,但如果你需要解析比较复杂的格式,它会是一个救星。


1
有一个反例证明你最初的陈述是夸大其词的。只需要稍微增加一些复杂性,就可以将要求推到超出 sscanf() 能够处理的范围之外(例如,考虑带引号的逗号包含单词的类似 CSV 的数据),但这实际上是可行的。 - Jonathan Leffler
本应该说“超出了scanf适当使用范围”,但是随便啦 :D - tbert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接