引言

在科学计算、数据分析和工程应用中,表格数据(如 CSV、TSV、空格分隔文件)是最常见的数据格式之一。然而,C++ 标准库并未提供开箱即用的表格处理工具,开发者往往需要自己编写解析逻辑,处理分隔符、表头、注释、空值等琐碎问题。

dsv_tableutilsxx 库中的一个核心组件,它提供了一套完整的表格数据读写与操作方案,支持多种分隔格式、灵活的行列访问、数据过滤与排序,以及 JSON 互操作。本文将带你从零开始,一文看懂 dsv_table 的设计理念与使用技巧。


一、什么是 dsv_table?

dsv_table 中的 DSVDelimiter-Separated Values 的缩写,泛指所有以分隔符(逗号、空格、竖线等)组织的文本表格格式。dsv_table 不仅能处理 CSV,还能处理任意自定义分隔符的文本文件。

核心设计特点

特性说明
统一存储所有单元格内部以字符串存储,支持按需转换为 intdoublestd::string
类型标记每个单元格带有 String / Int / Float 类型标记,影响输出行为
输出控制可单独控制行/列/单元格的输出开关(Enable/Disable),实现"软删除"
行列命名支持通过名称(如 "SurfaceArea_n")或内置编号(如 R3C5)访问
多格式支持原生支持 .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 APIget_columnget_rowfill_columnreorder 等)专注于数据操作,默认隔离表头,避免误改
  • 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)
加载 CSVload_csv(filename, head_type)
加载 JSONload_json(filename, format)
保存文本save_text(filename, ext)
保存 CSVsave_csv(filename)
保存 JSONsave_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