从逗号分隔的文件中读取到对象的向量
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
但在某个时间点,我们注意到这种漂亮而简单的格式将不再适用。原因是带有提取运算符>>
的格式化输入函数将在空白处停止转换。这不适用于以下示例:
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 个步骤:
-
将完整的行读取为
std::string
使用逗号作为分隔符将该字符串拆分为子字符串
将子字符串转换为所需的格式,例如从字符串转换为数字年龄
那么,让我们一步步解决这个问题。
读一整行很容易。为此,我们有函数std::getline
。它将从流(从任何 istream,如 std::cin
、std::ifstream
或 std::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::istringstream
将std::string
转换为流。您可以简单地定义这种类型的变量并将刚刚读取的字符串作为参数传递给它的构造函数。例如:
std::istringstream iss(line);
现在我们可以通过这个“iss”来使用所有的 iostream 函数。凉爽的。我们将使用std::getline
和',' 分隔符并接收一个子字符串。
不幸的是,第三个也是最后一个也是必要的。现在我们有一堆子字符串。但是我们也有 3 个数字作为属性。 “ID”是unsigned long
,“Age”是int
,“Score”是double
,所以我们需要使用字符串转换函数将子字符串转换为数字:std::stoul
, std::stoi
和 std::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 功能和提取器运算符 >>
和插入器运算符 <<
完成的。但是,如何做到这一点?我们的 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';
它只是重用我们上面学到的一切。只要把它放在正确的地方。
我们甚至可以领先一步。还有播放器列表,ste::vector<Player>
可以封装在一个类中,并使用 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 - 从文本文件中读取逗号分隔值,然后将结果输出到文本文件[关闭]