开窗见月,霜天悄然,欲更小文,以为消遣。
本篇以解析 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/ ,其中部分内容展示如下图。
数据既定,接下来便且书且析。
首先,确定输入与输出,写出函数原型。
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::split
和 std::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}
众多方法,于斯为巧,寥寥数行,便实现了需求。