rtthread操作系统libcsv库的使用
前言
最近做一个 STM32F4 的项目,需要做本地数据持久化。一开始的策略是,使用数据产生的时间戳作为文件名,保存为json格式的数据文件来进行存储,使用cJSON库进行数据解析,每个文件大小大概为100字节左右。该方法在数据量小的时候很方便,一次展示30条历史数据,从文件系统读取时几乎感觉不到卡顿。但是当数据量大到将近2000个文件时,一次读取30条数据耗时超过30秒,因此该方法不可采用。
通过debug发现,读取30条数据过程中,读取文件内容耗时可以忽略不记,主要是文件打开耗时严重。因此改变策略,将所有数据存储到一个大文件中。 使用 csv格式 代替 json格式 来进行数据存储,使用libcsv库进行数据解析,json只用来做配置文件。
开发环境
IDE: rt-thread studio v2.0.1
主芯片:STM32F407VG
软件包配置
完成基础工程配置后,在软件包中找到 libcsv 库,勾选后保存生成代码。(必须先完成文件系统配置,并挂载完成,该步骤不做赘述)
打开工程目录 packages/libcsv-v3.0.3/ 下的 libcsv.h
一般情况下,我们只需要用到红框框出的4个函数。
csv_init
顾名思义,对 csv_parser 结构体进行初始化。
参数 p,好理解,接收需要初始化的 csv_parser 结构体地址
参数 options 共有如下几种定义:
谷歌翻译一下:
#define CSV_STRICT 1 / *启用严格模式* /
#define CSV_REPALL_NL 2 / *报告所有未引用的回车和换行* /
#define CSV_STRICT_FINI 4 / *如果引用了最后一个字段且不包含结尾的引号,则使csv_fini返回CSV_EPARSE * /
#define CSV_APPEND_NULL 8 / *确保所有字段均以空值结尾* /
#define CSV_EMPTY_IS_NULL 16 / *当遇到空的,未引用的字段时,将空指针传递给cb1函数* /
返回值: 如果初始化成功,返回 0,初始化失败返回 -1.
int csv_init(struct csv_parser *p, unsigned char options)
{
/* Initialize a csv_parser object returns 0 on success, -1 on error */
if (p == NULL)
return -1;
p->entry_buf = NULL;
p->pstate = ROW_NOT_BEGUN;
p->quoted = 0;
p->spaces = 0;
p->entry_pos = 0;
p->entry_size = 0;
p->status = 0;
p->options = options;
p->quote_char = CSV_QUOTE;
p->delim_char = CSV_COMMA;
p->is_space = NULL;
p->is_term = NULL;
p->blk_size = MEM_BLK_SIZE;
p->malloc_func = NULL;
p->realloc_func = realloc;
p->free_func = free;
return 0;
}
查看源码,只要 p 不为空值都可以初始化成功。
csv_fini
查看函数注释:完成解析。 例如,当文件不以换行符结尾时需要。
该函数通常在整个文件读取完成后调用一次,防止csv文件最后一行数据末尾未加换行符。
参数 p,csv_parser 结构体地址,不做解释。
后面三个参数,cb1, cb2, data 在后面 csv_parse中含义一致,后面一并解释。
csv_free
用于在解析完成后,释放 csv_parser 开辟的堆区空间。
csv_parse
解析csv格式的数据。
参数 p , csv_parser 结构体地址
参数 s , csv格式数据地址
参数 len , 本次解析传入数据长度
参数 cb1, 读取到数据列分隔符后调用的回调函数(分隔符默认为英文半角逗号 ‘ , ’)
参数 cb2, 读取到数据行分隔符后调用的回调函数(分隔符默认为换行符 ‘ \n ’)
参数 data, 传入回调函数 cb1 和 cb2 的最后一个参数
回调函数 cb1 的参数解释:
第一个 (void *) 指该列数据的地址
第二个 size_t 指该列数据的长度
第三个 (void *) 指 csv_parse 最后一个参数 data
回调函数 cb2 的参数解释:
第一个参数 int 指当前正在处理的字符
第二个参数 (void *) 指 csv_parse 最后一个参数 data
文件读取解析流程示例
uint8_t csv_parse_file(const char *file_path, void (*cols_callback)(void *, size_t, void *),
void (*rows_callback)(int c, void *), void *data)
{
struct csv_parser parser;
FILE *fp;
size_t bytes_read;
char buf[128];
size_t retval;
size_t pos = 0;
uint8_t result = RT_ERROR;
if (csv_init(&parser, CSV_STRICT | CSV_STRICT_FINI) != 0)
{
rt_kprintf("failed to initialize csv parser\n");
return result;
}
fp = fopen(file_path, "rb+");
if (fp == NULL)
{
rt_kprintf("Failed to open file %s\n", file_path);
csv_free(&parser);
return result;
}
while ((bytes_read = fread(buf, 1, 128, fp)) > 0)
{
if ((retval = csv_parse(&parser, buf, bytes_read, cols_callback, rows_callback, data)) != bytes_read)
{
goto end;
}
pos += 128;
}
if (csv_fini(&parser, cols_callback, rows_callback, data) != 0)
{
goto end;
}
result = RT_EOK;
end: fclose(fp);
csv_free(&parser);
return result;
}
// 假设文件中存储的是传感器数据,数据产生的时间,传感器数据序号
// 我们定义一个结构体,用来存储读取到的数据
struct SensorData
{
int id;
int temperature;
int humidity;
char date[20];
}
// 因为回调函数只能传入一个参数,所以另外定义一个数据结构,将上述结构体封装在内,方便我们处理数据
struct DataHandle
{
int col_count;
int row_count;
SensorData *data;
}
// 接下来定义列数据处理函数
void csv_cols_callback(void *s, size_t len, void *data)
{
struct DataHandle *handle = (struct DataHandle *)data;
// 比如我们要取第5行的数据 (第一行时,row_count == 0)
if (handle->row_count == 5 - 1)
{
switch (handle ->col_count)
{
case 0:
sscanf(s, "%d", &(handle ->data->id));
break;
case 1:
sscanf(s, "%d", &(handle ->data->temperature));
break;
case 2:
sscanf(s, "%d", &(handle ->data->humidity));
break;
case 3:
strncpy(handle ->data->date, s, 19);
break;
}
}
// 每进入该函数一次,列计数 + 1
++handle->col_count;
}
// 定义行数据处理函数
void csv_rows_callback(int c, void *data)
{
struct DataHandle *handle = (struct DataHandle *)data;
// 每进入该函数一次,行计数 + 1,列计数 清零
++handle->row_count;
handle->col_count = 0;
}
// 假设我们需要读取的数据存储在 /sensor1.csv
// 定义一个数据结构来存储处理后的结果
struct SensorData data;
// 定义一个数据结构方便我们完成数据处理
struct DataHandle handle = {0};
handle.data = &data;
// 开始解析
csv_parse_file("/sensor1.csv", csv_cols_callback, csv_rows_callback, &handle);
文件写入
// 我们定义一个函数将传感器数据转换为csv格式的一行数据字符串
size_t csv_parse_sensordata2string(struct SensorData *data, void *str_buf)
{
return sprintf(str_buf, "%d,%d,%d,%s\n", data->id, data->temperature, data->humidity, data->date);
}
// 定义一个文件写入函数
size_t csv_write_file(FILE *fp, void *data, size_t size)
{
size_t count = 0;
char *data_p = data;
if (fp == NULL)
{
return count;
}
for (size_t i = 0; i < size; i++)
{
if (fputc(data_p[i], fp) == EOF)
{
return count;
}
++count;
}
return count;
}
// 将上面两个函数封装成一个追加数据的函数
uint8_t csv_write_sensordata2file(const char *file_path, struct SensorData *data)
{
char buf[128];
FILE *fp;
size_t len, res = 0;
len = csv_parse_sensordata2string(data, buf);
if (len == 0)
{
return RT_ERROR;
}
fp = fopen(file_path, "ab");
do
{
res = csv_write_file(fp, buf, len);
len -= res;
} while (len != 0);
fclose(fp);
return RT_EOK;
}
后记
使用单一csv大文件存储数据,代替大量json小文件存储数据后,效率有非常明显的提升,测试 一万行数据,读取最后30条数据,解析时间大概为 5秒钟左右。