正在分析字段中带有逗号的csv

Parsing a csv with comma in field

本文关键字:csv 字段      更新时间:2023-10-16

我正试图使用以下数据的csv创建一个对象

Alonso,Fernando,21,31,29,2,Racing
Dhoni,Mahendra Singh,22,30,4,26,Cricket
Wade,Dwyane,23,29.9,18.9,11,Basketball
Anthony,Carmelo,24,29.4,21.4,8,Basketball
Klitschko,Wladimir,25,28,24,4,Boxing
Manning,Peyton,26,27.1,15.1,12,Football
Stoudemire,Amar'e,27,26.7,21.7,5,Basketball
"Earnhardt, Jr.",Dale,28,25.9,14.9,11,Racing
Howard,Dwight,29,25.5,20.5,5,Basketball
Lee,Cliff,30,25.3,25.1,0.2,Baseball
Mauer,Joe,31,24.8,23,1.8,Baseball
Cabrera,Miguel,32,24.6,22.6,2,Baseball
Greinke,Zack,33,24.5,24.4,50,Baseball
Sharapova,Maria,34,24.4,2.4,22,Tennis
Jeter,Derek,35,24.3,15.3,9,Baseball

我使用以下代码来解析它:

void AthleteDatabase::createDatabase(void)
{
    ifstream inFile(INPUT_FILE.c_str());
    string inputString;
    if(!inFile)
    {
        cout << "Error opening file for input: " << INPUT_FILE << endl;
    }
    else
    {
        getline(inFile, inputString);
        while(inFile)
        {
            istringstream s(inputString);
            string field;
            string athleteArray[7];
            int counter = 0;
            while(getline(s, field, ','))
            {
                athleteArray[counter] = field;
                counter++;
            }
            string lastName = athleteArray[0];
            string firstName = athleteArray[1];
            int rank = atoi(athleteArray[2].c_str());
            float totalEarnings = strtof(athleteArray[3].c_str(), NULL);
            float salary = strtof(athleteArray[4].c_str(), NULL);
            float endorsements = strtof(athleteArray[5].c_str(), NULL);
            string sport = athleteArray[6];
            Athlete anAthlete(lastName, firstName, rank,
                              totalEarnings, salary, endorsements, sport);
            athleteDatabaseBST.add(anAthlete);
            display(anAthlete);
            getline(inFile, inputString);
        }
        inFile.close();
    }
}

我的代码在线中断:

"Earnhardt, Jr.",Dale,28,25.9,14.9,11,Racing

显然是因为那些名言。有更好的方法来处理这个问题吗?我对C++还很陌生,所以如果有任何帮助,我们将不胜感激。

我建议使用合适的CSV解析器。你可以在前面这个问题的答案中找到一些,或者在谷歌上搜索一个。

如果你坚持自己滚动,那么最简单的方法可能是从基础开始,将其设计为一个有限状态机,一次处理一个字符的输入。展望未来,您基本上需要两种状态:"读取正常输入"answers"读取带引号的字符串"。如果你不想使用前瞻性,你可以用更多的状态来实现,例如:

  • 初始状态:如果下一个字符是引号,则切换到状态引号字段;否则就表现为,就好像处于状态未引用字段

  • 无引号字段:如果下一个字符是EOF,则结束解析;否则,如果是新行,则启动新行并切换到初始状态;否则,如果是分隔符(逗号),则在同一行中启动一个新字段,并切换到初始状态;否则将字符附加到当前字段,并保持状态未引用字段。(可选地,如果字符是引号,则发出解析错误信号。)

  • 带引号字段:如果下一个字符是EOF,则表示解析错误;否则,如果是引号,则切换到状态结束引号;否则将字符附加到当前字段,并保持状态引用字段

  • 结束引号:如果下一个字符是引号,则将其附加到当前字段并返回到状态引号字段;否则,如果它是逗号、换行符或EOF,则表现为处于状态未引用字段;else信号解析错误。

(这是"传统"CSV,如RFC 4180中所述,其中引用字段中的引号通过加倍来转义。添加对反斜杠转义的支持(在CSV格式的一些相当常见的变体中使用)只是一个练习。它需要一个或两个以上的状态,这取决于您是希望支持带引号或不带引号的字符串中的反斜杠,还是同时支持传统转义和反斜杠转义。)

在高级脚本语言中,这种逐个字符的迭代确实效率很低,但由于您正在编写C++,它所需要的只是一些半吊子的I/O缓冲和一个相当高效的字符串附加操作。

您必须使用bool标志和累积下一个字段内容的std::string逐字符解析每一行;而不是像你那样费力地前进到下一个逗号。

最初,bool标志为false,您可以逐个字符地对整行进行迭代。引号字符翻转bool标志。逗号字符仅当bool标志为false时获取std::string的累积内容并将其保存为行上的下一个字段,并将std::string清除为空,为下一个域做好准备。否则,字符将被附加到缓冲区中。

这是算法的基本轮廓,还有一些小细节,你应该能够自己充实。还有其他几种方法可以做到这一点,效率略高,但对于像你这样的初学者来说,这种方法是最容易实现的。

简单答案:使用不同的分隔符。如果你使用'|'这样的东西来代替,那么所有的东西都会更容易解析

Stoudemire,Amar'e|27|26.7|21.7|5|Basketball
Earnhardt, Jr.|Dale|28|25.9|14.9|11|Racing

其他任何可能需要解析文件的应用程序的优势也同样清晰。

如果需要使用逗号,那么您必须根据字段的第一个字符有条件地获取字段:

std::istream& nextField(std::istringstream& s, std::string& field)
{
    char c;
    if (s >> c) {
        if (c == '"') {
            // using " as the delimeter
            getline(s, field, '"');
            return s >> c; // for the subsequent comma
                           // could potentially assert for error-checking
        }
        else if (c == ',') {
            // handle empty field case
            field = "";
        }
        else {
            // normal case, but prepend c
            getline(s, field, ',');
            field = c + field;
        }
    }
    return s;
}

用作getline:的替代品

while (nextField(s, field)) {
    athleteVec.push_back(field); // prefer vector to array
}

如果我们有一个未终止的带引号的字符串:,甚至可以通过继续使用getline来稍微简化这个逻辑

std::istream& nextField(std::istringstream& s, std::string& field)
{
    if (std::getline(s, field, ',')) {
        while (s && field[0] == '"' && field[field.size() - 1] != '"') {
            std::string next;
            std::getline(s, next, ',');
            field += ',' + next;
        }
        if (field[0] == '"' && field[field.size() - 1] == '"') {
            field = field.substr(1, field.size() - 2);
        }
    }
    return s;
}

我同意Imari的回答,为什么要重新发明轮子?话虽如此,您是否考虑过regex?我相信这个答案可以用来完成你想要的,然后再做一些。