Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров4.8K

#GNSS #CSV #NMEA #URL #IP #MAC

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

1--CSV строчки можно, например, повстречать в NMEA протоколе от навигационных GNSS приемников. Вот пример NMEA протокола

$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*

Поэтому можно сказать, что CSV это космический протокол для приема сигналов со спутников!

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

3-- Также компонент CSV позволит Вам одной строчкой в UART-CLI консоли прошивки распознавать и запускать на исполнение последовательно сразу несколько shell команд.

4--Прошивка может запросто логировать на SD карту по SPI данные в CSV формате как в файл на FatFS. Потом этот текстовый *.csv файлик можно будет открыть на LapTop(е) любым процессором электронных таблиц.

5--IP адрес, MAC адрес - это тоже CSV строчки. Только с со своими разделителями: точка для IP и двоеточие для MAC.

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

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

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

# теста

Строка

индекс

результат

разделитель

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

1

"aaa,bbbb,cccc,,eeeee,fffff"

5

"fffff"

','

6

2

"aaa,bbbb,cccc,,eeeee,fffff"

0

"aaa"

','

6

3

"aaa,bbbb,cccc,,eeeee,fffff"

1

"bbbb"

','

6

8

"asd ghh dff"

1

"ghh "

' '

3

4

"aaa,bbbb,cccc,,eeeee,fffff"

3

""

','

6

8

file.bin

0

"file"

'.'

2

5

","

0

""

','

2

7

"192.168.3.36"

0

"192"

'.'

4

6

"aaa,bbb,ccc,,eee,fff"

6

Error

','

6

При этом API должен быть именно такой

bool csv_parse_text(const char* const text,
                    char separator, 
                    uint32_t index, 
                    char* const sub_str,
                    uint32_t sub_str_size);

где

# аргумента

имя

тип данных

Пояснение

1

text

char*

Входная CSV строка

2

separator

char

символ разделителя

3

index

uint32_t

индекс колонки которую надо извлечь

4

sub_str

char*

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

5

sub_str_size

uint32_t

размер выходного массива

--

error

bool

В случае какой-либо ошибки (например выход за пределы предоставленной для sub_str памяти или неверном индексе) вернуть false.

При этом функция csv_parse_text() не должна менять исходную строку text. Строка text вообще может лежать в NOR-Flash памяти.

Этот код должен быть адаптирован для исполнения на микроконтроллере. То есть никакого динамического выделения памяти а язык написания Си. И крайне желательно написать код в соответствии со стандартом ISO26262-6. Вот, пожалуй, и все требования к программному компоненту CSV decoder(а).

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

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

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

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

№ входа

Пояснение

Токен

1

Символ

NOT_SEP

2

Разделитель

Sep

3

Конец строки

End

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

№ состояния

Пояснение

Токен

1

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

INIT

2

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

ACCUMULATE

3

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

SEP

4

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

END

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

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

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

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

У каждого программного компонента есть свои константы. Вот они для компонента CSV.

#ifndef CSV_CONST_H
#define CSV_CONST_H


#ifndef HAS_CSV
#error "+HAS_CSV"
#endif

typedef enum {
    CSV_STATE_INIT = 1,
    CSV_STATE_ACCUMULATE = 2,
    CSV_STATE_SEP = 3,
    CSV_STATE_END = 4,

    CSV_STATE_UNDEF = 0,
} CsvState_t;

typedef enum {
    CSV_INPUT_NOT_SEP = 1,
    CSV_INPUT_SEP = 2,
    CSV_INPUT_END = 3,

    CSV_INPUT_UNDEF = 0,
} CsvInput_t;

#endif /*CSV_CONST_H*/

У каждого программного компонента есть свои специфические типы данных.

#ifndef CSV_TYPEES_H
#define CSV_TYPEES_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdbool.h>
#include <stdint.h>

#ifndef HAS_CSV
#error "+HAS_CSV"
#endif

#include "csv_const.h"

typedef struct {
    char* out_buff; /*result array*/
    char prev_char;
    char separator;
    bool init_done;
    bool fetch_done;
    char symbol;
    uint32_t out_size;
    uint32_t i;/*char index*/
    int32_t fetch_index; /*starts from 0; -1 if fetch is not needed*/
    uint32_t cnt; /*total number of cells*/
    uint32_t position; /*starts from 0*/
    CsvState_t state;
    CsvInput_t input;
} CsvFsm_t;

#ifdef __cplusplus
}
#endif

#endif /* CSV_TYPEES_H */

А это API программного компонента CSV. Тут основная функция это csv_parse_text()

#ifndef CSV_H
#define CSV_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdbool.h>
#include <stdint.h>

#include "csv_types.h"

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 */

Инициализация конечного автомата разбора CSV


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

        res = true;
    }
    return res;
}


uint32_t csv_cnt(const char* const text, char separator) {
    bool res = false;
    size_t len = strlen(text);
    LOG_DEBUG(CSV, "Text:[%s] Size:%u byte Separator:%c", text, len, separator);

    CsvFsm_t Node;
    csv_init(&Node, separator, -1, NULL, 0);
    uint32_t i = 0;
    for(i = 0; i < len; i++) {
        Node.input = CSV_INPUT_UNDEF;
        res = csv_cnt_proc(&Node, text[i]);
    }
    Node.input = CSV_INPUT_END;
    res = csv_cnt_proc(&Node, 0x00);

    return Node.cnt;
}

Всю работу делает функция 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 Item={0};
        csv_init(&Item, separator, index, out_text, size);
        uint32_t i = 0;
        for(i = 0; i < len; i++) {
            Item.input = CSV_INPUT_UNDEF;
            res = csv_cnt_proc(&Item, in_text[i]);
        }
        Item.input = CSV_INPUT_END;
        res = csv_cnt_proc(&Item, 0x00);

        res = false;
        if(Item.fetch_done ) {
            if(0==Item.error_cnt){
                res = true;
            }
        }

    }
    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_add_letter(CsvFsm_t* const Node, uint32_t index, char letter, bool last_char){
    bool res = true;
    if(Node->position==Node->fetch_index){
        //Node->i = index;
        if(index<Node->out_size){
            Node->out_buff[index] = letter ;//Node->symbol;
            res = true;
            if(last_char){
                Node->fetch_done = last_char;
                LOG_PROTECTED(CSV, "CSV[%u]=[%s]",Node->position, Node->out_buff);
            }
        }else{
            Node->error_cnt++;
            res = false;
        }
    }
    return res;
}


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

        CsvNodeDiag(Node);

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

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

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

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

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

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

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

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

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

static bool csv_cnt_sep_proc(CsvFsm_t* Node) {
    bool res = false;
    //LOG_DEBUG(CSV, "ProcSep %c Input:%s", Node->symbol, CsvInput2Str(Node->input));
    if(Node) {
        switch(Node->input) {
        case CSV_INPUT_NOT_SEP: {
            Node->cnt++;
            res=csv_add_letter(Node, 0,Node->symbol, false);
            Node->state = CSV_STATE_ACCUMULATE;
        } break;
        case CSV_INPUT_SEP: {
            res=csv_add_letter(Node, 0,0, true);
            Node->position++;
            Node->state = CSV_STATE_SEP;
            Node->cnt++;
        } break;
        case CSV_INPUT_END: {
            res=csv_add_letter(Node, 0,0, false);
            Node->state = CSV_STATE_END;
            Node->cnt++;
        } break;
        default:
            break;
        }
    }
    return res;
}

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


static bool csv_cnt_end_proc(CsvFsm_t* Node) {
    bool res = false;
    LOG_DEBUG(CSV, "ProcEnd %c Input:%s", Node->symbol, CsvInput2Str(Node->input));
    if(Node) {
        switch(Node->input) {
        case CSV_INPUT_NOT_SEP: {
            res = false;
            Node->i = 0;
            Node->out_buff[0] = 0x00;
            Node->state = CSV_STATE_END;
        } break;
        case CSV_INPUT_SEP: {
            res = false;
            Node->i = 0;
            Node->out_buff[0] = 0x00;
            Node->state = CSV_STATE_END;
        } break;
        case CSV_INPUT_END: {
            res = false;
            Node->i = 0;
            Node->out_buff[0] = 0x00;
            Node->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

""

"aa;bb;cc"

3

Ошибка

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

Достоинства CVS протокола и данного парсера в частности

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

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

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

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

5--В представленном решении отсутствует нужда в динамическом выделении памяти, что особенно важно в программировании микроконтроллеров.

6--В каждой функции только 1 return. Это как раз соответствует стандарту ISO26262. Значит этот код можно использовать в автомобильной промышленности.

Недостатки CSV

2--В выделяемом CSV тексте не должно быть самого символа разделителя, как данных. Иначе конечный автомат заклинит. Но в 95% случаев это и не нужно! И потом, всегда можно подобрать разделитель для конкретного случая (/,;.|\`*).

Вывод`

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

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

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

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

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

Надеюсь, что этот текст поможет другим программистам микроконтроллеров решать повседневные задачи.

Я был бы признателен за набор тест-case(ов) для проверки данного программного компонента.

Cловарь

Акроним

Расшифровка

CSV

Comma-separated values

URL

Uniform Resource Locator

GNSS

Global Navigation Satellite System

API

application programming interface

NMEA

National Marine Electronics Association

Links, Cсылки

Распознавание Вещественного Числа из Строчки

ISO 26262-6 разбор документа (или как писать безопасный софт) https://habr.com/ru/articles/757216/

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вам приходилось делать синтаксический разбор CSV строчек?
81.4% да35
18.6% нет8
Проголосовали 43 пользователя. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы делали синтаксический разбор NMEA протокола?
22.73% да10
77.27% нет34
Проголосовали 44 пользователя. Воздержался 1 пользователь.
Теги:
Хабы:
Всего голосов 16: ↑5 и ↓11-4
Комментарии133

Публикации

Истории

Работа

Программист С
36 вакансий

Ближайшие события