繁体   English   中英

从逗号分隔的文件中读取到对象的向量

[英]Read from comma separated file into vector of objects

我做了一个简单的 C++ 程序来获得 C++ 的知识。 这是一个最终存储和读取文件的游戏。 分数、名称等。文件的每一行都存储了播放器 object 的内容。

例如:身份证年龄姓名等。

我现在想在文件中更改为逗号分隔,但后来我遇到了如何读取每一行并将 Player object 写入 Player 对象的向量 std::vector 正确的问题。

我今天的代码是这样的。

std::vector<Player> readPlayerToVector()
{
    // Open the File
    std::ifstream in("players.txt");

    std::vector<Player> players; // Empty player vector

    while (in.good()) {
        Player temp; //
        in >> temp.pID;
        ....
        players.push_back(temp);
    }
    in.close();
    return players;
}

我应该如何更改此代码以与逗号分隔兼容。 它不适用于具有 >> 过载的空间分隔。

请注意,我是 C++ 的初学者。 我尝试查看使用带有 stringstream 的 std::getline(ss, line) 的示例,但我无法找到使用该方法分配 Player object 的好方法。

我在这里提供了类似的解决方案:

c++ 中的 read.dat 文件并创建多种数据类型

#include <iostream>
#include <sstream>
#include <vector>


struct Coefficients {
    unsigned A;
    std::vector<double> B;
    std::vector< std::vector<double> > C;
};

std::vector<double> parseFloats( const std::string& s ) {
    std::istringstream isf( s );
    std::vector<double> res;
    while ( isf.good() ) {
        double value;
        isf >> value;
        res.push_back( value );
    }
    return res;
}

void readCoefficients( std::istream& fs, Coefficients& c ) {
    fs >> c.A;
    std::ws( fs );
    std::string line;
    std::getline( fs, line );
    c.B = parseFloats( line );
    while ( std::getline( fs, line ) ) {
        c.C.push_back( parseFloats( line ) );
    }
}

这也可能适用:

在 C++ 中读取文件内容并将不同数据类型分成不同向量的最佳方法

    std::vector<int> integers;
    std::vector<std::string> strings;

    // open file and iterate
    std::ifstream file( "filepath.txt" );
    while ( file ) {

        // read one line
        std::string line;
        std::getline(file, line, '\n');

        // create stream for fields
        std::istringstream ils( line );
        std::string token;

        // read integer (I like to parse it and convert separated)
        if ( !std::getline(ils, token, ',') ) continue;
        int ivalue;
        try { 
            ivalue = std::stoi( token );
        } catch (...) {
            continue;
        }
        integers.push_back(  ivalue );

        // Read string
        if ( !std::getline( ils, token, ',' )) continue;
        strings.push_back( token );
    }

您可以按行而不是逗号分隔每个变量。 我发现这种方法更简单,因为您可以使用 getline function。

阅读 ifstream/ofstream 的文档。 仅基于此文档,我就完成了几个项目!

C++ fstream 参考

我会尽力帮助并向您解释所有步骤。 我将首先展示一些理论,然后展示一些简单的解决方案、一些替代解决方案和 C++(面向对象)方法。

因此,我们将 go 从超级简单到更现代的 C++ 解决方案。

开始吧。 假设您有一个具有某些属性的玩家。 属性可以是例如:ID 名称年龄分数。 如果将此数据存储在文件中,它可能如下所示:

1  Peter   23   0.98
2  Carl    24   0.75
3  Bert    26   0.88
4  Mike    24   0.95

但是在某个时间点,我们注意到这种漂亮而简单的格式将不再适用。 原因是带有提取运算符>>的格式化输入函数将在空白处停止转换。 这不适用于以下示例:

1  Peter Paul    23   0.98
2  Carl Maria    24   0.75
3  Bert Junior   26   0.88
4  Mike Senior   24   0.95

然后语句fileStream >> id >> name >> age >> score; 将不再起作用,一切都会失败。 因此,广泛选择以 CSV(逗号分隔值)格式存储数据。

该文件将如下所示:

1,  Peter Paul,    23,   0.98
2,  Carl Maria,    24,   0.75
3,  Bert Junior,   26,   0.88
4,  Mike Senior,   24,   0.95

有了这个,我们可以清楚地看到,什么值属于哪个属性。 但不幸的是,这会使阅读变得更加困难。 因为您现在确实需要执行 3 个步骤:

  1. 将完整的行读取为std::string
  2. 使用逗号作为分隔符将此字符串拆分为子字符串
  3. 将子字符串转换为所需的格式,例如从字符串到数字年龄

所以,让我们一步一步解决这个问题。

读一整行很容易。 为此,我们有 function std::getline 它将从 stream (从任何 istream,如std::cinstd::ifstream或也从std::istringstream )读取一行(在文本中直到行字符'\n')并存储它在std::string变量中。 请在 此处阅读 CPP 参考中对 function 的描述。

现在,将 CSV 字符串拆分为各个部分。 可用的方法太多了,很难说什么是好的方法。 稍后我还将展示几种方法,但最常见的方法是使用std::getline完成的。 (我个人最喜欢的是std::sregex_token_iterator ,因为它非常适合 C++ 算法世界。但在这里,它太复杂了)。

好的, std::getline 正如您在 CPP 参考中所读到的, std::getline会读取字符,直到找到分隔符。 如果您不指定分隔符,则它将读取到行尾\n 但您也可以指定不同的分隔符。 我们将在我们的案例中这样做。 我们将选择分隔符','。

但是,另一个问题是,在步骤 1 中阅读了完整的一行之后,我们在std::string中有这一行。 而且, std::getline想要从 stream 中读取数据。 因此,以逗号作为分隔符的std::getline不能与作为源的std::string一起使用。 幸运的是,这里还有一种可用的标准方法。 我们将使用std::istringstreamstd::string转换为 stream。 您可以简单地定义这种类型的变量并将刚刚读取的字符串作为参数传递给它的构造函数。 例如:

std::istringstream iss(line);

现在我们也可以通过这个“iss”来使用所有的 iostream 函数。 凉爽的。 我们将使用带有 ',' 分隔符的std::getline并接收 substring。

不幸的是,第三个也是最后一个也是必要的。 现在我们有一堆子字符串。 但是我们也有 3 个数字作为属性。 “ID”是unsigned long ,“Age”是int ,“Score”是double ,所以我们需要使用字符串转换函数将 substring 转换为数字: std::stoul , std::stoistd::stod 如果输入数据总是OK,那么这样就OK了,但是如果我们需要验证输入,那就更复杂了。 让我们假设我们有一个好的输入。

然后,许多可能的例子之一:

#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>

struct Player {
    unsigned long ID{};
    std::string name{};
    int age{};
    double score{};
};

// !!! Demo. All without error checking !!!
int main() {

    // Open the source CSV file
    std::ifstream in("players.txt");

    // Here we will store all players that we read
    std::vector<Player> players{};

    // We will read a complete line and store it here
    std::string line{};

    // Read all lines of the source CSV file
    while (std::getline(in, line)) {
        
        // Now we read a complete line into our std::string line
        // Put it into a std::istringstream to be able to extract it with iostream functions
        std::istringstream iss(line);

        // We will use a vector to store the substrings
        std::string substring{};
        std::vector<std::string> substrings{};
        
        // Now, in a loop, get the substrings from the std::istringstream
        while (std::getline(iss, substring, ',')) {

            // Add the substring to the std::vector
            substrings.push_back(substring);
        }
        // Now store the data for one player in a Player struct
        Player player{};
        player.ID = std::stoul(substrings[0]);
        player.name = substrings[1];
        player.age = std::stoi(substrings[2]);
        player.score = std::stod(substrings[3]);

        // Add this new player to our player list
        players.push_back(player);
    }

    // Debug output
    for (const Player& p : players) {
        std::cout << p.ID << "\t" << p.name << '\t' << p.age << '\t'  << p.score << '\n';
    }
}

你看,它变得越来越复杂。

如果您更有经验,那么您也可以使用其他机制。 但是,您需要了解格式化未格式化输入之间的区别,并且需要更多练习。 这很复杂。 (所以,不要在一开始就使用它):

#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>

struct Player {
    unsigned long ID{};
    std::string name{};
    int age{};
    double score{};
};

// !!! Demo. All without error checking !!!
int main() {

    // Open the source CSV file
    std::ifstream in("r:\\players.txt");

    // Here we will store all players that we read
    Player player{};
    std::vector<Player> players{};
    
    char comma{}; // Some dummy for reading a comma

    // Read all lines of the source CSV file
    while (std::getline(in >> player.ID >> comma >> std::ws, player.name, ',') >> comma >> player.age >> comma >> player.score) {

        // Add this new player to our player list
        players.push_back(player);
    }
        // Debug output
    for (const Player& p : players) {
        std::cout << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score << '\n';
    }
}

如前所述,不要在开始时使用。

但是,您应该尝试学习和理解的是:C++ 是一种面向 object 的语言。 这意味着我们不仅将数据放入 Player 结构体中,还将对这些数据进行操作的方法放入其中。

而这些目前只是输入和output。 正如您已经知道的那样,输入和 output 是使用 iostream-functionality 与提取器运算符>>和插入器运算符<<完成的。 但是,如何做到这一点? 我们的 Player 结构是一个自定义类型。 它没有内置>><<运算符。

幸运的是,C++ 是一种功能强大的语言,可以让我们轻松添加此类功能。

结构的签名将如下所示:

struct Player {

    // The data part
    unsigned long ID{};
    std::string name{};
    int age{};
    double score{};

    // The methods part
    friend std::istream& operator >> (std::istream& is, Player& p);
    friend std::ostream& operator << (std::ostream& os, const Player& p);
};

并且,使用上述方法为这些运算符编写代码后,我们将得到:

#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>

struct Player {

    // The data part
    unsigned long ID{};
    std::string name{};
    int age{};
    double score{};

    // The methods part
    friend std::istream& operator >> (std::istream& is, Player& p) {
        std::string line{}, substring{}; std::vector<std::string> substrings{};
        std::getline(is, line);
        std::istringstream iss(line);
        // Read all substrings
        while (std::getline(iss, substring, ','))
            substrings.push_back(substring);
        // Now store the data for one player in the given  Player struct
        Player player{};
        p.ID = std::stoul(substrings[0]);
        p.name = substrings[1];
        p.age = std::stoi(substrings[2]);
        p.score = std::stod(substrings[3]);
        return is;
    }
    friend std::ostream& operator << (std::ostream& os, const Player& p) {
        return os << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score;
    }
};

// !!! Demo. All without error checking !!!
int main() {

    // Open the source CSV file
    std::ifstream in("r:\\players.txt");

    // Here we will store all players that we read
    Player player{};
    std::vector<Player> players{};


    // Read all lines of the source CSV file into players
    while (in >> player) {

        // Add this new player to our player list
        players.push_back(player);
    }

    // Debug output
    for (const Player& p : players) {
        std::cout << p << '\n';
    }
}

它只是重用我们上面学到的一切。 只要把它放在正确的地方。

我们甚至可以领先一步 go。 还有播放器列表, ste::vector<Player>可以包装在 class 中,并使用 iostream-functionality 进行修改。

通过了解以上所有内容,现在这将非常简单。 看:

#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>

struct Player {

    // The data part
    unsigned long ID{};
    std::string name{};
    int age{};
    double score{};

    // The methods part
    friend std::istream& operator >> (std::istream& is, Player& p) {
        char comma{}; // Some dummy for reading a comma
        return std::getline(is >> p.ID >> comma >> std::ws, p.name, ',') >> comma >> p.age >> comma >> p.score;
    }
    friend std::ostream& operator << (std::ostream& os, const Player& p) {
        return os << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score;
    }
};

struct Players {

    // The data part
    std::vector<Player> players{};

    // The methods part
    friend std::istream& operator >> (std::istream& is, Players& ps) {
        Player player{};
        while (is >> player) ps.players.push_back(player);
        return is;
    }
    friend std::ostream& operator << (std::ostream& os, const Players& ps) {
        for (const Player& p : ps.players) os << p << '\n';
        return os;
    }
};

// !!! Demo. All without error checking !!!
int main() {

    // Open the source CSV file
    std::ifstream in("players.txt");

    // Here we will store all players that we read
    Players players{};

    // Read the complete CSV file and store everything in the players list at the correct place
    in >> players;

    // Debug output of complete players data. Ultra short.
    std::cout << players;
}

如果您能看到简单而强大的解决方案,我会很高兴。

最后,正如承诺的那样。 将字符串拆分为子字符串的一些其他方法:

将字符串拆分为标记是一项非常古老的任务。 有许多可用的解决方案。 都有不同的属性。 有些难以理解,有些难以开发,有些更复杂,更慢或更快或更灵活或不灵活。

备择方案

  1. 手工制作,许多变体,使用指针或迭代器,可能难以开发且容易出错。
  2. 使用旧式std::strtok function。 也许不安全。 也许不应该再使用了
  3. std::getline 最常用的实现。 但实际上是一种“误用”,并没有那么灵活
  4. 使用专门为此目的开发的专用现代 function,最灵活且最适合 STL 环境和算法环境。 但是比较慢。

请在一段代码中查看 4 个示例。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <regex>
#include <algorithm>
#include <iterator>
#include <cstring>
#include <forward_list>
#include <deque>

using Container = std::vector<std::string>;
std::regex delimiter{ "," };


int main() {

    // Some function to print the contents of an STL container
    auto print = [](const auto& container) -> void { std::copy(container.begin(), container.end(),
        std::ostream_iterator<std::decay<decltype(*container.begin())>::type>(std::cout, " ")); std::cout << '\n'; };

    // Example 1:   Handcrafted -------------------------------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Search for comma, then take the part and add to the result
        for (size_t i{ 0U }, startpos{ 0U }; i <= stringToSplit.size(); ++i) {

            // So, if there is a comma or the end of the string
            if ((stringToSplit[i] == ',') || (i == (stringToSplit.size()))) {

                // Copy substring
                c.push_back(stringToSplit.substr(startpos, i - startpos));
                startpos = i + 1;
            }
        }
        print(c);
    }

    // Example 2:   Using very old strtok function ----------------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Split string into parts in a simple for loop
#pragma warning(suppress : 4996)
        for (char* token = std::strtok(const_cast<char*>(stringToSplit.data()), ","); token != nullptr; token = std::strtok(nullptr, ",")) {
            c.push_back(token);
        }

        print(c);
    }

    // Example 3:   Very often used std::getline with additional istringstream ------------------------------------------------
    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
        Container c{};

        // Put string in an std::istringstream
        std::istringstream iss{ stringToSplit };

        // Extract string parts in simple for loop
        for (std::string part{}; std::getline(iss, part, ','); c.push_back(part))
            ;

        print(c);
    }

    // Example 4:   Most flexible iterator solution  ------------------------------------------------

    {
        // Our string that we want to split
        std::string stringToSplit{ "aaa,bbb,ccc,ddd" };


        Container c(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});
        //
        // Everything done already with range constructor. No additional code needed.
        //

        print(c);


        // Works also with other containers in the same way
        std::forward_list<std::string> c2(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});

        print(c2);

        // And works with algorithms
        std::deque<std::string> c3{};
        std::copy(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {}, std::back_inserter(c3));

        print(c3);
    }
    return 0;
}

快乐编码!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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