Синтаксический разбор CSV строчек

#GNSS #CSV #NMEA #URL

c908285962af4f4bdf22df645b27b089.png

В программировании микроконтроллеров часто надо производить синтаксический разбор (парсинг) CSV строчек. CSV это просто последовательность символов который разделены запятой (или любым другим одиночным символом: ; | /). CSV строчки можно, например, повстречать в NMEA протоколе от навигационных GNSS приемников.

$GNGGA,102030.000,5546.95900,N,03740.69200,E,1,08,2.0,142.0,M,0.0,M,,*
$GNGLL,5546.95900,N,03740.69200,E,102030.000,A,A*
$GNGSA,A,3,10,16,18,20,26,27,,,,,,,4.8,2.0,4.3,1*
$GNGSA,A,3,19,  ,  ,  ,  ,  ,,,,,,,4.8,2.0,4.3,4*
$GNGSA,A,3,82,  ,  ,  ,  ,  ,,,,,,,4.8,2.0,4.3,2*
$GPGSV,3,1,12,07,08,343,,08,07,304,,10,28,195,42,13,20,054,,0*
$GPGSV,3,2,12,15,27,087,,16,47,262,39,18,66,082,23,20,58,174,23,0*
$GPGSV,3,3,12,21,75,089,23,26,33,222,31,27,38,298,40,29,15,127,,0*
$BDGSV,1,1,01,19,29,174,28,0*
$GLGSV,3,1,09,74,08,001,34,66,55,096,,82,69,318,21,73,25,326,,0*
$GLGSV,3,2,09,80,20,258,,65,18,025,,83,21,292,,81,51,092,,0*
$GLGSV,3,3,09,67,26,161,,0*
$GNRMC,102030.000,A,5546.95900,N,03740.69200,E,0.12,49.75,200220,,,A,V*
$GNVTG,49.75,T,,M,0.12,N,0.22,K,A*
$GNZDA,102030.000,20,02,2020,00,00*
$GPTXT,01,01,01,ANTENNA OK*
$GNDHV,102030.000,0.03,0.000,0.000,0.000,0.00,,,,,M*
$GNGST,102030.000,6.9,,,,5.6,9.2,10.1*
$GPTXT,01,01,02,MS=7,7,061A8200,33,0,00000000,20,2,00028000*

Любой URL (например этот https://habr.com/ru/article/edit/765066/) это, в сущности, та же самая CSV строчка, где разделитель это /.

Постановка задачи

Дана строка, например «aaa, bbbb, cccc, eeeee, fffff». Также дан индекс элемента в виде положительного целого числа. Например число два. Вернуть подстроку, которая соответствует индексу. Отсчет начитать от нуля. В данном случае результат это «сссс».

Вот несколько тестовых случаев. В данном случае в качестве разделителя служит запятая.

#

Строка

индекс

результат

количество элементов

1

«aaa, bbbb, cccc, eeeee, fffff»

5

»

6

2

«aaa, bbbb, cccc, eeeee, fffff»

0

«aaa»

6

3

«aaa, bbbb, cccc, eeeee, fffff»

1

«bbbb»

6

4

«aaa, bbbb, cccc, eeeee, fffff»

3

»

6

5

»,»

0

»

2

Для человека тут всё очевидно. Однако как заставить компьютерную программу выделять подстроки из CSV строчек?

Как же извлекать текст из CSV строчек?

Можно сказать, что СSV это своеобразный текстовый протокол для упаковки переменных в пакет. Как это обычно и бывает в программировании все задачи решаются золотым шаблоном: конечным автоматом. Надо спроектировать конечный автомат.

Фаза 1. Определить входы конечного автомата.

№ входа

Пояснение

Токен

1

Символ

NOT_SEP

2

Разделитель

Sep

3

Конец строки

End

Фаза 2. Определить состояния конечного автомата.

№ состояния

Пояснение

Токен

1

Конечный автомат только проинициализирован

INIT

2

Накапливаем данные в ячейку

ACCUMULATE

3

обнаружен разделитель

SEP

4

Работа выполнена

END

Фаза 3. Нарисовать граф переходов между состояниями.

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

88764d3f47c1871544e46196b1db06b9.png

Теперь у нас есть всё чтобы начать писать код.

Фаза 4. Написать программный код для парсера CSV строчек.

типы данных

#ifndef CSV_TYPEES_H
#define CSV_TYPEES_H

#ifdef __cplusplus
extern "C" {
#endif

#include 
#include 

#ifndef HAS_CSV
#error "+HAS_CSV"
#endif

#include "csv_const.h"

typedef struct {
    char* out_buff;
    char prev_char;
    char separator;
    bool init_done;
    char symbol;
    uint32_t out_size;

    char temp[CSV_VAL_MAX_SIZE];
    uint32_t i;
    int32_t fetch_index; /*-1 if fetch is not needed*/
    uint32_t cnt;
    CsvState_t state;
    CsvInput_t input;
} CsvFsm_t;

#ifdef __cplusplus
}
#endif

#endif /* CSV_TYPEES_H */

API. Тут основная функция это csv_parse_text ()

#ifndef CSV_H
#define CSV_H

#ifdef __cplusplus
extern "C" {
#endif

#include 
#include 

#include "csv_types.h"

#ifndef HAS_CSV
#error "+HAS_CSV"
#endif

uint32_t csv_cnt(const char* const text, char separator);
bool csv_parse_text(const char* const in_text, 
                    char separator, 
                    uint32_t index, 
                    char* const text,
                    uint32_t size);
  

#ifdef __cplusplus
}
#endif

#endif /* CSV_H */

инициализация

static bool csv_init(CsvFsm_t* CsvFsm, char separator, 
                     int32_t fetch_index, 
                     char* const out_text, uint32_t size) {
    bool res = false;
    LOG_DEBUG(CSV, "Init: Separator:%c", separator);
    if(CsvFsm) {
        CsvFsm->separator = separator;
        CsvFsm->init_done = true;
        CsvFsm->prev_char = 0x00;
        CsvFsm->state = CSV_STATE_INIT;
        CsvFsm->input = CSV_INPUT_UNDEF;
        CsvFsm->cnt = 0;
        CsvFsm->i = 0;
        CsvFsm->out_buff = out_text;
        CsvFsm->out_size = size;
        CsvFsm->fetch_index = fetch_index;
        LOG_DEBUG(CSV, "ValMaxSize:%u byte FetchIndex: %d", 
                  sizeof(CsvFsm->temp), fetch_index);
        memset(CsvFsm->temp, 0, sizeof(CsvFsm->temp));
        res = true;
    }
    return res;
}

Всю работу делает функция это csv_parse_text ()

bool csv_parse_text(const char* const in_text, char separator,
                    uint32_t index, char* const out_text, uint32_t size) {
    bool res = false;
    if(in_text && out_text) {
        size_t len = strlen(in_text);
        LOG_DEBUG(CSV, "InText:[%s] Size:%u Sep:%c Index:%u", in_text,
                  len, separator, index);

        CsvFsm_t CsvFsm;
        csv_init(&CsvFsm, separator, index, out_text, size);
        uint32_t i = 0;
        for(i = 0; i < len; i++) {
            CsvFsm.input = CSV_INPUT_UNDEF;
            res = csv_cnt_proc(&CsvFsm, in_text[i]);
        }
        CsvFsm.input = CSV_INPUT_END;
        res = csv_cnt_proc(&CsvFsm, 0x00);

    }
    return res;
}

прокручивает шестерни конечного автомата csv_cnt_proc

static CsvInput_t csv_symbol_2_input(CsvFsm_t* CsvFsm,
                                     char symbol) {
    if(symbol == CsvFsm->separator) {
        CsvFsm->input = CSV_INPUT_SEP;
    } else {
        CsvFsm->input = CSV_INPUT_NOT_SEP;
    }
    return CsvFsm->input;
}

static bool csv_cnt_proc(CsvFsm_t* CsvFsm, char symbol) {
    bool res = false;
    if(CsvFsm) {
        CsvFsm->symbol = symbol;
        if(CSV_INPUT_UNDEF == CsvFsm->input) {
            csv_symbol_2_input(CsvFsm, symbol);
        }

        switch(CsvFsm->state) {
        case CSV_STATE_INIT:
            res = csv_cnt_init_proc(CsvFsm);
            break;

        case CSV_STATE_ACCUMULATE:
            res = csv_cnt_acc_proc(CsvFsm);
            break;

        case CSV_STATE_SEP:
            res = csv_cnt_sep_proc(CsvFsm);
            break;

        case CSV_STATE_END:
            res = csv_cnt_end_proc(CsvFsm);
            break;
        default:
            break;
        }
        CsvFsm->prev_char = symbol;
    }
    return res;
}

Обработчик входов в состоянии сразу после инициализации

static bool csv_cnt_init_proc(CsvFsm_t* CsvFsm) {
    bool res = false;
    LOG_DEBUG(CSV, "ProcInit %c Input:%s", CsvFsm->symbol,
              CsvInput2Str(CsvFsm->input));
    if(CsvFsm) {
        switch(CsvFsm->input) {
        case CSV_INPUT_NOT_SEP: {
            CsvFsm->cnt++;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = CsvFsm->symbol;
            res = true;
            CsvFsm->state = CSV_STATE_ACCUMULATE;
        } break;
        case CSV_INPUT_SEP: {
            CsvFsm->cnt++;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0;
            LOG_PROTECTED(CSV, "CSV[0]=[]");
            csv_proc_fetch_value(CsvFsm, 0);
            res = true;
            CsvFsm->state = CSV_STATE_SEP;
        } break;
        case CSV_INPUT_END: {
            CsvFsm->state = CSV_STATE_END;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0;
            LOG_PROTECTED(CSV, "CSV[0]=[]");
            csv_proc_fetch_value(CsvFsm, 0);
            CsvFsm->cnt++;
            res = true;
        } break;
        default:
            break;
        }
    }
    return res;
}

Обработчик состояния аккумулятора

static bool csv_cnt_acc_proc(CsvFsm_t* CsvFsm) {
    bool res = false;
    LOG_DEBUG(CSV, "ProcAcc %c Input:%s", CsvFsm->symbol,
              CsvInput2Str(CsvFsm->input));
    if(CsvFsm) {
        switch(CsvFsm->input) {
        case CSV_INPUT_NOT_SEP: {
            /*SaveChar*/
            res = true;
            CsvFsm->state = CSV_STATE_ACCUMULATE;
            CsvFsm->i++;
            CsvFsm->temp[CsvFsm->i] = CsvFsm->symbol;
        } break;
        case CSV_INPUT_SEP: {
            res = true;
            CsvFsm->i++;
            CsvFsm->temp[CsvFsm->i] = 0x00;
            LOG_PROTECTED(CSV, "CSV[%u]=[%s]", CsvFsm->cnt - 1, CsvFsm->temp);
            csv_proc_fetch_value(CsvFsm, CsvFsm->cnt - 1);
            CsvFsm->i = 0;
            CsvFsm->state = CSV_STATE_SEP;
        } break;
        case CSV_INPUT_END: {
            res = true;
            CsvFsm->state = CSV_STATE_END;
            CsvFsm->i++;
            CsvFsm->temp[CsvFsm->i] = 0x00;
            LOG_PROTECTED(CSV, "CSV[%u]=[%s]", CsvFsm->cnt - 1, CsvFsm->temp);
            csv_proc_fetch_value(CsvFsm, CsvFsm->cnt - 1);
        } break;
        default:
            res = false;
            break;
        }
    }
    return res;
}

обработчик из состояния, когда уже был принят разделитель

static bool csv_cnt_sep_proc(CsvFsm_t* CsvFsm) {
    bool res = false;
    LOG_DEBUG(CSV, "ProcSep %c Input:%s", CsvFsm->symbol,
              CsvInput2Str(CsvFsm->input));
    if(CsvFsm) {
        switch(CsvFsm->input) {
        case CSV_INPUT_NOT_SEP: {
            CsvFsm->cnt++;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = CsvFsm->symbol;
            res = true;
            CsvFsm->state = CSV_STATE_ACCUMULATE;
        } break;
        case CSV_INPUT_SEP: {
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0x00;
            LOG_PROTECTED(CSV, "CSV[%u]=[]", CsvFsm->cnt);
            csv_proc_fetch_value(CsvFsm, CsvFsm->cnt);
            res = true;
            CsvFsm->state = CSV_STATE_SEP;
            CsvFsm->cnt++;
        } break;
        case CSV_INPUT_END: {
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0x00;
            CsvFsm->state = CSV_STATE_END;
            LOG_PROTECTED(CSV, "CSV[%u]=[]", CsvFsm->cnt);
            csv_proc_fetch_value(CsvFsm, CsvFsm->cnt);
            CsvFsm->cnt++;
            res = true;
        } break;
        default:
            break;
        }
    }
    return res;
}

и обработчик для состояния завершения обработки строки

static bool csv_cnt_end_proc(CsvFsm_t* CsvFsm) {
    bool res = false;
    LOG_DEBUG(CSV, "ProcEnd %c Input:%s", CsvFsm->symbol,
              CsvInput2Str(CsvFsm->input));
    if(CsvFsm) {
        switch(CsvFsm->input) {
        case CSV_INPUT_NOT_SEP: {
            res = false;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0x00;
            CsvFsm->state = CSV_STATE_END;
        } break;
        case CSV_INPUT_SEP: {
            res = false;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0x00;
            CsvFsm->state = CSV_STATE_END;
        } break;
        case CSV_INPUT_END: {
            res = false;
            CsvFsm->i = 0;
            CsvFsm->temp[0] = 0x00;
            CsvFsm->state = CSV_STATE_END;
        } break;
        default:
            break;
        }
    }
    return res;
}

Фаза 5. Тестирование.

Каждый программный компонент надо тестировать. Вот набор тестовых случаев которые должны отрабатывать.

CSV строчка

индекс

ожидаемый результат

»3975, 1.667, 27.50, 21:20:36, 7/8/2023, 1520045092»

5

1520045092

»4452,0.000,17.00, 00:00:18, 14/7/2023, 1517894674»

1

0.000

«ll wm8731 debug; ll i2c debug; tsr 127»

0

ll wm8731 debug

»

0

»

Сборка и прогон этого кода через модульные тесты на PC показали, что всё работает!

3af21240ea36ca19b5467329dd831147.png

Достоинства CVS протокола

1--Простота извлечения данных по индексу.

2--Человеко-читаемость CVS строчек.

3--Совместим с программами обработки электронных таблиц. Текстовый CSV файл можно загрузить в Google SpreadSheets или Excel.

4--Благодаря этому конечному автомат, СSV строки можно разбирать в потоковое режиме.

Недостатки CSV

2--В передаваемом тексте не должно быть самого символа разделителя как данных. Иначе конечный автомат заклинит.

Вывод

Как видите, написать надежный парсер CSV строчек не такая уж и тривиальная задача. Надо спроектировать конечный автомат на 4 состояния, подготовить достаточное количество тестов.

При этом даже наличие текста программы на С не всегда достаточно для понимания работы кода.

Исходник это не код исходник это документация.

Вот Вы и научились выделять подстроки из строк. Далее можно применять распознавание числа из строки. Про это у меня есть отдельный текст:

Распознавание Вещественного Числа из Строчки https://habr.com/ru/articles/757122/

Cловарь

Акроним

Расшифровка

CSV

Comma-separated values

URL

Uniform Resource Locator

NMEA

National Marine Electronics Association

Links, Cсылки

Распознавание Вещественного Числа из Строчки https://habr.com/ru/articles/757122/

© Habrahabr.ru