[英]Extracting certain columns from a CSV file in C++
我想知道如何从 C++ 中的 CSV 文件中提取/跳过某些列,例如age
和weight
。
在我加载整个 csv 文件后提取所需信息是否更有意义(如果 memory 没有问题)?
编辑:如果可能的话,我想要阅读、打印和修改部分。
如果可能,我只想使用 STL。 我的测试 csv 文件的内容如下所示:
*test.csv*
name;age;weight;height;test
Bla;32;1.2;4.3;True
Foo;43;2.2;5.3;False
Bar;None;3.8;2.4;True
Ufo;32;1.5;5.4;True
我使用以下 C++ 程序加载test.csv
文件,该程序在屏幕上打印文件的内容:
#include <iostream>
#include <vector>
#include <string>
#include <iomanip>
#include <fstream>
#include <sstream>
void readCSV(std::vector<std::vector<std::string> > &data, std::string filename);
void printCSV(const std::vector<std::vector<std::string>> &data);
int main(int argc, char** argv) {
std::string file_path = "./test.csv";
std::vector<std::vector<std::string> > data;
readCSV(data, file_path);
printCSV(data);
return 0;
}
void readCSV(std::vector<std::vector<std::string> > &data, std::string filename) {
char delimiter = ';';
std::string line;
std::string item;
std::ifstream file(filename);
while (std::getline(file, line)) {
std::vector<std::string> row;
std::stringstream string_stream(line);
while (std::getline(string_stream, item, delimiter)) {
row.push_back(item);
}
data.push_back(row);
}
file.close();
}
void printCSV(const std::vector<std::vector<std::string> > &data) {
for (std::vector<std::string> row: data) {
for (std::string item: row) {
std::cout << item << ' ';
}
std::cout << std::endl;
}
}
您能提供的任何帮助将不胜感激。
基本上我已经在类似的帖子中回答了这个问题。 但无论如何,我将在这里展示一个采用不同方法和一些解释的现成解决方案。
一个提示:你应该让自己更加熟悉面向 object 的编程。 并考虑您的设计。 在您的读写 function 中,您创建了对文件或std::cout
的不必要的依赖项 - 因此,您不应移交文件名,然后在 function 中打开文件,而是使用streams
。 Because, in the function that I created, using the C++ IO facilities, it doesn't matter, if we read from a file or a std::istringstream
or write to std::cout
or a file stream.
所有这些都将通过(重载的)提取器和插入器操作符进行处理。
所以,因为我希望代码更灵活一点,所以我将我的结构设为模板,以便能够放入选定的列并将相同的结构重用于其他列组合。
如果您想固定选定的列,那么您可以删除带有template
的行并可以替换std::vector<size_t> selectedFields{ {Colums...} };
与std::vector<size_t> selectedFields{ {1,2} };
稍后我们对模板使用using
以便于处理和理解:
// Define Dataype for selected columns age and weight
using AgeAndWeight = SelectedColumns<1, 2>;
OK,我们先看源码,再试着理解。
#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <fstream>
#include <initializer_list>
#include <iterator>
#include <algorithm>
std::regex re{ ";" };
// Proxy for reading an splitting a line and extracting certain fields and some simple output
template<size_t ... Colums>
struct SelectedColumns {
std::vector<std::string> data{};
std::vector<size_t> selectedFields{ {Colums...} };
// Overwrite extractor operator
friend std::istream& operator >> (std::istream& is, SelectedColumns& sl) {
// Read a complete line and check, if it could be read
if (std::string line{}; std::getline(is, line)) {
// Now split the line into tokens
std::vector tokens(std::sregex_token_iterator(line.begin(), line.end(), re, -1), {});
// Clear old data
sl.data.clear();
// So, and now copy the selected columns into our data vector
for (const size_t& column : sl.selectedFields)
if (column < tokens.size()) sl.data.push_back(tokens[column]);
}
return is;
}
// Simple extractor
friend std::ostream& operator << (std::ostream & os, const SelectedColumns & sl) {
std::copy(sl.data.begin(), sl.data.end(), std::ostream_iterator<std::string>(os, "\t"));
return os;
}
};
// Define Dataype for selected columns age and weight
using AgeAndWeight = SelectedColumns<1U, 2U>;
const std::string fileName{ "./test.csv" };
int main() {
// Open the csv file and check, if it is open
if (std::ifstream csvFileStream{ fileName }; csvFileStream) {
// Read complete csv file and extract age and weight columns
std::vector sc(std::istream_iterator<AgeAndWeight>(csvFileStream), {});
// Now all data is available in this vector sc Do something
sc[3].data[0] = "77";
// Show some debug out put
std::copy(sc.begin(), sc.end(), std::ostream_iterator<AgeAndWeight>(std::cout, "\n"));
// By the way, you could also write the 2 lines above in one line.
//std::copy(std::istream_iterator<AgeAndWeight>(csvFileStream), {}, std::ostream_iterator<AgeAndWeight>(std::cout, "\n"));
}
else std::cerr << "\n*** Error: Could not open source file\n\n";
return 0;
}
这里的一项主要任务是将带有 CSV 数据的行拆分为其令牌。 让我们来看看这个。
将字符串拆分为标记:
人们对 function 有什么期望,当他们阅读
获取线路?
大多数人会说,嗯,我想它会从某个地方读到完整的一行。 猜猜看,这就是这个 function 的基本意图。 从 stream 中读取一行并将其放入字符串中。
但是,正如您 在此处看到的那样, std::getline
具有一些附加功能。
这导致严重滥用此 function 将std::string
s 拆分为令牌。
将字符串拆分为标记是一项非常古老的任务。 在很早的 C 中有 function strtok
,即使在 C++ 中仍然存在。 这里std::strtok
。 请参阅std::strtok
-example
std::vector<std::string> data{};
for (char* token = std::strtok(const_cast<char *>(line.data()), ","); token != nullptr; token = std::strtok(nullptr, ","))
data.push_back(token);
很简单,对吧?
但是由于std::getline
的附加功能已被严重误用于标记字符串。 如果您查看有关如何解析 CSV 文件的首要问题/答案(请参阅此处),那么您将明白我的意思。
人们正在使用std::getline
从原始 ZF7B44CFFAFD5C52223D5498196C8A2E7BZ 中读取文本行、字符串,然后将其填充到std::istringstream
并再次使用带分隔符的std::getline
将字符串解析为标记。 诡异的。
但是,多年来,我们有一个专用的、特殊的 function 用于标记字符串,特别是专门为此目的而设计的。 它是
std::sregex_token_iterator
既然我们有这么一个专用的function,我们应该简单地使用它。
这个东西是一个迭代器。 对于遍历字符串,因此 function 名称以 s 开头。 开始部分定义了我们将在什么输入范围内操作,结束部分是默认构造的,然后有一个 std::regex 用于在输入字符串中应该匹配/不应该匹配的内容。 匹配策略的类型由最后一个参数给出。
我们可以使用这个迭代器将标记存储在std::vector
中。 std::vector
有一个范围构造函数,它接受 2 个迭代器作为参数,并将第一个迭代器和第二个迭代器之间的数据复制到 std::vector。 该声明
std::vector tokens(std::sregex_token_iterator(s.begin(), s.end(), re, -1), {});
将变量“tokens”定义为 std::vector 并使用 std::vector 的所谓范围构造函数。 请注意:我使用的是 C++17 并且可以在没有模板参数的情况下定义std::vector
。 编译器可以从给定的 function 参数中推断出参数。 此功能称为 CTAD(“类模板参数推导”)。
此外,您可以看到我没有明确使用“end()”迭代器。
这个迭代器将从带有正确类型的空大括号封闭的默认初始值设定项构造,因为由于std::vector
构造函数需要它,它将被推断为与第一个参数的类型相同。
您可以在一行中读取任意数量的标记并将其放入std::vector
但你可以做得更多。 您可以验证您的输入。 如果您使用 0 作为最后一个参数,则定义一个std::regex
甚至可以验证您的输入。 而且您只会获得有效的令牌。
总体而言,专用功能的使用优于误用的std::getline
,人们应该简单地使用它。
有些人抱怨 function 开销,他们是对的,但其中有多少人在使用大数据。 即使那样,该方法也可能是使用string.find
和string.substring
或std::stringviews
或其他。
所以,现在进入进一步的话题。
在提取器中,我们首先从源代码 stream 中读取完整的一行并检查它是否有效。 或者,如果我们有文件结尾或任何其他错误。
然后我们如上所述标记刚刚读取的字符串。
然后,我们将仅将标记中的选定列复制到我们的结果数据中。 这是在一个简单的 for 循环中完成的。 在这里,我们还检查了边界,因为有人可能指定无效的选定列,或者一行的标记可能比预期的要少。
所以提取器的主体非常简单。 只需 5 行代码。 . .
然后,再次,
您应该开始使用 C++ 中的面向对象功能。 在 C++ 中,您可以将数据和对这些数据进行操作的方法放在一个 object 中。 原因是外界不应该关心对象的内部结构。 例如,您的readCSV
和printCSV
function 应该是结构(或类)的一部分。
下一步,我们将不再使用您的“读取”和“打印”功能。 我们将使用专用的 function 用于 Stream-IO、提取器运算符 >> 和插入器运算符 <<。 我们将覆盖结构中的标准 IO 函数。
在 function main
我们将打开源文件并检查是否打开成功。 顺便提一句。 如果成功,则应检查所有输入 output 功能。
然后,我们使用下一个迭代器the std::istream_iterator
。 这与我们的“AgeAndWeight”类型和输入文件 stream 一起。 同样在这里,我们使用 CTAD 和默认构造的结束迭代器。 std::istream_iterator
将重复调用 AgeAndWeight 提取器操作符,直到源文件的所有行都被读取。
对于 output,我们将使用std::ostream_iterator
。 这将调用“AgeAndWeight”的插入器操作符,直到所有数据都被写入。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.