[英]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 的好方法。
我在这里提供了类似的解决方案:
#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++(面向对象)方法。
因此,我们将 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 个步骤:
std::string
所以,让我们一步一步解决这个问题。
读一整行很容易。 为此,我们有 function std::getline
。 它将从 stream (从任何 istream,如std::cin
、 std::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::istringstream
将std::string
转换为 stream。 您可以简单地定义这种类型的变量并将刚刚读取的字符串作为参数传递给它的构造函数。 例如:
std::istringstream iss(line);
现在我们也可以通过这个“iss”来使用所有的 iostream 函数。 凉爽的。 我们将使用带有 ',' 分隔符的std::getline
并接收 substring。
不幸的是,第三个也是最后一个也是必要的。 现在我们有一堆子字符串。 但是我们也有 3 个数字作为属性。 “ID”是unsigned long
,“Age”是int
,“Score”是double
,所以我们需要使用字符串转换函数将 substring 转换为数字: 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++ 是一种面向 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;
}
如果您能看到简单而强大的解决方案,我会很高兴。
最后,正如承诺的那样。 将字符串拆分为子字符串的一些其他方法:
将字符串拆分为标记是一项非常古老的任务。 有许多可用的解决方案。 都有不同的属性。 有些难以理解,有些难以开发,有些更复杂,更慢或更快或更灵活或不灵活。
备择方案
std::strtok
function。 也许不安全。 也许不应该再使用了std::getline
。 最常用的实现。 但实际上是一种“误用”,并没有那么灵活请在一段代码中查看 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.