Синтаксический разбор CSV строчек
#GNSS #CSV #NMEA #URL
В программировании микроконтроллеров часто надо производить синтаксический разбор (парсинг) 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. Нарисовать граф переходов между состояниями.
Взаимосвязь между состояниями и входными воздействиями показывает граф конечного автомата. Это простой планарный граф.
Теперь у нас есть всё чтобы начать писать код.
Фаза 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 показали, что всё работает!
Достоинства 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/