引言
在科学计算、数据分析和工程应用中,表格数据(如 CSV、TSV、空格分隔文件)是最常见的数据格式之一。然而,C++ 标准库并未提供开箱即用的表格处理工具,开发者往往需要自己编写解析逻辑,处理分隔符、表头、注释、空值等琐碎问题。
dsv_table 是 utilsxx 库中的一个核心组件,它提供了一套完整的表格数据读写与操作方案,支持多种分隔格式、灵活的行列访问、数据过滤与排序,以及 JSON 互操作。本文将带你从零开始,一文看懂 dsv_table 的设计理念与使用技巧。
一、什么是 dsv_table?
dsv_table 中的 DSV 是 Delimiter-Separated Values 的缩写,泛指所有以分隔符(逗号、空格、竖线等)组织的文本表格格式。dsv_table 不仅能处理 CSV,还能处理任意自定义分隔符的文本文件。
核心设计特点
| 特性 | 说明 |
|---|---|
| 统一存储 | 所有单元格内部以字符串存储,支持按需转换为 int、double、std::string |
| 类型标记 | 每个单元格带有 String / Int / Float 类型标记,影响输出行为 |
| 输出控制 | 可单独控制行/列/单元格的输出开关(Enable/Disable),实现"软删除" |
| 行列命名 | 支持通过名称(如 "SurfaceArea_n")或内置编号(如 R3、C5)访问 |
| 多格式支持 | 原生支持 .txt、.csv、.json 的读写 |
| 头信息/注释/标记 | 文件中的注释行、标记行、头信息行会被自动提取并保留 |
二、快速上手
2.1 基本加载与查看
假设我们有一个 CSV 文件 sample_data.csv:
id,lon,lat,depth,den,sus,name,date,location
,32.23,65.23,,2.3,,gabbro,,"enshi, hubei"
,65.2,12.7,,2.5,,basalt,,"wuhan"
,70.23,10.6,,3.1,,volcanic,,"hangzhou, zhejiang"
加载并查看列信息:
#include "dsv_table.hpp"
int main() {
utilsxx::dsv_table t("data/sample_data", ".csv");
t.info(utilsxx::ColInfo); // 打印列信息
return 0;
}
输出示例:
Columns:
id | Enabled | String | ->
lon | Enabled | Float | 32.23 -> 70.23
lat | Enabled | Float | 65.23 -> 10.6
depth | Enabled | Float | ->
den | Enabled | Float | 2.3 -> 3.1
sus | Enabled | Float | ->
name | Enabled | String | gabbro -> volcanic
date | Enabled | String | ->
location | Enabled | String | enshi, hubei -> hangzhou, zhejiang
------------
注意:
load_csv()默认认为第一行是列头(ColHead),所以id, lon, lat...被自动识别为列名称。
2.2 自定义分隔符加载
对于竖线分隔的文件(如 world_data.txt),需要手动指定分隔符:
utilsxx::dsv_table tc;
tc.delimeter('|'); // 设置分隔符为竖线
tc.head_number(1); // 指定有1行头信息
tc.load_text("data/world_data", ".txt", utilsxx::ColHead | utilsxx::RowHead);
这里 ColHead | RowHead 表示既有列头,又有行头——第一行是列名称,第一列是行名称。
三、文件格式详解
dsv_table 支持一种富文本表格格式,其规则如下:
3.1 注释行(以 # 开头)
# World Bank Population Dataset
# Data is available at xxx.com
会被保存到 annotates_ 中,可通过 annotations() 获取。
3.2 标记行(以 #! 开头)
#! rows = 240
#! cols = 15
会被保存到 tags_ 中,可通过 tags() 获取。适合存放元数据。
3.3 头信息行(不以 # 开头,且在数据之前)
This is a test file for the 2014 World Bank population dataset.
通过 head_number(n) 设置前 n 行为头信息,会被保存到 heads_ 中。
3.4 数据行
头信息之后的非空行即为数据。dsv_table 会自动:
- 去除首尾空白
- 处理 CRLF 换行符
- 对 CSV 格式使用专用解析器(支持引号包裹的字段,如
"enshi, hubei") - 动态补齐缺失列(用空单元格填充)
四、数据访问与操作
4.1 索引访问
dsv_table 中有一个重要的索引约定需要牢记:
除
cell()外,所有行列操作均为 1-based(不直接操作行头/列头);只有cell(r, c)是 0-based,可以访问包括表头在内的任意单元格。
// cell() 是 0-based,可以操作表头
double val = t.cell<double>(1, 2); // 第2行第3列的数据(0-based)
t.cell(0, 0, "CornerName"); // 修改左上角表头单元格
t.cell(0, 1, "ColumnA"); // 修改第1个列名
t.cell(1, 0, "Row1"); // 修改第1个行名
// 其他所有 API 都是 1-based,只操作数据区域
std::vector<double> col3 = t.get_column<double>(3); // 第3列数据
std::vector<double> row2 = t.get_row<double>(2); // 第2行数据
t.fill_column(data, 1); // 填充第1列数据
t.row_type(utilsxx::Int, 2); // 设置第2行类型
这种设计的好处是:
- 1-based API(
get_column、get_row、fill_column、reorder等)专注于数据操作,默认隔离表头,避免误改 - 0-based
cell()提供底层细粒度控制,需要时可以直接修改行列名称或角落单元格
4.2 名称访问
// 通过列名获取一整列
std::vector<double> lon = t.get_column<double>("lon");
// 通过行名获取一整行(world_data.txt 中第一列是行名)
std::vector<std::string> china_row = t.get_row<std::string>("CHN");
4.3 内置编号访问
如果某行/列没有名称,可以使用内置格式 R<id> 和 C<id>:
// 获取第5列(无论是否有列名)
std::vector<double> col5 = t.get_column<double>("C5");
// 获取第10行
std::vector<std::string> row10 = t.get_row<std::string>("R10");
五、行列操作
5.1 添加列
// 在末尾添加一个空白列,命名为 "new_col"
int idx = t.add_column("new_col");
// 在索引2的位置插入数据列
std::vector<double> new_data = {1.0, 2.0, 3.0};
t.add_column(2, new_data); // 在第二列前插入
// 通过列名定位插入
t.add_column("after_this", new_data); // 在 "after_this" 列后插入
5.2 添加行
// 添加空白行
int row_idx = t.add_row("China");
// 插入数据行
std::vector<std::string> row_data = {"CHN", "China", "Asia"};
t.add_row("new_row", row_data);
5.3 填充与修改
// 填充指定列
std::vector<double> depths = {100.0, 200.0, 150.0};
t.fill_column(depths, "depth");
// 填充指定行
t.fill_row(row_data, "CHN");
六、数据过滤与排序
6.1 按正则表达式过滤
filter() 支持按行或按列过滤,不符合条件的行列会被禁用输出(并非真正删除):
// 按列过滤:只保留 "Continent_s" 列中包含 "America" 的行
// 参数说明:正则表达式, 基准列/行名, 基准类型
tc.filter("America", "Continent_s", utilsxx::ColHead);
这里 ColHead 表示 "Continent_s" 是一个列名,因此会按行过滤——只保留该列匹配 “America” 的行。
6.2 自定义函数过滤
// 定义过滤函数:保留第一列(行头除外)大于100的行
bool filter_func(const std::vector<utilsxx::table_cell>& row) {
return row[1].value<double>() > 100.0;
}
tc.filter(filter_func, utilsxx::RowHead); // 按行过滤
6.3 排序
// 按 "SurfaceArea_n" 列升序排序(整行联动)
tc.reorder<int>("SurfaceArea_n", utilsxx::ASCENDING);
// 按第3列降序排序
tc.reorder<double>(3, utilsxx::DESCENDING);
reorder是扩展模式排序,即排序时整行数据跟随排序键一起移动。
七、输出控制:Enable / Disable
dsv_table 的一大特色是输出开关机制。你可以禁用某些行或列,它们仍存在于内存中,但不会被保存到文件:
// 禁用第3列
t.column_output(3, utilsxx::Disable);
// 禁用名为 "deprecated" 的列
t.column_output("deprecated", utilsxx::Disable);
// 禁用第5行
t.row_output(5, utilsxx::Disable);
// 禁用整个表格
t.table_output(utilsxx::Disable);
// 重新启用
t.table_output(utilsxx::Enable);
导出过滤后的表格
// 导出时忽略被禁用的行列(默认行为)
utilsxx::dsv_table filtered = tc.export_table();
// 包含被禁用的行列
utilsxx::dsv_table full = tc.export_table(false);
八、JSON 互操作
dsv_table 支持 JSON 格式的读写,方便与现代 Web 流程对接。
8.1 从 JSON 加载
支持两种格式:
格式0 - 对象数组(默认):
[
{"姓名": "张三", "年龄": 25, "城市": "北京", "分数": 85.5},
{"姓名": "李四", "年龄": 30, "城市": "上海", "分数": 92.0}
]
utilsxx::dsv_table t;
t.load_json("data/student_data", 0); // 格式0
格式1 - 表格格式:
{
"headers": ["姓名", "年龄", "城市", "分数"],
"data": [
["张三", 25, "北京", 85.5],
["李四", 30, "上海", 92.0]
]
}
t.load_json("data/table_data", 1); // 格式1
8.2 保存为 JSON
// 保存为对象数组格式(默认)
t.save_json("output", 0);
// 保存为表格格式
t.save_json("output", 1);
输出会自动根据单元格类型(Int/Float/String)生成对应的 JSON 类型,而非全部转为字符串。
九、实用技巧
9.1 浮点数精度控制
// 设置 double 类型保存时的有效数字为4位
t.cell(1, 1, 3.1415926, 4); // 保存为 "3.142"
// 批量填充时控制精度
std::vector<double> data = {1.234567, 2.345678};
t.fill_column(data, "value", 3); // 保留3位有效数字
9.2 类型设置
// 将 "id" 列设为整数类型
t.column_type(utilsxx::Int, "id");
// 将第2行设为字符串类型
t.row_type(utilsxx::String, 2);
类型会影响 JSON 输出时的数据类型。
9.3 检查行列是否存在
if (t.has_column("depth")) {
// 处理 depth 列
}
if (t.has_row("CHN")) {
// 处理中国行
}
9.4 遍历表格(跳过表头)
// begin() 自动跳过第0行(表头行)
for (auto it = t.begin(); it != t.end(); ++it) {
for (const auto& cell : *it) {
std::cout << cell.str_ << " ";
}
std::cout << "\n";
}
9.5 移动语义优化
dsv_table 实现了移动构造和移动赋值,大数据量传递时无需深拷贝:
utilsxx::dsv_table load_big_data() {
utilsxx::dsv_table t;
t.load_csv("huge_file.csv");
return t; // 移动语义,高效
}
auto data = load_big_data();
十、完整示例
以下是一个综合示例,演示从加载到过滤、排序、保存的完整流程:
#include "dsv_table.hpp"
#include <iostream>
int main() try {
// 1. 加载竖线分隔的世界银行数据
utilsxx::dsv_table tc;
tc.delimeter('|');
tc.head_number(1);
tc.load_text("data/world_data", ".txt", utilsxx::ColHead | utilsxx::RowHead);
// 2. 查看文件元信息
tc.info(utilsxx::AttInfo | utilsxx::HeadInfo | utilsxx::TagInfo);
// 3. 过滤:只保留美洲国家
tc.filter("America", "Continent_s", utilsxx::ColHead);
// 4. 检查列是否存在
if (tc.has_column("SurfaceArea_n")) {
std::cout << "找到 SurfaceArea_n 列\n";
}
// 5. 导出过滤结果(生成新表格)
utilsxx::dsv_table tc2 = tc.export_table();
// 6. 按国土面积升序排序
tc2.reorder<int>("SurfaceArea_n", utilsxx::ASCENDING);
// 7. 设置分隔符并保存
tc2.delimeter('|');
tc2.save_text("america_sorted");
// 8. 同时保存为 JSON
tc2.save_json("america_sorted", 0);
return 0;
}
catch(const utilsxx::error_handler& e) {
e.show();
return 1;
}
十一、API 速查表
| 操作 | 方法 |
|---|---|
| 加载文本 | load_text(filename, ext, head_type) |
| 加载 CSV | load_csv(filename, head_type) |
| 加载 JSON | load_json(filename, format) |
| 保存文本 | save_text(filename, ext) |
| 保存 CSV | save_csv(filename) |
| 保存 JSON | save_json(filename, format) |
| 设置分隔符 | delimeter(char) |
| 获取列数据 | get_column<T>(idx/name) |
| 获取行数据 | get_row<T>(idx/name) |
| 获取单元格 | cell<T>(r, c) |
| 设置单元格 | cell(r, c, value, precision) |
| 填充列 | fill_column(data, idx/name, precision) |
| 填充行 | fill_row(data, idx/name, precision) |
| 添加列 | add_column(name/idx, [data]) |
| 添加行 | add_row(name/idx, [data]) |
| 设置列类型 | column_type(type, idx/name) |
| 设置行类型 | row_type(type, idx/name) |
| 过滤(正则) | filter(regex, target, head_type) |
| 过滤(函数) | filter(func, head_type) |
| 排序 | reorder<T>(idx/name, order) |
| 禁用列 | column_output(idx/name, Disable) |
| 禁用行 | row_output(idx/name, Disable) |
| 导出表格 | export_table(ignore_disabled) |
| 查看信息 | info(mask, os) |
结语
dsv_table 是一个设计精良、功能完整的 C++ 表格处理工具。它将文件解析、数据存储、类型转换、过滤排序、多格式导出整合在一个类中,既适合快速脚本式数据处理,也能胜任大型科学计算项目的数据预处理需求。
如果你正在寻找一个比手动解析 CSV 更强大、比引入重型数据库更轻量的表格处理方案,dsv_table 值得一试。
项目地址:utilsxx