使用getline读取.csv文件时遇到问题

时间:2019-12-01 05:49:41

标签: c++ csv

程序的一部分包括尝试读取.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);
}

3 个答案:

答案 0 :(得分:1)

您的程序执行了错误的错误检查。它尝试读取文件末尾的内容,失败,忽略错误,然后尝试将不存在的输入字符串转换为double。这将导致异常。

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

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

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

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

答案 1 :(得分:1)

除了@n. 'pronouns' m.指出的无法验证getline返回值的问题外,您还会很快遇到尝试通过连续调用{{ 1}}使用getline作为定界符(或任何其他非换行定界符)。当您指定除','以外的分隔符时,'\n'将在尝试读取所有字段时忽略getline,寻找下一个'\n'

相反,通常是从.csv读取的行中创建一个',',然后使用stringstream和一个定界符从stringstream解析您需要的内容。为什么?您在getline中只有一行数据,因此stringstream必须在读取最后一个字段后停止-因为getline将为空...因为您未读在所有字段中,您都可以使用字段计数器和stringstream的临时实例来填充数据。您可以简单地App将从字符串流读取的数据分离为正确的成员变量。

您已经在使用switch(fieldcount) {...},也可以使用std::string并使用#include <vector>来存储,而不是一个普通的旧数组。您可以简单地将std::vector<App>作为读取函数的返回类型,并在读取.csv文件时填充向量,然后返回该向量以在程序的其他地方使用。

将各个部分放在一起,您可以按以下方式定义读取功能:

std::vector<App>

现在只需声明一个/* 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创建一个std::stringstream,一个line来保存每个字段并循环从字符串流中获取每个字段,例如

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 */ ... 即可将其分配给正确的成员变量,然后在填写第4 个字段后,添加{ {1}}到您的向量,例如

switch

读取循环中保留的所有内容都是更新字段计数器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 */ } 调用read函数,类似于:

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

在一个简短的示例中将其全部放入,您可以执行以下操作以将googleplay .csv中的所有所需信息读入main()的向量中,

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

使用/输出示例

使用文件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 input\n" <<
                     "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向量而不是普通的旧数组的优点。如果您还有其他问题,请告诉我。

答案 2 :(得分:1)

尽管答案已经被接受,但我想展示一种更“现代”的C ++方法。

如果您可以研究此解决方案并在将来尝试使用某些功能,我会很高兴。

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

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

在提取器中,我们将使用现代C ++算法将字符串拆分为标记。为此,我们有std::sregex_token_iterator。并且因为有一个专用的功能用于此目的,我们应该使用它。而且,这非常简单。

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

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

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

对于演示输出,我也覆盖了inserter运算符。现在,您可以对类型为App的变量使用除数运算符和插入符(“ >>”和“ <<”),就像对任何其他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;
}

可惜没人会读这个。 。

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