Удалите конечные пробелы внутри [] с помощью sscanf

Мне нужно проанализировать строки, возвращаемые utmpdump /var/log/wtmp. Они в таком формате:

[8] [13420] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:18:29,836564+00:00]
[7] [13611] [ts/3] [john    ] [pts/3       ] [192.168.1.38        ] [192.168.1.38   ] [2024-07-22T11:21:30,065856+00:00]
[8] [13611] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:21:41,814051+00:00]

Характеристика заключается в том, что:

  • Все столбцы заключены в [],
  • Существует фиксированное количество столбцов в строке,
  • Каждый столбец может содержать текст или быть пустым (только пробелы). На практике не все, но лучше предположить все,
  • Если в столбце есть текст, его длина может различаться (до определенного предела можно использовать префикс, т. е. 64 символа, включая \0),
  • Если в столбце есть текст, он никогда не будет иметь предшествующих пробелов, но часто будет иметь конечные пробелы.

Как разобрать такую ​​строку на 8 переменных char* с помощью sscanf конкретно? Проблема, которую я не могу решить, заключается в том, как обрезать конечные пробелы и в то же время позволить промежуточной строке [] изменять длину. Возможно ли это вообще с sscanf?

У меня есть рабочее решение без sscanf, но это нужно сделать специально с sscanf. Обрезка пробелов после того, как sscanf выполняет синтаксический анализ, является резервным решением, но я пытаюсь сделать это с помощью sscanf полного анализа.

Обновлено: Основываясь на предоставленных ответах, я ищу решение sscanf «1 строка». Этот код дает сбой, если есть пустые столбцы, как указано в комментариях:

#include <stdio.h>

#define BUF_LEN     (64)

int main (void) 
{
    const char* const line = "[8] [13420] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:18:29,836564+00:00]";
    size_t record_num = 0;
    size_t pid = 0;
    char session_type[BUF_LEN] = { 0 };
    char username[BUF_LEN] = { 0 };
    char terminal[BUF_LEN] = { 0 };
    char source_ip[BUF_LEN] = { 0 };
    char dest_ip[BUF_LEN] = { 0 };
    char timestamp[BUF_LEN] = { 0 };

    sscanf(line, "[%zu] [%zu] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ]", 
        &record_num, &pid, session_type, username, terminal, source_ip, dest_ip, timestamp);
    
    printf("Record number: %zu \n", record_num);
    printf("Pid: %zu \n", pid);
    printf("Session type: %s \n", session_type);
    printf("User name: %s \n", username);
    printf("Terminal: %s \n", terminal);
    printf("Source IP: %s \n", source_ip);
    printf("Destination IP: %s \n", dest_ip);
    printf("Timestamp: %s \n", timestamp);

    return 0;
}

Выход:

Record number: 8
Pid: 13420
Session type:
User name:
Terminal:
Source IP:
Destination IP:
Timestamp:

Есть ли способ исправить формат для учета пустых столбцов без необходимости анализировать каждый столбец с помощью sscanf индивидуально?

Обязательно ли это делать с помощью sscanf? Почему? Каково ваше фактическое задание? Каковы его ограничения и требования?

Some programmer dude 25.07.2024 13:59

Вы должны показать, что вы пробовали. Затем ответы можно будет сформулировать на уровне, который вам будет лучше всего понятен.

Fe2O3 25.07.2024 14:03

Я бы рекомендовал написать собственную функцию вместо использования sscanf для чего-то, что не предназначено для простой и полной реализации.

chqrlie 25.07.2024 14:11

У меня есть рабочее решение путем анализа строки вручную. Однако мне сказали, что это можно сделать только с помощью sscanf. У меня нет работающего решения sscanf, поскольку я не знаю, как отформатировать часть синтаксического анализа, поскольку с моей стороны это кажется невозможным с учетом предоставленных требований. Сейчас я пытаюсь выяснить, действительно ли это невозможно или это действительно возможно.

Łukasz Przeniosło 25.07.2024 14:19

Конечно, это можно сделать с помощью sscanf, но это нетривиально. Простой метод «ручного анализа» может быть легче читать, понимать и поддерживать.

Some programmer dude 25.07.2024 14:23

Моя лучшая попытка: ideone.com/1Kq6Ae TIL scanf не любит пустые строки :-)

pmg 25.07.2024 14:24
I do have a working solution by manually parsing the stringСохраняйте. I need to parse lines Почему бы не использовать существующую библиотеку для анализа файла utmp.h? utmpdump написано на C, зачем анализировать вывод utmpdump, почему бы не анализировать файл? (где «разобрать» на самом деле просто fread(struct utmp, вот и все). Глядя на исходники, utmpdump есть функция undump github.com/util-linux/util-linux/blob/master/login-utils/…, которая может быть вам интересна. Наконец, это не вопрос XY? Вас действительно волнует использование именно sscanf?
KamilCuk 25.07.2024 14:36

Похоже, " [%1023[^]]]" делает свое дело (где буфер имеет размер 1024). Просто обрежьте пробелы постфактум.

William Pursell 25.07.2024 14:47

@KamilCuk причина в том, что код построен с использованием другой стандартной библиотеки C, отличной от той, которая используется по умолчанию для целевой системы. Было проще просто проанализировать текст, поскольку упомянутый вами подход я пробовал вначале и не смог прочитать какие-либо данные. Кроме того, что касается XY, мне специально сказали, что это можно сделать с помощью просто sscanf - я просто хочу доказать, что это не так (или, если это так, узнайте, как это сделать).

Łukasz Przeniosło 25.07.2024 14:54

@WilliamPursell Я знаю, что после вызова sscanf можно обрезать пробелы. Речь идет конкретно о том, чтобы доказать, возможно ли это с помощью одного вызова sscanf или нет...

Łukasz Przeniosło 25.07.2024 14:56

Формат " [ %N[^]]]" с N — максимальный размер поля, включая \0, может читать каждую строку и удалять пробелы в заголовках. Но удалить конечные пробелы невозможно в sscanf(), необходим еще один шаг по их удалению.

dalfaB 25.07.2024 14:56

@dalfaB я тоже это узнал. Спасибо.

Łukasz Przeniosło 25.07.2024 14:58

Если строка не содержит пробелов, " [%1023[^] ] ]" удалит конечные пробелы. %1023[^] ] читается до тех пор, пока не встретится пробел или ]. Тогда следующий ` ]` будет фильтровать любые пробелы и соответствовать последнему ].

Weather Vane 25.07.2024 15:23

@WeatherVane, хорошо, я думаю, что в строках не должно быть пробелов. Попробую это, спасибо.

Łukasz Przeniosło 25.07.2024 15:25

@WeatherVane: предлагаемый вами формат не работает для полей, содержащих только пробелы. %1023[^] ] завершится ошибкой, а остальная часть строки формата ` ]` будет проигнорирована. Именно по этой причине индивидуальный подход предпочтительнее.

chqrlie 25.07.2024 15:27

@chqrlie в моем тесте входная строка " [ ] " выдает на выходе "". Сканирование останавливается на первом замкнутом пространстве, затем фильтруется остальное.

Weather Vane 25.07.2024 15:28

@chqrlie Понятно, преобразование не выполняется. Если входные данные — " [ ] [abc ]", а строка формата содержит то, что я написал дважды, например sscanf(str, " [%1023[^] ] ] [%1023[^] ] ]", то второе значение также не преобразуется.

Weather Vane 25.07.2024 15:35

@dalfaB «Но удаление конечных пробелов невозможно в sscanf()» --> Коду не нужно их удалять, просто просканируйте их.

chux - Reinstate Monica 25.07.2024 16:36

Сделайте шаг назад и подумайте, как [s]scanf() обрабатывает свой спецификатор формата для строки. Он (обычно) начинается с отбрасывания начальных пробелов, а затем по одному символу сопоставляется со спецификатором. В вашей проблеме вы хотите, чтобы он прекратил «потреблять» и назначал либо (конечный) SP, либо ']', а затем, при необходимости, потреблял больше SP, пока не будет найден определенный ']'. Несмотря на свои возможности, функция на это не способна. «Один экземпляр scanf()» не даст вам желаемых результатов. Тот, кто сказал вам, что это возможно, ошибся (или использовал альтернативное значение слова «один»).

Fe2O3 26.07.2024 09:15
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
19
110
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Используйте "%n", чтобы определить смещение частей сканирования.

Используйте " ", чтобы просмотреть необязательные пробелы.

*scanf() не любит сканировать/формировать строки нулевой длины, поэтому при сканировании столбца рассматривайте [ как часть строки, чтобы гарантировать наличие хотя бы 1 символа в строке. Позже начните поле, которое прошло '['.

#include <stdio.h>
#define FN 8

char *cut_up_line(char *offset[FN], char * line) {
  for (int i = 0; i < FN; i++) {
    int field_end;
    int column_end = 0;
    // If possible for the line to include unusually white-spaces,
    // adjust format to include them: "%*[^] \t\r\v\f]%n ] %n"
    sscanf(line, "%*[^] \t]%n ] %n", &field_end, &column_end);
    if (column_end == 0) {
      // debug: printf("-%d <%s>\n", i, line);
      return NULL;
    }
    // Maybe add test here that *line is a '['.
    offset[i] = line + 1;
    line[field_end] = '\0';
    line += column_end;
    // debug: printf("+%d <%s>\n", i, offset[i]);
  }
  return line;
}


void test(char *line) {
  printf("<%s>\n", line);
  char *field[FN];
  if (cut_up_line(field, line) == NULL) {
    printf("** Failed **\n");
  } else {
    for (int i = 0; i < FN; i++) {
      printf("  %d:<%s>\n", i, field[i]);
    }
  }
}


int main() {
  char s1[] = "[8] [13420] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:18:29,836564+00:00]";
  char s2[] = "[7] [13611] [ts/3] [john    ] [pts/3       ] [192.168.1.38        ] [192.168.1.38   ] [2024-07-22T11:21:30,065856+00:00]";
  char s3[] = "[8] [13611] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:21:41,814051+00:00]";

  test(s1);
  test(s2);
  test(s3);
}

Выход

<[8] [13420] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:18:29,836564+00:00]>
  0:<8>
  1:<13420>
  2:<>
  3:<>
  4:<pts/3>
  5:<>
  6:<0.0.0.0>
  7:<2024-07-22T11:18:29,836564+00:00>
<[7] [13611] [ts/3] [john    ] [pts/3       ] [192.168.1.38        ] [192.168.1.38   ] [2024-07-22T11:21:30,065856+00:00]>
  0:<7>
  1:<13611>
  2:<ts/3>
  3:<john>
  4:<pts/3>
  5:<192.168.1.38>
  6:<192.168.1.38>
  7:<2024-07-22T11:21:30,065856+00:00>
<[8] [13611] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:21:41,814051+00:00]>
  0:<8>
  1:<13611>
  2:<>
  3:<>
  4:<pts/3>
  5:<>
  6:<0.0.0.0>
  7:<2024-07-22T11:21:41,814051+00:00>
  • Некоторые форматы заслуживают разделения для ясности.
#define FMT_ALL_BUT_RBRACKET_WSPACE "%*[^] \t\r\n\f\v]"
...
sscanf(line, FMT_ALL_BUT_RBRACKET_WSPACE "%n ] %n", &field_end, &column_end);
  • Этот код использует то, что line, скорее всего, доступен для записи, поэтому токенизированные поля сохраняются в line путем добавления нулевых символов. Это позволяет избежать проблем с переполнением буфера.

UV за смелость написать это, когда strtok() и некоторые друзья (или одно из нескольких других решений) выполнили бы работу без сложностей с псевдорегулярными выражениями... Ура! :-)

Fe2O3 25.07.2024 16:41

Основываясь на вашем решении, можно ли сделать лайнер sscanf` для одновременного анализа всех 8 переменных?

Łukasz Przeniosło 26.07.2024 07:53

@ŁukaszPrzeniosło Да. Повторите "%*[^] \t]%n ] " 8 раз. Хотя это не элегантно.

chux - Reinstate Monica 26.07.2024 10:22

@chux-ReinstateMonica это анализируется неправильно. Предполагая, что первые два элемента — это size_t, имели ли вы в виду этот синтаксис? [%zu] [%zu] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ]

Łukasz Przeniosło 26.07.2024 11:36

@ŁukaszPrzeniosło, формат, который я имею в виду в этом ответе, определяет смещения в строке (int) начала поля и его конца. Чтобы получить начальное смещение минус 1 и конечное смещение, используйте "%n%*[^] \t]%n ] " 8 раз. Почему вы хотите использовать один вызов sscanf(), а не цикл?

chux - Reinstate Monica 26.07.2024 19:01

@chux-ReinstateMonica Для простоты я хотел использовать 1 вызов. Теперь я понял вашу идею, спасибо.

Łukasz Przeniosło 26.07.2024 23:48

@ŁukaszPrzeniosło "для простоты используйте 1 вызов." --> Цикл становится более понятным, его легче писать правильно и его легче поддерживать. Удачи.

chux - Reinstate Monica 26.07.2024 23:51

@chux-ReinstateMonica В целом я согласен, но если идти по этому пути, становится еще более очевидным, что несколько лучше анализировать его вручную.

Łukasz Przeniosło 27.07.2024 09:35

Вот простой пользовательский парсер для этого формата:

#include <stdio.h>

// split the line and populate `fields` with pointers to trimmed contents.
int split_log(char *fields[], int n, char *line) {
    char *p = line;
    char *tail;
    int i;
    for (i = 0; i < n; i++) {
        while (*p == ' ')
            p++;
        if (*p != '[')
            break;
        p++;
        fields[i] = p;
        for (tail = p; *p && *p != ']'; p++) {
            if (*p != ' ')
                tail = p + 1;
        }
        if (*p != ']')
            break;
        *tail = '\0';
        p++;
    }
    return i;
}

void print_log(char *fields[], int n) {
    for (int i = 0; i < n; i++)
        printf("field%d = '%s'\n", i + 1, fields[i]);
    printf("\n");
}

int test_split(char *line, int n) {
    char *fields[8];
    int n1 = split_log(fields, n, line);
    printf("split_log found %d fields:\n", n1);
    print_log(fields, n1);
    return n == n1;
}

int main(void) {
    char line1[] = "[8] [13420] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:18:29,836564+00:00]\n";
    char line2[] = "[7] [13611] [ts/3] [john    ] [pts/3       ] [192.168.1.38        ] [192.168.1.38   ] [2024-07-22T11:21:30,065856+00:00]\n";
    char line3[] = "[8] [13611] [    ] [        ] [pts/3       ] [                    ] [0.0.0.0        ] [2024-07-22T11:21:41,814051+00:00]\n";

    test_split(line1, 8);
    test_split(line2, 8);
    test_split(line3, 8);
    return 0;
}

Выход:

split_log found 8 fields:
field1 = '8'
field2 = '13420'
field3 = ''
field4 = ''
field5 = 'pts/3'
field6 = ''
field7 = '0.0.0.0'
field8 = '2024-07-22T11:18:29,836564+00:00'

split_log found 8 fields:
field1 = '7'
field2 = '13611'
field3 = 'ts/3'
field4 = 'john'
field5 = 'pts/3'
field6 = '192.168.1.38'
field7 = '192.168.1.38'
field8 = '2024-07-22T11:21:30,065856+00:00'

split_log found 8 fields:
field1 = '8'
field2 = '13611'
field3 = ''
field4 = ''
field5 = 'pts/3'
field6 = ''
field7 = '0.0.0.0'
field8 = '2024-07-22T11:21:41,814051+00:00'

Как разобрать такую ​​строку на 8 переменных char * с помощью sscanf конкретно?

Предполагая, что все поля содержат хотя бы один символ, возможно, пробел, вышеуказанную функцию можно реализовать с помощью sscanf для большей части работы, но я боюсь, что код менее читабелен и его труднее понять:

// split the line and populate `fields` with pointers to trimmed contents.
int split_log(char *fields[], int n, char *line) {
    char *p = line;
    char open[2], close[2];
    int i, begin, end;
    for (i = 0; i < n; i++) {
        if (sscanf(p, " %1[[]%n%*[^]]%n%1[]]", open, &begin, &end, close) != 2)
            break;
        fields[i] = p + begin;
        while (p[end - 1] == ' ')
            end--;
        p[end] = '\0';
        p += end + 1;
    }
    return i;
}

Проблема, которую я не могу решить, заключается в том, как обрезать конечные пробелы и в то же время позволить промежуточной строке [] изменять длину. Возможно ли это вообще с sscanf?

Как видно из приведенного выше кода, обрезка по-прежнему выполняется с помощью специального кода. Не существует простого способа обрезать конечные пробелы с помощью sscanf, однако можно использовать sscanf запутанным способом, чтобы избежать ручной проверки содержимого буфера и, таким образом, еще больше запутать код:

        // while (p[end - 1] == ' ')
        //    end--;
        while (sscanf(p + end - 1, "%1[ ]", open))
            end--;

Однако если содержимое поля содержит не более одного слова, вы можете использовать sscanf для вычисления длины этого слова:

int split_log(char *fields[], int n, char *line) {
    char *p = line;
    char open[2], close[2];
    int i, begin, end, len;
    for (i = 0; i < n; i++) {
        if (sscanf(p, " %1[[]%n%*[^]]%n%1[]]", open, &begin, &end, close) != 2)
            break;
        fields[i] = p + begin;
        p[end] = '\0';
        len = 0;
        sscanf(p + begin, "%*s%n", &len);
        p[begin + len] = '\0';
        p += end + 1;
    }
    return i;
}

sscanf — мощный инструмент, но полный особенностей и подводных камней. Его легко использовать неправильно даже для простых задач синтаксического анализа. В таких случаях написание собственного кода — более безопасный способ получить правильные результаты.

Другие вопросы по теме