使用getline读取.csv文件时出现问题

Having trouble using getline to read from a .csv file

本文关键字:问题 文件 getline 读取 csv 使用      更新时间:2023-10-16

程序的一部分是我尝试从.csv文件中读入行,并将其中的一部分存储到结构中。然而,当我试图执行下面显示的代码时,控制台中告诉我有一个无效的stod实例。有了这些信息,我进入调试器,发现文件中没有任何内容被读取到我创建的伪变量(part1 - part4)中,并且它们的值仍然是""

我正在阅读的.csv中的一行示例是:

"Photo Editor & Candy Camera & Grid & ScrapBook,ART_AND_DESIGN,4.1,159,19M,10;000+,Free,0,Everyone,Art & Design,January 7; 2018,1.0.0,4.0.3 and up"

我只想一直到它说"159"(也就是评论的数量)。

#include <iostream>
#include <cmath>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <string>
using namespace std;
struct App {
std::string name;
std::string category;
double rating;
int reviewNum;
};
void readData(App appList[], const int size) {
ifstream inFile;
inFile.open("googleplaystore.csv");
if(inFile.fail()) {
cout << "File open error!";
exit(0);  //Close program
}
for(int appIndex = 0; appIndex < size; appIndex++) {
string part1, part2, part3, part4, trash = "";
//Line1
getline(inFile, part1, ',');    //read until ,
getline(inFile, part2, ',');
getline(inFile, part3, ',');
getline(inFile, part4 , ',');
getline(inFile, trash);         //read until end of line
appList[appIndex].name = part1;
appList[appIndex].category = part2;
appList[appIndex].rating = stod(part3);
appList[appIndex].reviewNum = stoi(part4);
if(inFile.fail()) {
cout << "File read error!";
exit(0);  //Close program
}
}
int main()
{
cout << fixed << setprecision(1);
App appList[NUM_RECORDS] = {};
readData(appList, NUM_RECORDS);
}

您的程序执行错误检查。它尝试读取超过文件末尾的内容,但失败,忽略错误,然后尝试将不存在的输入字符串转换为double。出现异常时,此操作失败。

在程序尝试执行所有这些操作后,检查文件为时已晚。

在每个IO操作完成后立即检查其是否成功。

一种流行的方法是逐行读取输入,然后tgen分别解析每一行,例如下面的代码片段。

while (std::getline(infile, instring)) {
std::istringstream linestream (instring);
// read the linestream
}

除@n指出的问题外代词"m"。由于无法验证getline的返回,您将很快遇到尝试通过使用','作为分隔符(或任何其他非换行分隔符)连续调用getline来解析.csv文件的限制。当您指定除'n'之外的分隔符时,getline将在尝试读取所有字段时忽略'n',以查找下一个','

相反,通常从.csv中读取的行创建stringstream,然后使用getline和分隔符解析stringstream中需要的内容。为什么?stringstream中只有一行数据,因此getline必须在读取最后一个字段后停止——因为stringstream将为空。。。在您的情况下,由于您没有读取所有字段,因此可以使用字段计数器和App的临时实例来填充数据。您可以简单地使用switch(fieldcount) {...}将从字符串流读取的数据分离到正确的成员变量中。

您已经在使用std::string,您还可以使用#include <vector>并使用std::vector<App>作为存储,而不是一个普通的旧阵列。您可以简单地将std::vector<App>作为读取函数的返回类型,并在读取.csv文件时填充矢量,然后返回矢量以供程序中的其他地方使用。

把这些部分放在一起,你可以定义你的读取函数如下:

/* function reading csv and returning std::vector<App> containing
* first 4 fields from each line of input .csv
*/
std::vector<App> read_csv (const std::string& name)
{
std::vector<App> v {};          /* vector of App */
std::string line;               /* string to hold each line */
std::ifstream fin (name);       /* input file stream */
...

现在只需声明App的一个临时实例,从line创建一个std::stringstream,一个用于保存每个字段的std:string,并循环从字符串流中获取每个字段,例如

while (std::getline (fin, line)) {      /* read entire line into line */
App tmp;                            /* temporary instance to fill */
size_t nfield = 0;                  /* field counter for switch */
std::string field;                  /* string to hold each field */
std::stringstream ss (line);        /* create stringstream from line */
while (getline (ss, field, ',')) {  /* read each field from line */
...

现在您有了字段,只需在字段计数器上输入switch即可将其分配给正确的成员变量,并在填充第4字段后,将App的临时实例添加到向量中,例如

switch (nfield) {                           /* switch on nfield */
case 0: tmp.name = field; break;        /* fill name */
case 1: tmp.category = field; break;    /* fill category */
case 2: try {               /* convert field to double */
tmp.rating = stod (field);
}
catch (const std::exception & e) {
std::cerr << "error invalid tmp.rating: " << 
e.what() << 'n';
goto nextline;
}
break;
case 3: try {               /* convert field to int */
tmp.reviewNum = stoi (field);
v.push_back(tmp);
}
catch (const std::exception & e) {
std::cerr << "error invalid tmp.reviewNum: " << 
e.what() << 'n';
}
goto nextline;   /* all done with fields, get next line */
break;
}
...

读取循环中剩下的就是更新字段计数器nfield,并提供一个标签来打破嵌套循环和switch,例如

nfield++;   /* increment field counter */
}
nextline:;      /* label for nextline */
}

从文件中读取所有行后,只需返回向量:

return v;   /* return filled vector of App */
}

您可以从main()调用类似于的读取函数

/* fill vector of App from csv */
std::vector<App> appdata = read_csv (argv[1]);

在一个简短的例子中,您可以执行以下操作,将您的googleplay.csv中所有想要的信息读取到App,的向量中

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
struct App {
std::string name;
std::string category;
double rating;
int reviewNum;
};
/* function reading csv and returning std::vector<App> containing
* first 4 fields from each line of input .csv
*/
std::vector<App> read_csv (const std::string& name)
{
std::vector<App> v {};          /* vector of App */
std::string line;               /* string to hold each line */
std::ifstream fin (name);       /* input file stream */
while (std::getline (fin, line)) {      /* read entire line into line */
App tmp;                            /* temporary instance to fill */
size_t nfield = 0;                  /* field counter for switch */
std::string field;                  /* string to hold each field */
std::stringstream ss (line);        /* create stringstream from line */
while (getline (ss, field, ',')) {  /* read each field from line */
switch (nfield) {                           /* switch on nfield */
case 0: tmp.name = field; break;        /* fill name */
case 1: tmp.category = field; break;    /* fill category */
case 2: try {               /* convert field to double */
tmp.rating = stod (field);
}
catch (const std::exception & e) {
std::cerr << "error invalid tmp.rating: " << 
e.what() << 'n';
goto nextline;
}
break;
case 3: try {               /* convert field to int */
tmp.reviewNum = stoi (field);
v.push_back(tmp);
}
catch (const std::exception & e) {
std::cerr << "error invalid tmp.reviewNum: " << 
e.what() << 'n';
}
goto nextline;   /* all done with fields, get next line */
break;
}
nfield++;   /* increment field counter */
}
nextline:;      /* label for nextline */
}
return v;   /* return filled vector of App */
}
int main (int argc, char **argv) {
if (argc < 2) {
std::cerr << "error: insufficient inputn" <<
"usage: " << argv[0] << " <file>n";
return 1;
}
/* fill vector of App from csv */
std::vector<App> appdata = read_csv (argv[1]);
for (auto& v : appdata)     /* output results */
std::cout << "nname     : " << v.name
<< "ncategory : " << v.category
<< "nrating   : " << v.rating
<< "nreviewNum: " << v.reviewNum << 'n';
}

示例使用/输出

使用文件dat/googleplay.csv中提供的一行输入,您将收到程序的以下输出:

$ ./bin/read_googlecsv dat/googleplay.csv
name     : Photo Editor & Candy Camera & Grid & ScrapBook
category : ART_AND_DESIGN
rating   : 4.1
reviewNum: 159

读取每一行并使用std::stringstream从逗号分隔的值解析字段,可以解决在需要使用所有字段时将面临的许多问题。它还允许在每次调用getline时消耗整行输入,以防止在文件出现格式问题时部分读取一行。仔细考虑使用App向量而不是普通的旧数组的优点。如果您还有其他问题,请告诉我。

虽然答案已经被接受,但我想展示一种更"现代"的C++方法。

如果你能研究这个解决方案,并在未来尝试使用一些功能,我会很高兴。

在面向对象的世界中,我们使用类(或结构),并将对这些数据进行操作的数据和函数放在一个(封装的)对象中。

只有类应该知道如何读取和写入其数据。不是一些外部全局函数。因此,我为您的结构添加了2个成员函数。我已经重写了插入器和提取器运算符。

在提取器中,我们将使用现代C++算法,将字符串拆分为令牌。为此,我们有了std::sregex_token_iterator。因为有一个专门的功能,我们应该使用它。此外,它非常简单。

使用下面的一行,我们将整个字符串拆分为令牌,并将生成的令牌放入std::vector

std::vector token(std::sregex_token_iterator(line.begin(), line.end(), delimiter, -1), {});

然后,我们将结果数据复制到成员变量中。

对于演示输出,我还覆盖了插入器运算符。现在,您可以对App类型的变量使用extractor和inserter运算符(">>"answers"<<"),就像对任何其他C++积分变量一样。

总的来说,我们还使用了一种超简单的方法。首先,我们打开文件并检查是否可以。

然后,我们定义了一个变量"apps"(App的std::vector),并使用其范围构造函数和std::istream_operator来读取完整的文件。而且,由于应用程序有一个重写的提取器运算符,它知道如何读取并将为我们解析完整的CSV文件。

再次,非常简单和短的一行

std::vector apps(std::istream_iterator<App>(inFile), {});

将读取完整的源文件、所有行、解析行并将成员变量存储在生成的std::vector的单个App元素中。

请参阅下面的完整示例:

#include <string>
#include <iostream>
#include <vector>
#include <fstream>
#include <regex>
#include <iterator>
#include <algorithm>
std::regex delimiter{ "," };
struct App {
// The data. Member variables
std::string name{};
std::string category{};
double rating{};
int reviewNum{};
// Overwrite extractor operator
friend std::istream& operator >> (std::istream& is, App& app) {
// Read a complete line
if (std::string line{}; std::getline(is, line)) {
// Tokenize it
std::vector token(std::sregex_token_iterator(line.begin(), line.end(), delimiter, -1), {});
// If we read at least 4 tokens then assign the values to our struct
if (4U <= token.size()) {
// Now copy the data from the vector to our members
app.name = token[0]; app.category = token[1]; 
app.rating = std::stod(token[2]); app.reviewNum = std::stoi(token[2]);
}
}
return is;
}
// Overwrite inserter operator
friend std::ostream& operator << (std::ostream& os, const App& app) {
return os << "Name:     " << app.name << "nCategory: " << app.category
<< "nRating:   " << app.rating << "nReviews:  " << app.reviewNum;
}
};
int main() {
// Open file and check, if it could be opened
if (std::ifstream inFile("googleplaystore.csv"); inFile) {
// Define the variable and use range constructor to read and parse the complete file
std::vector apps(std::istream_iterator<App>(inFile), {});
// Show result to the user
std::copy(apps.begin(), apps.end(), std::ostream_iterator<App>(std::cout, "n"));
}
return 0;
}

真遗憾没有人会读这篇。

免责声明:这是纯粹的示例代码,没有生产力,因此没有错误处理。