【问题标题】:sscanf - Parse frame with optional/empty format specifierssscanf - 使用可选/空格式说明符解析帧
【发布时间】:2018-11-24 04:25:57
【问题描述】:

我正在尝试解析格式为以下方案的帧:

$[number],[number],[number],<string>;[string]~<string>

用'[]'包围的参数是可选的,用''包围的参数总是存在定义的:

因此,以下帧都是正确的:

$0,0,0,thisIsFirstString;secondString~thirdOne
$0,,0,firstString;~thirdOne
$,,,firstString;~thirdString

目前,当所有元素都存在时,我可以使用以下代码解析框架

int main() {
    char frame[100] = "$1,2,3,string1;string2~string3";
    char num1[10], num2[10], num3[10], str1[100], str2[100], str3[100];

    printf("frame : %s\n", frame);

    sscanf(frame,"$%[^,],%[^,],%[^,],%[^;];%[^~]~%s", num1, num2, num3, str1, str2, str3);

    printf("Number 1 : %s\n", num1);
    printf("Number 2 : %s\n", num2);
    printf("Number 3 : %s\n", num3);
    printf("String 1 : %s\n", str1);
    printf("String 2 : %s\n", str2);
    printf("String 3 : %s\n", str3);

    return 0;
}

结果如下

frame : $1,2,3,string1;string2~string3
Number 1 : 1
Number 2 : 2
Number 3 : 3
String 1 : string1
String 2 : string2
String 3 : string3

但是,如果缺少参数,则前面的参数会被很好地解析,而丢失参数之后的参数则不会。

frame : $1,,3,string1;string2~string3
Number 1 : 1
Number 2 : 
Number 3 : 
String 1 :��/�
String 2 : �\<��
String 3 : $[<��

frame : $1,2,3,string1;~string3
Number 1 : 1
Number 2 : 2
Number 3 : 3
String 1 : string1
String 2 : h�v��
String 3 : ��v��

如何向sscanf 指定帧中可能缺少某些参数,以便在这种情况下将它们丢弃?

【问题讨论】:

  • 我的提示:如果输入可能与您期望的不同,请不要使用sscanf,而是自己解析字符串。
  • 这样的解析真的不是scanf 和家人的强项之一。您可能需要考虑使用其他方法,例如使用strtok 甚至regular expressions 进行标记。
  • 如果没有提供可选数字,是否有默认值?
  • string 只用字母组成吗?
  • @J...S 否,可选数字(字符串相同)要么指定要么不存在(空字段)。

标签: c parsing scanf optional-parameters format-specifiers


【解决方案1】:

scanf() 无法转换句柄空字符类,strtok() 将每个分隔符序列视为单个分隔符,实际上只适用于空白。

这是一个简单的类似 scanf 的非贪婪解析器,用于您的目的:

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

int my_sscanf(const char *s, const char *fmt, ...) {
    int res = 0;
    va_list ap;

    va_start(ap, fmt);
    for (; *fmt; fmt++) {
        if (*fmt == '%') {
            fmt++;
            if (*fmt == 's') {
                size_t i = 0, size = va_arg(ap, size_t);
                char *dest = va_arg(ap, char *);

                while (*s && *s != fmt[1]) {
                    if (i + 1 < size)
                        dest[i++] = *s;
                    s++;
                }
                if (size)
                    dest[i] = '\0';
                res++;
                continue;
            }
            if (*fmt == 'd') {
                *va_arg(ap, int *) = strtol(s, (char **)&s, 10);
                res++;
                continue;
            }
            if (*fmt == 'i') {
                *va_arg(ap, int *) = strtol(s, (char **)&s, 0);
                res++;
                continue;
            }
            /* add support for other conversions as you wish */
            if (*fmt != '%')
                return -1;
        }
        if (*fmt == ' ') {
            while (isspace((unsigned char)*s))
                s++;
            continue;
        }
        if (*s == *fmt) {
            s++;
        } else {
            break;
        }
    }
    va_end(ap);
    return res;
}

int main() {
    char frame[100] = "$1,,3,string1;~string3";
    char str1[100], str2[100], str3[100];
    int res, num1, num2, num3;

    printf("frame : %s\n", frame);

    res = my_sscanf(frame, "$%d,%d,%d,%s;%s~%s", &num1, &num2, &num3,
                    sizeof str1, str1, sizeof str2, str2, sizeof str3, str3);

    if (res == 6) {
        printf("Number 1 : %d\n", num1);
        printf("Number 2 : %d\n", num2);
        printf("Number 3 : %d\n", num3);
        printf("String 1 : %s\n", str1);
        printf("String 2 : %s\n", str2);
        printf("String 3 : %s\n", str3);
    } else {
        printf("my_scanf returned %d\n", res);
    }
    return 0;
}

【讨论】:

    【解决方案2】:

    正如其他人所说,scanf() 系列函数可能不适合这种情况,因为如果输入字符串不是预期的格式,则无法进行适当的错误处理。

    但如果您确定输入字符串将始终采用该格式,您可以使用指向输入字符串相关部分的指针,然后使用sscanf() 处理该部分。

    首先将所有字符数组初始化为一个空字符串,这样打印的时候就不会显示垃圾了。喜欢

    char num1[10]="";
    

    对于要写入提取参数的所有char 数组。

    声明一个字符指针,使其指向输入字符串frame的开头。

    char *ptr=frame;
    

    现在检查第一个可选参数,例如

    if(sscanf(ptr, "$%[^,],", num1)==1)
    {
        //parameter 1 is present.
        ptr+=strlen(num1);  
    }
    ptr+=2;
    

    如果参数存在,我们将ptr 增加参数字符串的长度,并为“$”和逗号进一步增加2

    接下来的两个参数也是如此,它们也是可选的。

    if(sscanf(ptr, "%[^,]", num2)==1)
    {
        //Parameter 2 is present
        ptr+=strlen(num2);  
    }
    ptr+=1;
    
    if(sscanf(ptr, "%[^,]", num3)==1)
    {
        //Parameter 3 is present
        ptr+=strlen(num3);  
    }
    ptr+=1;
    

    下一个参数,参数 4,不是可选的。

    sscanf(ptr, "%[^;]", str1);
    ptr+=strlen(str1)+1;
    

    现在是可选参数 5,

    if(sscanf(ptr, "%[^~]", str2)==1)
    {
        //Parameter 5 is present
        ptr+=strlen(str2);  
    }
    ptr+=1; //for ~
    

    最后对于非可选参数 6,

    sscanf(ptr, "%s", str3);
    

    为简洁起见,省略了可能的错误检查。为了防止溢出,请在scanf() 格式字符串中使用宽度说明符,例如

    sscanf(ptr, "%9[^,],", num2);
    

    其中9num2 字符数组的长度小一。

    在您的程序中,如果%[^,] 部分对应于空字符串,sscanf() 将有效地停止分配给变量。

    【讨论】:

    • sscanf(ptr, "%[^,],", num2)==1 格式的第二个, 没有任何功能用途。此外,如果ptr 以没有, 结尾,则此代码将索引以ptr+=1; 传递的字符串结尾。也许这是“为简洁起见,省略了可能的错误检查”的一部分。
    • @chux 你的意思是,如果ptr指向的字符串部分匹配%[^,]是一个空字符串,sscanf()会停止分配,后面的逗号不会被考虑无论如何?
    • sscanf(ptr, "%[^,],", num2)==1 是测试num2 是否有写入内容。然而,它并不能保证, 紧随其后。考虑if(sscanf("abc", "%[^,],", num2)==1) { ptr+=strlen(num2); } ptr+=1; 的效果。 ptr 现在指向哪里?
    • sscanf(",","%[^,],", num2) 不应返回 1。因此该测试处理格式正确的输入。恕我直言,缺乏错误处理,显然对 OP 来说还可以,但这种方法的普遍适用性仍然是一个弱点。
    • sscanf() 本身不是错误问题,而是像 "if(sscanf("abc", "%[^,],", num2)==1) { ptr+ 这样的问题=strlen(num2); } ptr+=1;"和 "if(sscanf("abc(200 chracters)xyz,", "%[^,],", num2)。如果输入格式正确,您的答案可以正常工作,但我发现很难将其用作良好的基础错误处理。
    【解决方案3】:

    猜测最好的仍然是编写自己的解析器函数:

    #define _GNU_SOURCE 1
    #define _POSIX_C_SOURCE 1
    #include <stdio.h>
    #include <stddef.h>
    #include <assert.h>
    #include <string.h>
    #include <stdlib.h>
    
    #define __arraycount(x) (sizeof(x)/sizeof(x[0]))
    
    // from https://stackoverflow.com/a/3418673/9072753
    static char *mystrtok(char **m,char *s,char c)
    {
      char *p = s ? s : *m;
      if (!*p)
        return NULL;
      *m = strchr(p, c);
      if (*m)
        *(*m)++ = '\0';
      else
        *m = p + strlen(p);
      return p;
    }
    
    static char getseparator(size_t i)
    {
        return i <= 2 ? ',' : i == 3 ? ';' : i == 4 ? '~' : 0;
    }
    
    int main()
    {
        char ***output = NULL;
        size_t outputlen = 0;
        const size_t outputstrings = 6;
    
        char *line = NULL;
        size_t linelen = 0;
        size_t linecnt;
        for (linecnt = 0; getline(&line, &linelen, stdin) > 0; ++linecnt) {
            if (line[0] != '$') {
                printf("Lines not starting with $ are ignored\n");
                continue;
            }
    
            // alloc memory for new set of 6 strings
            output = realloc(output, sizeof(*output) * outputlen++);
            if (output == NULL) {
                fprintf(stderr, "%d Error allocating memory", __LINE__);
                return -1;
            }
            output[outputlen - 1] = malloc(sizeof(*output[outputlen - 1]) * outputstrings);
            if (output[outputlen - 1] == NULL) {
                fprintf(stderr, "%d Error allocating memory", __LINE__);
                return -1;
            }
    
            // remove closing newline
            line[strlen(line)-1] = '\0';
    
            //printf("Read line `%s`\n", line);
    
            char *token;
            char *rest = &line[1];
            char *state;
            size_t i;
            for (i = 0, token = mystrtok(&state, &line[1], getseparator(i)); 
                    i < outputstrings && token != NULL;
                    ++i, token = mystrtok(&state, NULL, getseparator(i))) {
                output[outputlen - 1][i] = strdup(token);
                if (output[outputlen - 1][i] == NULL) {
                    fprintf(stderr, "%d Error allocating memory", __LINE__);
                    return -1;
                }
                //printf("Read %d string: `%s`\n", i, output[outputlen - 1][i]);
            }
            if (i != outputstrings) {
                printf("Malformed line: %s %d %p \n", line, i, token);
                continue;
            }
        }
        free(line);
    
        for (size_t i = 0; i < outputlen; ++i) {
            for (size_t j = 0; j < outputstrings; ++j) {
                printf("From line %d the string num %d: `%s`\n", i, j, output[i][j]);
            }
        }
    
        for (size_t i = 0; i < outputlen; ++i) {
            for (size_t j = 0; j < outputstrings; ++j) {
                free(output[i][j]);
            }
            free(output[i]);
        }
        free(output);
    
        return 0;
    }
    

    输入哪个:

    $0,0,0,thisIsFirstString;secondString~thirdOne
    $0,,0,firstString;~thirdOne
    $,,,firstString;~thirdString
    

    产生结果:

    From line 0 the string num 0: `0`
    From line 0 the string num 1: `0`
    From line 0 the string num 2: `0`
    From line 0 the string num 3: `thisIsFirstString`
    From line 0 the string num 4: `secondString`
    From line 0 the string num 5: `thirdOne`
    From line 1 the string num 0: `0`
    From line 1 the string num 1: ``
    From line 1 the string num 2: `0`
    From line 1 the string num 3: `firstString`
    From line 1 the string num 4: ``
    From line 1 the string num 5: `thirdOne`
    From line 2 the string num 0: ``
    From line 2 the string num 1: ``
    From line 2 the string num 2: ``
    From line 2 the string num 3: `firstString`
    From line 2 the string num 4: ``
    From line 2 the string num 5: `thirdStrin`
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-04-10
      • 2014-06-27
      • 1970-01-01
      • 2014-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多