sscanf — анализировать фрейм с необязательными/пустыми спецификаторами формата

Я пытаюсь разобрать кадры, отформатированные по следующей схеме:

$[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, что некоторые параметры могут отсутствовать во фрейме и в этом случае они будут отброшены?


person Arkaik    schedule 14.06.2018    source источник
comment
Мой совет: не используйте sscanf, если ввод потенциально отличается от того, что вы ожидаете, а проанализируйте строку самостоятельно.   -  person Jabberwocky    schedule 14.06.2018
comment
Такой разбор действительно не является одной из сильных сторон scanf и семьи. Возможно, вы захотите рассмотреть возможность использования других методов, таких как токенизация с помощью strtok или возможно, даже регулярные выражения.   -  person Some programmer dude    schedule 14.06.2018
comment
Имеют ли необязательные числа значение по умолчанию, если они не указаны или что-то в этом роде?   -  person J...S    schedule 14.06.2018
comment
string состоит только из букв?   -  person chux - Reinstate Monica    schedule 14.06.2018
comment
@J...S Нет, необязательные числа (то же самое для строк) либо указаны, либо отсутствуют (пустое поле).   -  person Arkaik    schedule 14.06.2018
comment
Что должно произойти, если проанализированная длина string превысит 99?   -  person chux - Reinstate Monica    schedule 14.06.2018
comment
@chux Нет, строковые поля могут содержать любой символ ascii, включая цифры. И я мог бы добавить условия длины, но я знаю, что строки не могут превышать 20 байтов, поэтому в этом случае это не проблема, даже если бы это могло быть.   -  person Arkaik    schedule 14.06.2018
comment
Тогда как узнать, когда заканчивается string? любой символ ASCII включает '\0', так что это, безусловно, слишком широко. С <string>; это любой символ, кроме ;. Являются ли пробелы частью str1 и str3?   -  person chux - Reinstate Monica    schedule 14.06.2018
comment
@chux На самом деле я также знаю, что в строке не будет символа '\ 0', кроме как в конце кадра, также не будет никакого разделителя (',' или ';' или '~' ) внутри строки.   -  person Arkaik    schedule 14.06.2018


Ответы (3)


Как уже говорили другие, функции семейства 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);

где 9 на единицу меньше длины массива символов num2.

И в вашей программе sscanf() фактически прекращает присваивать значения переменным, если часть %[^,] соответствует пустой строке.

person J...S    schedule 14.06.2018
comment
Второй , в формате sscanf(ptr, "%[^,],", num2)==1 не служит никакой функциональной цели. Кроме того, если ptr заканчивается без ,, этот код будет индексировать конец строки с ptr+=1;. Возможно, это часть проверки возможных ошибок, которая для краткости опущена. - person chux - Reinstate Monica; 14.06.2018
comment
@chux Вы имеете в виду, что если часть строки, на которую указывает ptr, соответствующая %[^,], является пустой строкой, sscanf() перестанет назначаться, и после этого запятая все равно не будет учитываться? - person J...S; 14.06.2018
comment
sscanf(ptr, "%[^,],", num2)==1 — это проверка, было ли что-то записано в num2. Тем не менее, это не гарантирует, что за ним последовало ,. Рассмотрим эффект if(sscanf("abc", "%[^,],", num2)==1) { ptr+=strlen(num2); } ptr+=1;. Куда теперь указывает ptr? - person chux - Reinstate Monica; 14.06.2018
comment
@chux Я вижу, что если запятая отсутствует, она выйдет из-под контроля, но если ввод хорошо отформатирован и есть запятая, sscanf() перестает назначаться, если %[^,] соответствует пустой строке? - person J...S; 14.06.2018
comment
sscanf(",","%[^,],", num2) не должен возвращать 1. Таким образом, этот тест обрабатывает правильно сформированный ввод. ИМХО, отсутствие обработки ошибок, по-видимому, нормальное для OP, остается слабым местом для общей применимости этого подхода. - person chux - Reinstate Monica; 14.06.2018
comment
@chux Отсутствие методов обработки ошибок действительно является проблемой для семейства scanf(). Но строка формата sscanf() в вашем последнем комментарии выглядит странно. Может в нем ошибка? Или это просто что-то, чего я не знаю? - person J...S; 14.06.2018
comment
Давайте продолжим это обсуждение в чате. - person chux - Reinstate Monica; 14.06.2018
comment
sscanf() сам по себе не является проблемой ошибки, это такие проблемы, как if(sscanf(abc, %[^,],,, num2)==1) { ptr+=strlen(num2); } птр+=1; и если (sscanf (abc (200 символов) xyz,, %[^,],, num2). При правильно сформированном вводе ваш ответ работает нормально, но мне трудно использовать его в качестве основы с хорошей обработкой ошибок. - person chux - Reinstate Monica; 14.06.2018

Думаю, лучше всего написать собственную функцию парсера:

#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`
person KamilCuk    schedule 14.06.2018

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;
}
person chqrlie    schedule 14.06.2018