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

Posted

技术标签:

【中文标题】从逗号分隔的文件中读取到对象的向量【英文标题】:Read from comma separated file into vector of objects 【发布时间】:2021-12-27 15:59:16 【问题描述】:

我做了一个简单的 C++ 程序来获得 C++ 知识。这是一个最终存储和读取文件的游戏。分数,姓名等。 在文件的每一行都存储了 Player 对象的内容。

例如:ID 年龄名称等

我现在想在文件中更改为逗号分隔,但后来我遇到了如何读取每一行并将 Player 对象写入 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 对象的好方法。

【问题讨论】:

在循环条件中使用(in.good()),当文件到达末尾时,您将使用temp.pID 中的默认值推送temp 您可以使用 getline 并指定逗号作为分隔符。 请参阅下面的答案,其中包含非常详细的分步说明以及许多代码示例。 【参考方案1】:

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

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

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

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

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

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 &gt;&gt; id &gt;&gt; name &gt;&gt; age &gt;&gt; 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 个步骤:

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

那么,让我们一步步解决这个问题。

读一整行很容易。为此,我们有函数std::getline。它将从流(从任何 istream,如 std::cinstd::ifstreamstd::istringstream)中读取一行(在文本中直到行字符 '\n')并将其存储在std::string 变量。请阅读 CPP 参考 here 中的函数说明。

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

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

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

std::istringstream iss(line);

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

不幸的是,第三个也是最后一个也是必要的。现在我们有一堆子字符串。但是我们也有 3 个数字作为属性。 “ID”是unsigned long,“Age”是int,“Score”是double,所以我们需要使用字符串转换函数将子字符串转换为数字:std::stoulstd::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++ 是一种面向对象的语言。这意味着我们不仅将数据放入 Player 结构体中,还将对这些数据进行操作的方法放入其中。

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

幸运的是,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';
    

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

我们甚至可以领先一步。还有播放器列表,ste::vector&lt;Player&gt; 可以封装在一个类中,并使用 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;

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

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

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

替代品

    手工制作,多种变体,使用指针或迭代器,可能难以开发且容易出错。 使用旧式std::strtok 函数。也许不安全。也许不应该再使用了 std::getline。最常用的实现。但实际上是一种“误用”,并不那么灵活 使用专门为此目的开发的专用现代功能,最灵活且最适合 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;

编码愉快!

【讨论】:

感谢@Armin,这就是 *** 的强大之处。所有有知识的人都愿意帮助和指导!【参考方案2】:

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

read .dat file in c++ and create to multiple data types

#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 ) );
    

这个也可能适用:

Best way to read a files contents and separate different data types into separate vectors in 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 );
    

【讨论】:

【参考方案3】:

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

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

C++ fstream reference

【讨论】:

以上是关于从逗号分隔的文件中读取到对象的向量的主要内容,如果未能解决你的问题,请参考以下文章

Python - 从文本文件中读取逗号分隔值,然后将结果输出到文本文件[关闭]

将逗号分隔的数据分配给向量

需要来自 txt 文件的数据,以逗号分隔,以使用现有类中的对象填充数组列表

Visual Basic 中读取逗号分隔的文本文件

如何从字符向量中解析 CSV 数据以提取数据框?

gnuplot 如何从文件读取数据