Qtの基礎 - CSVファイル

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

CSV (Comma-Separated Values) ファイルは、テキストベースのデータ形式である。
各行がデータレコードを表しており、フィールドはカンマで区切られている。

Qtでは、CSVファイルの読み書きを簡単に行うことができる。

主にQFileクラス、QTextStreamクラス、QStringListを使用してCSVファイルを操作する。
QFileクラスはファイルのオープン・クローズを行い、QTextStreamクラスはテキストの読み書きを行う。
QStringListクラスは、CSVの各行を分割して扱う場合に便利である。

ファイルの読み込み時は、QFileオブジェクトでファイルを開き、QTextStreamオブジェクトを使用して1行ずつ読み込む。
各レコードは、QString::splitメソッドでフィールドに分割することができる。

書き込み時も同様、QFileオブジェクトとQTextStreamオブジェクトを使用する。
データをカンマで連結して、QTextStreamクラスの<<演算子を使用して書き込む。

また、ファイルがオープンできない、書き込むことができない等の状況に対応するため、適切な例外処理やエラー処理を実装することが推奨される。

なお、CSVファイルの形式が統一されていない場合 (例: フィールド内にカンマが含まれる)、パースが複雑になることがある。
このような場合は、正規表現やより高度なパーシング技術が必要になることもある。


CSVファイル

以下の例では、CSVファイルを作成している。

処理の流れを以下に示す。

  1. 数値データを生成する。
    ここでは、sin曲線である を生成する。
  2. 数値データをテキストデータに変換する。
  3. ファイル保存ダイアログを開いて、保存するファイル名を入力する。
  4. 全テキストデータを書き込む。


 #include <QCoreApplication>
 #include <QFile>
 #include <QTextStream>
 #include <iostream>
 #include <vector>
 #include <limits>
 #include <cmath>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    // 数値データを生成 : sin曲線 y = sin(x)
    int iSize = 100;
    std::vector<double> lx(iSize, 0.0),
                        ly(iSize, 0.0);
 
    double dx = 2 * M_PI / iSize;
    for (int i = 0; i < iSize; i++) {
       lx[i] = i * dx;
       ly[i] = sin(lx[i]);
    }
 
    // 出力形式の選択
    int formatChoice;
    std::cout << "Choose output format:\n";
    std::cout << "1. Fixed-point notation\n";
    std::cout << "2. Scientific notation (e-notation)\n";
    std::cout << "Enter your choice (1 or 2): ";
    std::cin >> formatChoice;
 
    // 入力バッファをクリア
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    // 精度の設定
    int precision;
    std::cout << "Enter the number of decimal places (1-15): ";
    std::cin >> precision;
    precision = std::max(1, std::min(15, precision));  // 1から15の範囲に制限
 
    // 入力バッファをクリア
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
 
    // 数値データをテキストデータに変換
    QString     strFileData;
    QTextStream strStream(&strFileData);
    strStream.setRealNumberPrecision(precision);
 
    if (formatChoice == 1) {
       strStream.setRealNumberNotation(QTextStream::FixedNotation);
    }
    else {
       strStream.setRealNumberNotation(QTextStream::ScientificNotation);
    }
 
    for(int i = 0; i < iSize; i++) {
       strStream << lx[i] << ", " << ly[i] << "\n";
    }
 
    // ファイル名の入力
    std::cout << "Enter the file name to save the CSV data (e.g., data.csv): ";
    std::string fileName;
    std::getline(std::cin, fileName);
 
    QString FileName = QString::fromStdString(fileName);
 
    // 全テキストデータを書き込む
    QFile File(FileName);
 
    if (!File.open(QIODevice::WriteOnly)) { 
       std::cerr << "Unable to open file: " << File.errorString().toStdString() << std::endl;
       return -1;
    }
 
    QTextStream OutStream(&File);
    OutStream << strFileData;
 
    File.close();
 
    std::cout << "Data has been successfully saved to " << fileName << std::endl;
 
    return 0;
 }



CSVファイル (RFC4180準拠) の読み込み

RFC4180は、CSVファイルの標準的な形式を定義しており、これに従うことで互換性の高いCSV処理が可能になる。

以下の例では、RFC4180の規則に従ってCSVファイルを読み込んでいる。

  • ダブルクォートで囲まれたフィールド内のカンマと改行を正しく処理する。
  • フィールド内の二重引用符 ("") を単一の引用符 () として解釈する。
  • 各行の末尾の空白を除去する。


 #include <QFile>
 #include <QTextStream>
 #include <QStringList>
 #include <QDebug>
 
 QList<QStringList> readCSV(const QString &filename)
 {
    QList<QStringList> result;
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
       qDebug() << "ファイルを開けません:" << filename;
       return result;
    }
 
    QTextStream in(&file);
    in.setCodec("UTF-8");
 
    QString     line;
    QStringList fields;
    bool inQuotes = false;
    QString field;
 
    while (!in.atEnd()) {
       QChar ch;
       in >> ch;
 
       if (inQuotes) {
          if (ch == '"') {
             if (in.peek() == '"') {
                field += ch;
                in >> ch;  // 次の文字(二重引用符)を読み飛ばす
             }
             else {
                inQuotes = false;
             }
          }
          else {
             field += ch;
          }
       }
       else {
          if (ch == '"') {
             inQuotes = true;
          }
          else if (ch == ',' || ch == '\n' || in.atEnd()) {
             fields.append(field.trimmed());
             field.clear();
             if (ch == '\n' || in.atEnd()) {
                result.append(fields);
                fields.clear();
             }
          }
          else {
             field += ch;
          }
       }
    }
 
    file.close();
 
    return result;
 }



CSVファイル (RFC4180準拠) の書き込み

RFC4180は、CSVファイルの標準的な形式を定義しており、これに従うことで互換性の高いCSV処理が可能になる。

以下の例では、RFC4180の規則に従ってCSVファイルを書き込んでいる。

  • カンマ、引用符、改行を含むフィールドを適切にダブルクォートで囲む。
  • フィールド内の引用符を二重引用符 ("") にエスケープする。
  • 各行の終わりに改行を挿入する。


 #include <QFile>
 #include <QTextStream>
 #include <QStringList>
 #include <QDebug>
 
 bool writeCSV(const QString &filename, const QList<QStringList> &data)
 {
    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
       qDebug() << "ファイルを開けません:" << filename;
       return false;
    }
 
    QTextStream out(&file);
    out.setCodec("UTF-8");
 
    for (const QStringList &row : data) {
       for (int i = 0; i < row.size(); ++i) {
          QString field = row[i];
          bool needQuotes = field.contains(QRegExp("[,\"\n\r]"));
 
          if (needQuotes) {
             out << '"' << field.replace("\"", "\"\"") << '"';
          }
          else {
             out << field;
          }
 
          if (i < row.size() - 1) {
             out << ",";
          }
       }
       out << "\n";
    }
 
    file.close();
 
    return true;
 }



ストリーミング処理を使用したCSVファイルの読み込み

  • ストリーミング処理
    ファイル全体を1度にメモリに読み込むのではなく、1レコードずつ処理する。
    これにより、大規模なファイルでもメモリ使用量を抑えることができる。
  • コールバック関数
    各レコードの処理をコールバック関数として渡すことにより、柔軟な処理が可能になる。


 #include <QFile>
 #include <QTextStream>
 #include <QStringList>
 #include <functional>
 #include <QDebug>
 
 enum class CSVError {
    NoError,
    FileOpenError,
    ReadError,
    ParseError
 };
 
 class CSVReader
 {
 private:
    QString m_filename;
 
 public:
    CSVReader(const QString &filename) : m_filename(filename) {}
 
    CSVError readCSV(std::function<void(const QStringList&)> rowCallback)
    {
       QFile file(m_filename);
       if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
          qDebug() << "ファイルのオープンに失敗: " << m_filename;
          return CSVError::FileOpenError;
       }
 
       QTextStream in(&file);
       in.setCodec("UTF-8");
 
       QString line;
       QStringList fields;
       bool inQuotes = false;
       QString field;
       qint64 lineNumber = 0;
 
       while (!in.atEnd()) {
          QChar ch;
          in >> ch;
 
          if (in.status() != QTextStream::Ok) {
             qDebug() << "読み込みエラー:" << m_filename << "行:" << lineNumber;
             return CSVError::ReadError;
          }
 
          if (inQuotes) {
             if (ch == '"') {
                if (in.peek() == '"') {
                   field += ch;
                   in >> ch;  // 次の文字(二重引用符)を読み飛ばす
                }
                else {
                   inQuotes = false;
                }
             }
             else {
                field += ch;
             }
          }
          else {
             if (ch == '"') {
                inQuotes = true;
             }
             else if (ch == ',' || ch == '\n' || in.atEnd()) {
                fields.append(field.trimmed());
                field.clear();
                if (ch == '\n' || in.atEnd()) {
                   rowCallback(fields);
                   fields.clear();
                   lineNumber++;
                }
             }
             else {
                field += ch;
             }
          }
       }
 
       if (inQuotes) {
          qDebug() << "パースエラー: 閉じていない引用符があります。行:" << lineNumber;
          return CSVError::ParseError;
       }
 
       file.close();
 
       return CSVError::NoError;
    }
 };


 int main()
 {
    CSVReader reader("example.csv");
    CSVError error = reader.readCSV([](const QStringList& row) {
       // 各レコードの処理
       qDebug() << row;
    });
 
    switch (error) {
       case CSVError::NoError:
          qDebug() << "CSVファイルの読み込みに成功";
          break;
       case CSVError::FileOpenError:
          qDebug() << "ファイルのオープンに失敗";
          break;
       case CSVError::ReadError:
          qDebug() << "ファイルの読み込み中にエラーが発生";
          break;
       case CSVError::ParseError:
          qDebug() << "CSVのパース中にエラーが発生";
          break;
    }
 
    return 0;
 }



ストリーミング処理を使用したCSVファイルの書き込み

以下の例では、ストリーミング処理およびQtConcurrentを使用して非同期でCSVファイルを書き込みしている。

  • ストリーミング処理
    QTextStreamクラスを使用して、データを逐次的に書き込む。
    これにより、大規模なデータセットでもメモリ使用量を抑えることができる。
  • 非同期処理
    書き込み処理をバックグラウンドで行うことにより、メインスレッドのブロッキングを防ぎ、アプリケーションの応答性を向上させることができる。
  • キューイング
    enqueueRowメソッドを使用して、書き込む行をキューに追加する。
    これにより、メインスレッドはブロックされることなく、次の処理に進むことができる。
  • スレッドセーフ
    ミューテックスとウェイトコンディションを使用して、スレッド間の同期を行う。


※注意

  • メモリ使用量
    キューに大量のデータが蓄積される時、メモリ使用量が増大する可能性がある。
    必要に応じて、キューのサイズに制限を設けることを検討する。
  • エラー処理
    以下の例では、エラーが発生してもすぐには検出されない。
    リアルタイムでエラーを検出する場合は、追加機能が必要になる。


 #include <QFile>
 #include <QTextStream>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QMutex>
 #include <QQueue>
 #include <QDebug>
 
 enum class CSVError {
    NoError,
    FileOpenError,
    WriteError
 };

 class CSVWriter
 {
 private:
    QString             m_filename;
    QFile               m_file;
    QTextStream         m_stream;
    QMutex              m_mutex;
    QQueue<QStringList> m_queue;
    QWaitCondition      m_condition;
    QFuture<void>       m_future;
    bool                m_isRunning;
    CSVError            m_error = CSVError::NoError;
 
 private:
    void writeWorker()
    {
       while (true) {
          QStringList row;
          {
             QMutexLocker locker(&m_mutex);
             while (m_queue.isEmpty() && m_isRunning) {
                m_condition.wait(&m_mutex);
             }
 
             if (!m_isRunning && m_queue.isEmpty()) {
                break;
             }
             row = m_queue.dequeue();
          }
 
          if (!writeRow(row)) {
             m_error = CSVError::WriteError;
             break;
          }
       }
    }
 
    bool writeRow(const QStringList &row)
    {
       for (int i = 0; i < row.size(); i++) {
          if (!writeField(row[i])) {
             return false;
          }
          if (i < row.size() - 1) {
             m_stream << ",";
          }
       }
 
       m_stream << "\n";
 
       return m_stream.status() == QTextStream::Ok;
    }
 
    bool writeField(const QString &field)
    {
       bool needQuotes = field.contains(QRegExp("[,\"\n\r]"));
 
       if (needQuotes) {
          m_stream << '"' << field.replace("\"", "\"\"") << '"';
       }
       else {
          m_stream << field;
       }
 
       return m_stream.status() == QTextStream::Ok;
    }
 
 public:
    CSVWriter(const QString &filename) : m_filename(filename), m_isRunning(false) {}
 
    ~CSVWriter()
    {
       if (m_isRunning) {
          waitForFinished();
       }
    }
 
    CSVError open()
    {
       if (!m_file.open(QIODevice::WriteOnly | QIODevice::Text)) {
          qDebug() << "ファイルのオープンに失敗: " << m_filename;
          return CSVError::FileOpenError;
       }
       m_stream.setDevice(&m_file);
       m_stream.setCodec("UTF-8");
       m_isRunning = true;
       m_future = QtConcurrent::run(&CSVWriter::writeWorker, this);
 
       return CSVError::NoError;
    }
 
    void enqueueRow(const QStringList &row)
    {
       QMutexLocker locker(&m_mutex);
       m_queue.enqueue(row);
       m_condition.wakeOne();
    }
 
    void close()
    {
       {
          QMutexLocker locker(&m_mutex);
          m_isRunning = false;
          m_condition.wakeOne();
       }
       waitForFinished();
       m_file.close();
    }
 
    void waitForFinished()
    {
       m_future.waitForFinished();
    }
 
    bool hasError() const
    {
       return m_error != CSVError::NoError;
    }
 
    CSVError error() const
    {
       return m_error;
    }
 };


 int main()
 {
    CSVWriter writer("example.csv");
    CSVError error = writer.open();
    if (error != CSVError::NoError) {
       qDebug() << "ファイルのオープンに失敗";
       return -1;
    }
 
    QStringList headers = {"Name", "Age", "City"};
    writer.enqueueRow(headers);
 
    QList<QStringList> data = {
       {"John Doe", "30", "New York"},
       {"Jane Smith", "25", "Los Angeles"},
       {"Mike Johnson", "35", "Chicago"}
    };
 
    for (const auto &row : data) {
       writer.enqueueRow(row);
    }
 
    writer.close();
 
    if (writer.hasError()) {
       qDebug() << "CSVファイルの書き込み中にエラーが発生";
       return -1;
    }
 
    qDebug() << "CSVファイルの書き込みに成功";
 
    return 0;
 }



分割処理を使用したCSVファイルの読み込み

以下の例では、RFC4180に準拠したCSVファイルを分割読み込みしている。
分割処理は、大規模なCSVファイルを効率的に処理する場合に特に有効である。

  • 分割読み込み
    ファイルを1度に全て読み込むのではなく、指定されたチャンクサイズごとに読み込む。
    これにより、大規模なファイルでもメモリ使用量を抑えることができる。
  • 行番号の追跡
    各レコード番号を追跡して、コールバック関数に渡す。
    これにより、エラーが発生した場合に正確な位置を特定することができる。


※注意
チャンクサイズの選択はパフォーマンスに影響を与える可能性がある。
適切なサイズは使用環境やファイルサイズによって異なる場合がある。

 #include <QFile>
 #include <QTextStream>
 #include <functional>
 #include <QDebug>
 
 enum class CSVError {
    NoError,
    FileOpenError,
    ReadError,
    ParseError
 };
 
 class CSVReader
 {
 private:
    QString m_filename;
    qint64  m_chunkSize;
 
 private:
    QStringList parseLine(const QString &line)
    {
       QStringList fields;
       QString     field;
       bool        inQuotes = false;
 
       for (int i = 0; i < line.length(); i++) {
          if (line[i] == '"') {
             if (inQuotes && i + 1 < line.length() && line[i + 1] == '"') {
                field += '"';
                i++;
             }
             else {
                inQuotes = !inQuotes;
             }
          }
          else if (line[i] == ',' && !inQuotes) {
             fields.append(field);
             field.clear();
          }
          else {
             field += line[i];
          }
       }
       fields.append(field);
 
       return fields;
    }
 
 public:
    CSVReader(const QString &filename, qint64 chunkSize = 1024 * 1024) : m_filename(filename), m_chunkSize(chunkSize) {}
 
    CSVError readCSV(std::function<bool(const QStringList&, qint64)> rowCallback)
    {
       QFile file(m_filename);
       if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
          qDebug() << "ファイルのオープンに失敗: " << m_filename;
          return CSVError::FileOpenError;
       }
 
       QTextStream in(&file);
       in.setCodec("UTF-8");
 
       QString buffer;
       QString remainder;
       qint64  lineNumber = 0;
       bool    inQuotes   = false;
 
       while (!in.atEnd()) {
          buffer = remainder + in.read(m_chunkSize);
          remainder.clear();
 
          int start = 0;
          for (int i = 0; i < buffer.length(); i++) {
             if (buffer[i] == '"') {
                inQuotes = !inQuotes;
             }
             else if (buffer[i] == '\n' && !inQuotes) {
                QString line = buffer.mid(start, i - start);
                QStringList fields = parseLine(line);
 
                if (fields.isEmpty() && !line.isEmpty()) {
                   qDebug() << "パースエラー: 行" << lineNumber + 1;
                   return CSVError::ParseError;
                }
 
                if (!rowCallback(fields, lineNumber)) {
                   // ユーザが処理を中断した場合
                   return CSVError::NoError;
                }
 
                start = i + 1;
                lineNumber++;
             }
          }
 
          remainder = buffer.mid(start);
       }
 
       if (!remainder.isEmpty()) {
          QStringList fields = parseLine(remainder);
          if (!fields.isEmpty()) {
             if (!rowCallback(fields, lineNumber)) {
                // ユーザが処理を中断した場合
                return CSVError::NoError;
             }
          }
          else if (!remainder.trimmed().isEmpty()) {
             qDebug() << "パースエラー: 最終行";
             return CSVError::ParseError;
          }
       }
 
       if (inQuotes) {
          qDebug() << "パースエラー: 閉じていない引用符があります";
          return CSVError::ParseError;
       }
 
       return CSVError::NoError;
    }
 };


 int main()
 {
    CSVReader reader("example.csv");
    CSVError error = reader.readCSV([](const QStringList& row, qint64 lineNumber) {
       qDebug() << "行" << lineNumber + 1 << ":" << row;
 
       // 例: 100行まで読み込みして中断
       if (lineNumber >= 99) {
          qDebug() << "100行読み込んだので中断します";
          return false;
       }
 
       return true; // 続けて読み込む
    });
 
    switch (error) {
       case CSVError::NoError:
          qDebug() << "CSVファイルの読み込みに成功";
          break;
       case CSVError::FileOpenError:
          qDebug() << "ファイルのオープンに失敗";
          break;
       case CSVError::ReadError:
          qDebug() << "ファイルの読み込み中にエラーが発生";
          break;
       case CSVError::ParseError:
          qDebug() << "CSVのパース中にエラーが発生";
          break;
    }
 
    return 0;
 }