繁体   English   中英

从 C++ 中的 CSV 文件中提取某些列

[英]Extracting certain columns from a CSV file in C++

我想知道如何从 C++ 中的 CSV 文件中提取/跳过某些列,例如ageweight

在我加载整个 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 用于在输入字符串中应该匹配/不应该匹配的内容。 匹配策略的类型由最后一个参数给出。

  • 0 --> 给我在正则表达式中定义的东西和(可选)
  • -1 --> 告诉我根据正则表达式不匹配的内容。

我们可以使用这个迭代器将标记存储在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.findstring.substringstd::stringviews或其他。


所以,现在进入进一步的话题。

在提取器中,我们首先从源代码 stream 中读取完整的一行并检查它是否有效。 或者,如果我们有文件结尾或任何其他错误。

然后我们如上所述标记刚刚读取的字符串。

然后,我们将仅将标记中的选定列复制到我们的结果数据中。 这是在一个简单的 for 循环中完成的。 在这里,我们还检查了边界,因为有人可能指定无效的选定列,或者一行的标记可能比预期的要少。

所以提取器的主体非常简单。 只需 5 行代码。 . .


然后,再次,

您应该开始使用 C++ 中的面向对象功能。 在 C++ 中,您可以将数据和对这些数据进行操作的方法放在一个 object 中。 原因是外界不应该关心对象的内部结构。 例如,您的readCSVprintCSV 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM