Parsing CSV Files in C++20

教育   科技   2023-11-29 23:53   美国  


开窗见月,霜天悄然,欲更小文,以为消遣。

本篇以解析 CSV 为例,再谈 C++20 的使用。网上方法,颇为陈旧,看新方式何以优雅实现。

开始之前,定义为先:

Comma-separated values (CSV) is a text file format that uses commas to separate values. A CSV file stores tabular data (numbers and text) in plain text, where each line of the file typically represents one data record. Each record consists of the same number of fields, and these are separated by commas in the CSV file. If the field delimiter itself may appear within a field, fields can be surrounded with quotation marks.

CSV 文件是以逗号分隔数据的一种文本格式,每行表示一个数据记录,列数一致。机器学习中的许多数据集便是此种格式,解析工作,相当常见。

本文便以一真实数据集为例,进行演示。数据集地址为https://www.kaggle.com/datasets/michaelbryantds/cpu-and-gpu-product-data/ ,其中部分内容展示如下图。


该芯片数据集,含 2185 条 CPU 数据和 2668 条 GPU 数据。

数据既定,接下来便且书且析。

首先,确定输入与输出,写出函数原型。

 1using dataset_sequence_type = std::vector<std::vector<std::string>>;
2
3auto read_csv(std::string_view file, std::string_view type = ""std::string_view delimiter = ",")
4    -> std::optional<dataset_sequence_type>
5
{
6    std::ifstream data_file(file.data());
7    if (!data_file.is_open())
8        return {};
9
10    // do parsing
11
12    data_file.close();
13}

三个输入参数分别表示数据集文件路径、筛选类型(CPU or GPU)和 分隔符,后二者皆为可选参数。

返回值采用 std::optional,便于检测结果的有效性,实现返回值为 std::vector 构成的动态二维数组,一条记录占一行,每个元素占一列。

其次,逐行读取文件,依分隔符拆分数据。

 1using dataset_sequence_type = std::vector<std::vector<std::string>>;
2
3auto read_csv(std::string_view file, std::string_view type = ""std::string_view delimiter = ",")
4    -> std::optional<dataset_sequence_type>
5
{
6    std::ifstream data_file(file.data());
7    if (!data_file.is_open())
8        return {};
9
10    // do parsing
11    std::string line;
12    dataset_sequence_type result;
13    std::getline(data_file, line); // skip the title
14    while (std::getline(data_file, line)) {
15        auto tokens = line
16                    | std::views::split(delimiter)
17                    | std::views::transform([](auto&& token) {
18                        return std::string_view(&*token.begin(), std::ranges::distance(token));
19                    });
20
21        // oher work
22    }
23
24    data_file.close();
25}

表头为数据描述信息,是以弃之。

解析工作,乃 Views 拿手好戏,由 std::views::splitstd::views::transform 轻松拿下。因 split_ivew 里面的值类型为 ranges::subrange,这里借助 transform 将其转换为 string_view

至此,已实现殆半。余下难题主要在于过滤与保存,若无需过滤,type 参数便可弃去,问题顿消。

 1// ...
2
3auto read_csv(std::string_view file, std::string_view type = ""std::string_view delimiter = ",")
4    -> std::optional<dataset_sequence_type>
5
{
6    // ...
7    while (std::getline(data_file, line)) {
8        auto tokens = line
9                    | std::views::split(delimiter)
10                    | std::views::transform([](auto&& token) {
11                        return std::string_view(&*token.begin(), std::ranges::distance(token));
12                    });
13
14        // oher work
15        result.push_back(dataset_sequence_type::value_type(tokens.begin(), tokens.end()));
16    }
17
18    // ...
19
20    return result;
21}

若是过滤,将所有 Views 转换成 std::vector,些许始建即弃,未免浪费。于是先筛后存。type 为数据集第二列,然而 transform_view 并不支持随机访问,你无法像 vector 那般以便下标直接访问某列元素。

对此问题,最简之法是借助 std::advance,它可以控制迭代器前进。

 1// ...
2
3auto read_csv(std::string_view file, std::string_view type = ""std::string_view delimiter = ",")
4    -> std::optional<dataset_sequence_type>
5
{
6    // ...
7    while (std::getline(data_file, line)) {
8        auto tokens = line
9                    | std::views::split(delimiter)
10                    | std::views::transform([](auto&& token) {
11                        return std::string_view(&*token.begin(), std::ranges::distance(token));
12                    });
13
14        // filter
15        auto it = std::ranges::begin(tokens);
16        std::ranges::advance(it, 2);
17        if (type.empty() || *it == type) {
18            // save all records or filtered records.
19            result.push_back(dataset_sequence_type::value_type(tokens.begin(), tokens.end()));
20        }
21    }
22
23    // ...
24
25    return result;
26}

最后,你可能还想对 read_csv() 添加 constexpr,只惜 std::ifstream 当前并不支持编译期,无法实现。那是否存在其他方式呢?暂不作表,暇日续究。

该实现具有通用性(去除过滤,或将过滤以 lambda 抽象出来,则可更加通用),完整代码及使用示例:

 1using dataset_sequence_type = std::vector<std::vector<std::string>>;
2
3auto read_chip_dataset(std::string_view file, std::string_view type, std::string_view delimiter)
4    -> std::optional<dataset_sequence_type>
5{
6    std::ifstream data_file(file.data());
7    if (!data_file.is_open()) {
8        return {};
9    }
10
11    std::string line;
12    std::getline(data_file, line); // skip the title
13    dataset_sequence_type result;
14    while (std::getline(data_file, line)) {
15        auto tokens = line
16                    | std::views::split(delimiter)
17                    | std::views::transform([](auto&& token) {
18                        return std::string_view(&*token.begin(), std::ranges::distance(token));
19                    });
20
21        auto it = std::ranges::begin(tokens);
22        std::ranges::advance(it, 2);
23        if (type.empty() || *it == type) {
24            // save all records or filtered records.
25            result.push_back(dataset_sequence_type::value_type(tokens.begin(), tokens.end()));
26        }
27    }
28
29    data_file.close();
30
31    return result;
32}
33
34int main() {
35    // 加载数据集
36    auto chip = read_chip_dataset("./datasets/chip_dataset.csv""CPU");
37    if (chip) {
38        std::ranges::for_each(chip.value(), [](const dataset_sequence_type::value_type& cpu) {
39            fmt::print("{}\n", cpu);
40        });
41    }
42}

众多方法,于斯为巧,寥寥数行,便实现了需求。


CppMore
Dive deep into the C++ core, and discover more!
 最新文章