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秒钟左右。

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值