在Qt中解析CSV文件

18

有人熟悉如何解析CSV文件并将其放入字符串列表中吗?目前我正在将整个CSV文件放入字符串列表中。我正在尝试弄清楚是否有一种方法可以仅获取第一列。

#include "searchwindow.h"
#include <QtGui/QApplication>

#include <QApplication>
#include <QStringList>
#include <QLineEdit>
#include <QCompleter>
#include <QHBoxLayout>
#include <QWidget>
#include <QLabel>

#include <qfile.h>
#include <QTextStream>


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QWidget *widget = new QWidget();
    QHBoxLayout *layout = new QHBoxLayout();

    QStringList wordList;

    QFile f("FlightParam.csv");
    if (f.open(QIODevice::ReadOnly))
    {
        //file opened successfully
        QString data;
        data = f.readAll();
        wordList = data.split(',');

        f.close();
    }

    QLabel *label = new QLabel("Select");
    QLineEdit *lineEdit = new QLineEdit;
    label->setBuddy(lineEdit);

    QCompleter *completer = new QCompleter(wordList);
    completer->setCaseSensitivity(Qt::CaseInsensitive); //Make caseInsensitive selection

    lineEdit->setCompleter(completer);

    layout->addWidget(label);
    layout->addWidget(lineEdit);

    widget->setLayout(layout);
    widget->showMaximized();

    return a.exec();
}
6个回答

26

给你:

FlightParam.csv

1,2,3,
4,5,6,
7,8,9,

main.cpp

#include <QFile>
#include <QStringList>
#include <QDebug>

int main()
{
    QFile file("FlightParam.csv");
    if (!file.open(QIODevice::ReadOnly)) {
        qDebug() << file.errorString();
        return 1;
    }

    QStringList wordList;
    while (!file.atEnd()) {
        QByteArray line = file.readLine();
        wordList.append(line.split(',').first());
    }

    qDebug() << wordList;

    return 0;
}

main.pro

TEMPLATE = app
TARGET = main
QT = core
SOURCES += main.cpp

构建和运行

qmake && make && ./main

输出

("1", "4", "7")

这个有用。唯一的问题是在我的文件中第一列是编号。我尝试用 .second 替换 wordList.append(line.split(',').first()),但它没有起作用。有没有另一个 Qt 内置函数可以让你拿到第二列? - user3878223
@user3878223: at(1). - László Papp
6
这种方法无法处理包含逗号的数值行。例如,在“1,“2, 3” ,4”的表格中,第二列是“2,3”。 - Gallaecio
2
@Gallaecio:确实。这里还有其他几个不错的答案,它们似乎更加努力地解析输入,可惜它们没有获得更多的投票来超过这个快速而简略的解决方案。 - timday
wordList.append(line.split(',').first()); 在遇到 "a",2,3 的第一个出现时会崩溃。 - Michał Leon

15
这是我通常使用的代码。我是作者,将其视为原样,公共领域(gist)。它具有与CodeLurker的代码类似的功能集和概念,只是状态机的表示方式不同,代码稍微简短一些。
bool readCSVRow (QTextStream &in, QStringList *row) {

    static const int delta[][5] = {
        //  ,    "   \n    ?  eof
        {   1,   2,  -1,   0,  -1  }, // 0: parsing (store char)
        {   1,   2,  -1,   0,  -1  }, // 1: parsing (store column)
        {   3,   4,   3,   3,  -2  }, // 2: quote entered (no-op)
        {   3,   4,   3,   3,  -2  }, // 3: parsing inside quotes (store char)
        {   1,   3,  -1,   0,  -1  }, // 4: quote exited (no-op)
        // -1: end of row, store column, success
        // -2: eof inside quotes
    };

    row->clear();

    if (in.atEnd())
        return false;

    int state = 0, t;
    char ch;
    QString cell;

    while (state >= 0) {

        if (in.atEnd())
            t = 4;
        else {
            in >> ch;
            if (ch == ',') t = 0;
            else if (ch == '\"') t = 1;
            else if (ch == '\n') t = 2;
            else t = 3;
        }

        state = delta[state][t];

        if (state == 0 || state == 3) {
            cell += ch;
        } else if (state == -1 || state == 1) {
            row->append(cell);
            cell = "";
        }

    }

    if (state == -2)
        throw runtime_error("End-of-file found while inside quotes.");

    return true;

}
  • 参数: in, 一个QTextStream
  • 参数: row, 一个QStringList,用于接收行数据。
  • 返回: 如果读取了一行,则为true,如果到达文件结尾,则为false
  • 抛出: 如果发生错误,则为std::runtime_error

它解析Excel风格的CSV文件,适当处理引号和双引号,并允许字段中包含换行符。只要您使用QFile :: Text打开文件,它就可以正确处理Windows和Unix换行符。我认为Qt不支持旧式Mac换行符,而且这个函数不支持二进制模式下未翻译的换行符,但是在大多数情况下,这应该不是问题。

其他注意事项:

  • 与CodeLurker的实现不同,如果在引号内到达EOF,则会故意失败。如果您将状态表中的-2更改为-1,则会容错。
  • x"y"z解析为xyz,不确定中间字符串引号的规则。我不知道这是否正确。
  • 性能和内存特征与CodeLurker相同(即非常好)。
  • 不支持Unicode(转换为ISO-5589-1),但更改为QChar应该很简单。

示例:

QFile csv(filename);
csv.open(QFile::ReadOnly | QFile::Text);

QTextStream in(&csv);
QStringList row;
while (readCSVRow(in, &row))
    qDebug() << row;

3
现在有一个知道状态机的人! - CodeLurker
@Jason C,你说得完全正确,但我认为控制字符不应该“属于”CSV。虽然没有明确禁止它们,但我从未遇到过包含任何<32的内容。至于Unicode,UTF-8符号都>127。 - Michał Leon
1
@MichałLeon 如果您在Excel或其他地方的单个单元格中放置多行并换行,它们可能会出现在那里。 - Jason C
@MichałLeon 用 QFile::Text 打开输入是否可以消除这个问题?https://doc.qt.io/qt-5/qiodevice.html#OpenModeFlag-enum - Jason C
1
@Jason C,“Excel中的单个单元格或其他内容” 将会 打破我的编辑。请毫不犹豫地拒绝我的编辑。顺便说一句,我刚刚检查了,在Linux上,“单元格内换行”可以正常工作,即使它是LF(0x0A)。 - Michał Leon
显示剩余2条评论

14
你所需要的是一个QTextStream类。它提供了各种读写文件的接口。
一个简单的例子:

What you are looking for is a QTextStream class. It provides all kind of interfaces for reading and writing files.

A simple example:

QStringList firstColumn;
QFile f1("h:/1.txt");
f1.open(QIODevice::ReadOnly);
QTextStream s1(&f1);
while (!s1.atEnd()){
  QString s=s1.readLine(); // reads line from file
  firstColumn.append(s.split(",").first()); // appends first column to list, ',' is separator
}
f1.close();

或者,是的,你可以像这样做,这样会产生相同的结果:

wordList = f.readAll().split(QRegExp("[\r\n]"),QString::SkipEmptyParts); //reading file and splitting it by lines
for (int i=0;i<wordList.count();i++) 
   wordList[i]=wordlist[i].split(",").first(); // replacing whole row with only first value
f.close();    

我认为QTextStream是不必要的复杂化,而且QRegExp版本相当丑陋。:-) - László Papp

12

有些人可能更喜欢这样做:

QStringList MainWindow::parseCSV(const QString &string)
{
    enum State {Normal, Quote} state = Normal;
    QStringList fields;
    QString value;

    for (int i = 0; i < string.size(); i++)
    {
        const QChar current = string.at(i);

        // Normal state
        if (state == Normal)
        {
            // Comma
            if (current == ',')
            {
                // Save field
                fields.append(value.trimmed());
                value.clear();
            }

            // Double-quote
            else if (current == '"')
            {
                state = Quote;
                value += current;
            }

            // Other character
            else
                value += current;
        }

        // In-quote state
        else if (state == Quote)
        {
            // Another double-quote
            if (current == '"')
            {
                if (i < string.size())
                {
                    // A double double-quote?
                    if (i+1 < string.size() && string.at(i+1) == '"')
                    {
                        value += '"';

                        // Skip a second quote character in a row
                        i++;
                    }
                    else
                    {
                        state = Normal;
                        value += '"';
                    }
                }
            }

            // Other character
            else
                value += current;
        }
    }

    if (!value.isEmpty())
        fields.append(value.trimmed());

    // Quotes are left in until here; so when fields are trimmed, only whitespace outside of
    // quotes is removed.  The outermost quotes are removed here.
    for (int i=0; i<fields.size(); ++i)
        if (fields[i].length()>=1 && fields[i].left(1)=='"')
        {
            fields[i]=fields[i].mid(1);
            if (fields[i].length()>=1 && fields[i].right(1)=='"')
                fields[i]=fields[i].left(fields[i].length()-1);
        }

    return fields;
}
  • 强大:能够正确处理带有逗号、双双引号(表示一个双引号字符)和空格的引用材料
  • 灵活:即使忘记了最后一个字符串上的最后一个引号,也不会失败,并且可以处理更复杂的CSV文件;让您一次只处理一行,而无需先将整个文件读入内存
  • 简单:只需将此状态机拖放到您的代码中,在QtCreator中右键单击函数名称,然后选择Refactor | Add private declaration,就可以开始使用它了。
  • 高效:比在每个字符上进行RegEx look-ahead更快地准确处理CSV行
  • 方便:不需要外部库
  • 易于阅读:代码直观,以防需要修改它。

编辑:我终于想到了在字段前后修剪空格。 不会修剪引号内部的任何空格或逗号。 否则,所有字段的开头和结尾都会修剪空白。 经过一段时间的思考,我想到了可以在字段周围留下引号的主意;因此可以修剪所有字段。 这样,只有在引号或文本之前和之后的空格才被删除。 然后添加了最后一步,以去除以引号开头和结尾的字段的引号。

这是一个更具挑战性的测试用例:

QStringList sl=
{
    "\"one\"",
    "  \" two \"\"\"  , \" and a half  ",
    "three  ",
    "\t  four"
};

for (int i=0; i < sl.size(); ++i)
    qDebug() << parseCSV(sl[i]);

这对应于文件

"one"
 " two """  , " and a half  
three  
<TAB>  four

其中 <TAB> 代表制表符;每行都会依次输入到 parseCSV() 中。不要像这样编写 .csv 文件!

其输出结果为(其中 qDebug() 用 \" 表示字符串中的引号,并将内容放在引号和括号中):

("one")
(" two \"", " and a half")
("three")
("four")

您可以看到,引号和额外的空格在“two”项目中被保留在引号内。对于“and a half”的错误情况,引号前面和最后一个单词后面的空格被删除,但其他空格未被删除。该程序中缺少终端空格可能是缺少终端引号的迹象。字段中不以引号开头或结尾的引号仅被视为字符串的一部分。如果没有引号,则不会从字段末尾删除引号。要检测此处的错误,只需检查以引号开头但不以引号结尾的字段和/或包含引号但不以引号开头和结尾的字段即可在最终循环中。

虽然这比您的测试案例所需的更多,但它仍是一个扎实的通用答案 - 或许适用于其他人。

摘自: https://github.com/hnaohiro/qt-csv/blob/master/csv.cpp


3
先生,我爱你。请娶我。这段代码太棒了。已点赞。 - GeneCode

8

尝试使用qtcsv库来读写csv文件。示例:

#include <QList>
#include <QStringList>
#include <QDir>
#include <QDebug>

#include "qtcsv/stringdata.h"
#include "qtcsv/reader.h"
#include "qtcsv/writer.h"

int main()
{
    // prepare data that you want to save to csv-file
    QStringList strList;
    strList << "one" << "two" << "three";

    QtCSV::StringData strData;
    strData.addRow(strList);
    strData.addEmptyRow();
    strData << strList << "this is the last row";

    // write to file
    QString filePath = QDir::currentPath() + "/test.csv";
    QtCSV::Writer::write(filePath, strData);

    // read data from file
    QList<QStringList> readData = QtCSV::Reader::readToList(filePath);
    for ( int i = 0; i < readData.size(); ++i )
    {
        qDebug() << readData.at(i).join(",");
    }

    return 0;
}

我试图使它变得小巧易用。请查看Readme文件以获取库的文档和其他代码示例。


1
lines = data.split('\n');

然后。
for line in lines
   column1.add(line.split(',')[0])

我不确定是否存在添加函数以将内容添加到数组中,让我们称其为第1列。


"lines" 是什么值类型?我想它是一个字符串列表。对吗? - user3878223
是的,它与您的单词列表变量相同 - 我只是提供了一个想法。 - Linh Nguyen
1
通常像这样的简单问题,算法(伪代码通常是一个好主意)并不是主要问题,而是实现... - dom0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接