QTextStream::skipWhiteSpace()だけじゃない!Qtで空白をスキップする代替テクニック

2025-05-26

具体的には、この関数が呼ばれると、QTextStream は現在の読み込み位置から、以下のような空白文字をスキップします。

  • キャリッジリターン (\r)
  • 改行 (\n または \r\n)
  • タブ (\t)
  • スペース ()

なぜ skipWhiteSpace() が必要か?

QTextStream を使ってテキストファイルを読み込む際、多くの場合、単語や数値、特定の区切り文字などに注目してデータを処理します。しかし、それらのデータの間に余分な空白文字が含まれていることがあります。

例えば、以下のようなテキストファイルがあったとします。

Hello   World
123 456

QTextStream のストリーミング演算子 (>>) を使って単語や数値を読み込む場合、通常は先頭の空白は自動的にスキップされます。しかし、文字単位で読み込んだり、特定の区切り文字を自分で処理したい場合など、明示的に空白をスキップする必要がある場合に skipWhiteSpace() が役立ちます。

QTextStream を使って、ファイルから1文字ずつ読み込みながら、途中の空白をスキップする例を以下に示します。

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    QFile file("input.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "ファイルを開けませんでした。";
        return 1;
    }

    QTextStream in(&file);

    qDebug() << "スキップなしで読み込み:";
    file.seek(0); // ファイルの先頭に戻る
    QChar c;
    while (!in.atEnd()) {
        in >> c; // 1文字ずつ読み込む
        qDebug() << "読み込んだ文字:" << c;
    }

    qDebug() << "\nskipWhiteSpace() を使用して読み込み:";
    file.seek(0); // ファイルの先頭に戻る
    while (!in.atEnd()) {
        in.skipWhiteSpace(); // 空白をスキップ
        if (in.atEnd()) { // スキップ後にファイルの終端に達した場合はループを抜ける
            break;
        }
        in >> c; // 空白以外の最初の文字を読み込む
        qDebug() << "読み込んだ文字 (空白スキップ後):" << c;
    }

    file.close();

    return a.exec();
}

input.txt の内容

A   B
C

上記のコードを実行すると、以下のようになります。

出力例

スキップなしで読み込み:
読み込んだ文字: "A"
読み込んだ文字: " "
読み込んだ文字: " "
読み込んだ文字: " "
読み込んだ文字: "B"
読み込んだ文字: "\n"
読み込んだ文字: "C"

skipWhiteSpace() を使用して読み込み:
読み込んだ文字 (空白スキップ後): "A"
読み込んだ文字 (空白スキップ後): "B"
読み込んだ文字 (空白スキップ後): "C"

この例からわかるように、skipWhiteSpace() を使用しない場合、スペースや改行も1文字として読み込まれますが、skipWhiteSpace() を使用すると、これらの空白文字が無視され、次の非空白文字から読み込みが開始されます。



skipWhiteSpace() の役割の誤解

よくあるエラー

  • 「改行コードをスキップしたいのに、skipWhiteSpace() ではうまくいかない」
  • skipWhiteSpace() を呼んだのに、特定の区切り文字(例:カンマ、コロンなど)がスキップされない!」

説明とトラブルシューティング
skipWhiteSpace() は、QChar::isSpace() が true を返す文字のみをスキップします。これは主に以下の文字を指します。

  • キャリッジリターン (\r)
  • 改行 (\n)
  • タブ (\t)
  • 半角スペース ()

「特定の区切り文字」や「改行コードをスキップしないケース」

  • readLine()readAll() といったメソッドは、内部的にストリームを読み進めるため、skipWhiteSpace() と組み合わせて使うと意図しない結果になることがあります。skipWhiteSpace() は通常、QCharchar などの文字単位での読み込みと組み合わせて使用するものです。
  • カンマ (,) やコロン (:) などは isSpace()false を返すため、skipWhiteSpace() ではスキップされません。これらの文字をスキップしたい場合は、手動で読み込んで破棄する必要があります(例: in >> ch; で読み込み、if (ch == ',') { /* 破棄 */ })。

トラブルシューティング

  • 特定の区切り文字をスキップしたい場合は、skipWhiteSpace() の代わりに、ループで目的の文字が見つかるまでストリームを読み進める処理を実装します。
  • スキップしたい文字が本当に「空白文字」なのか、QChar::isSpace() のドキュメントを確認しましょう。

ストリームの終端 (atEnd()) との組み合わせ

よくあるエラー

  • skipWhiteSpace() の後に atEnd() をチェックし忘れて、空のデータを読み込もうとしてクラッシュする。
  • skipWhiteSpace() を呼んだ後、atEnd()true を返しているのに、まだデータが残っているはずだ。

説明とトラブルシューティング
skipWhiteSpace() は、空白文字をスキップし、次の非空白文字の直前にストリームの位置を移動させます。もし、空白文字をスキップした結果、ファイルの終端に達した場合、atEnd()true を返します。

トラブルシューティング
skipWhiteSpace() を呼び出した後は、必ず atEnd() をチェックして、読み込みが可能な状態であることを確認する習慣をつけましょう。

// 悪い例: スキップ後にatEnd()のチェックがない
in.skipWhiteSpace();
in >> someData; // 空白の後にデータがない場合、問題が起こる可能性がある

// 良い例: スキップ後にatEnd()をチェックする
in.skipWhiteSpace();
if (!in.atEnd()) {
    in >> someData;
} else {
    // ファイルの終端に達したことを処理
    qDebug() << "ファイルの終端に達しました。";
}

バッファリングによる挙動のずれ

よくあるエラー

  • QTextStream と基盤の QIODevice (例: QFile) を同時に使って読み書きすると、skipWhiteSpace() の結果が不安定になる。

説明とトラブルシューティング
QTextStream は内部的にバッファリングを行います。QFile などの QIODevice を直接操作して読み書きを行うと、QTextStream の内部バッファと QFile の読み書き位置が同期されなくなり、skipWhiteSpace() を含むすべてのストリーム操作が予期せぬ結果になる可能性があります。

トラブルシューティング

  • もし、QFile の位置をリセットしたい場合は、QTextStream::seek(0) を使うか、QTextStream を再構築することを検討してください。
  • QTextStream を使っている間は、その QTextStream のみに依存して読み書き操作を行いましょう。 基盤となる QIODevice を直接操作するのは避けるべきです。

operator>> との相互作用の理解不足

よくあるエラー

  • operator>> (ストリーミング演算子) で文字列や数値を読み込もうとすると、なぜか期待する動作にならない。skipWhiteSpace() を呼んでも呼ばなくても同じ結果に見える。

説明とトラブルシューティング
QTextStreamoperator>> は、デフォルトで先頭の空白文字を自動的にスキップします。したがって、QStringint などの型にストリーミング演算子を使って読み込む場合、skipWhiteSpace() を明示的に呼び出す必要がないことがほとんどです。

トラブルシューニング

  • skipWhiteSpace() が真に役立つのは、以下のようなシナリオです。
    • 1文字ずつ QChar に読み込む際に、空白文字を明示的に無視したい場合。
    • 特定の非空白文字(例:# で始まるコメント行をスキップしたいが、その前に空白がある場合など)を処理する前に、ストリームを適切な位置に進めたい場合。
  • operator>> を使う場合、通常は skipWhiteSpace() は不要です。

エンコーディングの問題

よくあるエラー

説明とトラブルシューティング
QTextStream はデフォルトでシステムのロケールに合ったエンコーディングを使用しようとしますが、ファイルが異なるエンコーディングで保存されている場合、正しく文字を認識できません。空白文字もエンコーディングによって表現が異なる場合があります。

  • 特にBOM (Byte Order Mark) が含まれるUTF-16やUTF-32のファイルの場合、QTextStream はデフォルトで自動検出しますが、念のため確認しておくと良いでしょう。
  • QFile::open() の前に、QTextStream::setCodec() を使ってファイルの正しいエンコーディング(例: QTextCodec::codecForName("UTF-8")QTextCodec::codecForName("Shift-JIS") など)を設定しましょう。
  • Qtドキュメントの熟読
    QTextStream の詳細な動作はQtの公式ドキュメントに記載されています。特に atEnd(), pos(), operator>> との相互作用について再確認すると良いでしょう。
  • 小さなテストケースを作成
    問題を切り分けるために、最小限のコードとテストファイルで問題を再現させてみましょう。
  • デバッグ出力の活用
    qDebug() を使って、skipWhiteSpace() の前後でストリームの現在の位置 (QTextStream::pos()) や、読み込んだ文字 (QCharQString) を出力し、ストリームの動作を追跡しましょう。


例1: 空白をスキップして単語を1文字ずつ読み込む

この例では、ファイルから1文字ずつ読み込みながら、途中の空白文字を skipWhiteSpace() で無視する方法を示します。

input.txt の内容

Hello   World
Qt

C++ コード

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    // 入力ファイルを開く
    QFile file("input.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "エラー: input.txt を開けませんでした。";
        return 1;
    }

    QTextStream in(&file);

    qDebug() << "--- skipWhiteSpace() を使用して文字を読み込む ---";
    QChar ch;
    while (!in.atEnd()) {
        in.skipWhiteSpace(); // 次の非空白文字までスキップ

        if (in.atEnd()) { // スキップ後にファイルの終端に達したかチェック
            break;
        }

        in >> ch; // 非空白文字を読み込む
        qDebug() << "読み込んだ文字: '" << ch << "'";
    }

    file.close();

    return a.exec();
}

実行結果の例

--- skipWhiteSpace() を使用して文字を読み込む ---
読み込んだ文字: 'H'
読み込んだ文字: 'e'
読み込んだ文字: 'l'
読み込んだ文字: 'l'
読み込んだ文字: 'o'
読み込んだ文字: 'W'
読み込んだ文字: 'o'
読み込んだ文字: 'r'
読み込んだ文字: 'l'
読み込んだ文字: 'd'
読み込んだ文字: 'Q'
読み込んだ文字: 't'

この例では、"Hello""World" の間の複数のスペースや、"World""Qt" の間の改行が skipWhiteSpace() によってスキップされていることがわかります。in >> ch; は常に空白以外の文字を読み込みます。

例2: 特定の区切り文字と空白を同時に処理する

skipWhiteSpace() は空白文字のみをスキップするため、カンマなどの他の区切り文字は手動で処理する必要があります。この例では、空白とカンマで区切られた数値を読み込む方法を示します。

data.txt の内容

10 ,  20,30, 40

C++ コード

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    QFile file("data.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "エラー: data.txt を開けませんでした。";
        return 1;
    }

    QTextStream in(&file);
    QList<int> numbers;
    int value;
    QChar separator;

    qDebug() << "--- 数値と区切り文字を読み込む ---";

    while (!in.atEnd()) {
        in.skipWhiteSpace(); // 数値の前の空白をスキップ

        if (in.atEnd()) {
            break;
        }

        // 数値を読み込む (>> は自動的に先行する空白をスキップするが、ここでは明示的にskipWhiteSpace()を呼んでいる)
        in >> value;
        if (!in.status() == QTextStream::Ok) { // 読み込みエラーチェック
            qDebug() << "エラー: 数値の読み込みに失敗しました。";
            break;
        }
        numbers.append(value);
        qDebug() << "読み込んだ数値:" << value;

        in.skipWhiteSpace(); // 数値の後の空白をスキップ

        // 次の文字がカンマかチェックし、カンマなら読み飛ばす
        if (!in.atEnd()) {
            in >> separator; // 1文字読み込む
            if (separator != ',') {
                // カンマ以外の文字であれば、ストリームを戻すか、エラー処理を行う
                // QTextStream::seek() で位置を戻すことも可能だが、ここでは簡単のため無視
                qDebug() << "警告: 予期せぬ区切り文字 '" << separator << "' が見つかりました。";
                break; // 処理を終了
            }
        }
    }

    qDebug() << "すべての数値:" << numbers;

    file.close();

    return a.exec();
}

実行結果の例

--- 数値と区切り文字を読み込む ---
読み込んだ数値: 10
読み込んだ数値: 20
読み込んだ数値: 30
読み込んだ数値: 40
すべての数値: (10, 20, 30, 40)

この例では、skipWhiteSpace() を使って数値の前後の空白をスキップし、カンマは in >> separator; で明示的に読み込んで破棄しています。

ファイル内に # で始まるコメント行と、空白行が混在している場合に、それらをスキップして実データのみを読み込む例です。

config.txt の内容

# これはコメント行です
  # もう一つのコメント
ItemA = ValueA

ItemB = ValueB

C++ コード

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    QFile file("config.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "エラー: config.txt を開けませんでした。";
        return 1;
    }

    QTextStream in(&file);
    QString line;

    qDebug() << "--- コメント行と空白行をスキップして実データを読み込む ---";

    while (!in.atEnd()) {
        // まずは行の先頭の空白をスキップ
        in.skipWhiteSpace();

        // スキップ後にファイルの終端に達したかチェック
        if (in.atEnd()) {
            break;
        }

        // 現在の位置の文字が '#' なら、その行全体をスキップ
        // in.peekChar() はストリームの現在位置の文字を読み込まずに返す
        if (in.peekChar() == '#') {
            in.readLine(); // コメント行全体を読み飛ばす
            qDebug() << "コメント行をスキップしました。";
            continue; // 次のループへ
        }

        // それ以外の行(実データ行)を読み込む
        line = in.readLine().trimmed(); // 行を読み込み、前後の空白を削除
        if (!line.isEmpty()) { // 空行でなければ処理
            qDebug() << "読み込んだ実データ行:" << line;
            // ここで 'line' を解析する処理を行う (例: "ItemA = ValueA" をパース)
        }
    }

    file.close();

    return a.exec();
}

実行結果の例

--- コメント行と空白行をスキップして実データを読み込む ---
コメント行をスキップしました。
コメント行をスキップしました。
読み込んだ実データ行: "ItemA = ValueA"
読み込んだ実データ行: "ItemB = ValueB"

この例では、skipWhiteSpace() で行頭の空白をスキップし、peekChar() で次の文字が # かどうかを確認しています。# であれば readLine() でその行全体を読み飛ばし、そうでなければ実データ行として処理しています。



ストリーミング演算子 (operator>>) を利用する

QTextStream の最も一般的な使用方法の一つであるストリーミング演算子 (>>) は、デフォルトで先行する空白文字を自動的にスキップします。したがって、skipWhiteSpace() を明示的に呼び出す必要がないケースが非常に多いです。

適用シナリオ

  • 入力データがスペース、タブ、改行で適切に区切られている場合。
  • 数値 (int, double など) や文字列 (QString) を単語や区切り文字で区切って読み込みたい場合。

C++ コード例

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    QFile file("data_auto_skip.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "ファイルを開けませんでした。";
        return 1;
    }

    QTextStream in(&file);
    QString word1, word2;
    int num1, num2;

    // input.txt の内容: "Hello   World\n123  456"
    in >> word1 >> word2; // "Hello" と "World" が読み込まれる (間の空白は自動スキップ)
    qDebug() << "単語1:" << word1 << "単語2:" << word2;

    in >> num1 >> num2; // 123 と 456 が読み込まれる (間の空白は自動スキップ)
    qDebug() << "数値1:" << num1 << "数値2:" << num2;

    file.close();

    return a.exec();
}

data_auto_skip.txt の内容

Hello   World
123  456

出力

単語1: "Hello" 単語2: "World"
数値1: 123 数値2: 456

メリット
コードが簡潔で読みやすい。多くの基本的な読み込みニーズに対応。 デメリット: 空白以外の特定の区切り文字(例:カンマ)をスキップしたい場合は、この方法だけでは不十分。文字単位で厳密な制御が必要な場合には使えない。

QTextStream::readLine() と QString::trimmed() または QString::simplified() を利用する

行全体を読み込み、その後で文字列処理関数を使って行の前後の空白や、内部の連続する空白を処理する方法です。

適用シナリオ

  • コメント行など、特定の形式の行をスキップしたい場合。
  • 行の先頭や末尾の空白、または単語間の余分な空白を削除したい場合。
  • 1行ごとにデータを処理したい場合。

C++ コード例

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    QFile file("data_lines.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "ファイルを開けませんでした。";
        return 1;
    }

    QTextStream in(&file);
    QString line;

    // data_lines.txt の内容:
    // "  これはテストです  \n"
    // "\n"
    // "# コメント行\n"
    // "  数値: 123   456  \n"

    while (!in.atEnd()) {
        line = in.readLine(); // 1行読み込む

        // 空白のみの行や、完全に空の行をスキップ
        if (line.trimmed().isEmpty()) {
            qDebug() << "空行または空白のみの行をスキップしました。";
            continue;
        }

        // コメント行をスキップ (trimmed() 後の先頭文字で判断)
        if (line.trimmed().startsWith('#')) {
            qDebug() << "コメント行をスキップしました:" << line.trimmed();
            continue;
        }

        // 行の前後の空白を削除
        qDebug() << "読み込んだ行 (trimmed): '" << line.trimmed() << "'";

        // 連続する空白を単一のスペースに置き換え、前後の空白も削除
        qDebug() << "読み込んだ行 (simplified): '" << line.simplified() << "'";
    }

    file.close();

    return a.exec();
}

data_lines.txt の内容

  これはテストです  

# コメント行
  数値: 123   456  

出力

読み込んだ行 (trimmed): "これはテストです"
読み込んだ行 (simplified): "これはテストです"
空行または空白のみの行をスキップしました。
コメント行をスキップしました: "# コメント行"
読み込んだ行 (trimmed): "数値: 123   456"
読み込んだ行 (simplified): "数値: 123 456"

メリット
行単位での処理が容易。コメント行のスキップなど、特定の行形式のフィルタリングに便利。trimmed()simplified() で柔軟に空白を処理できる。 デメリット: 行内に含まれる空白以外の区切り文字を処理するには、さらに QString::split() などの処理が必要になる。

QTextStream::readChar() で1文字ずつ読み込み、QChar::isSpace() を使って手動で空白文字を判定・スキップする方法です。skipWhiteSpace() はこの処理の内部実装に近いものですが、より細かい制御が必要な場合に利用できます。

適用シナリオ

  • ストリームを非常に低レベルで制御したい場合。
  • 空白以外の特定の文字(例:複数行にまたがるコメント開始/終了記号)を読み飛ばしたい場合。
  • skipWhiteSpace() が想定する空白文字以外の文字もスキップしたい場合。

C++ コード例

#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QDebug>

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

    QFile file("data_manual_skip.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "ファイルを開けませんでした。";
        return 1;
    }

    QTextStream in(&file);
    QChar ch;
    QString currentWord;

    // data_manual_skip.txt の内容:
    // "データ /* コメント */ 値"

    qDebug() << "--- 手動で空白とコメントをスキップ ---";
    while (!in.atEnd()) {
        ch = in.peekChar(); // 文字を読み込まずに確認

        // 空白文字をスキップ
        if (ch.isSpace()) {
            in.readChar(); // 空白文字を読み飛ばす
            qDebug() << "スキップした空白: '" << ch << "'";
            continue;
        }

        // Cスタイルのコメントをスキップ (例: /* ... */)
        if (ch == '/') {
            in.readChar(); // '/' を読み飛ばす
            if (!in.atEnd() && in.peekChar() == '*') {
                in.readChar(); // '*' を読み飛ばす
                qDebug() << "コメント開始 '/*' を検出。";
                // コメント終了 '*/' が見つかるまで読み飛ばす
                while (!in.atEnd()) {
                    ch = in.readChar();
                    if (ch == '*' && !in.atEnd() && in.peekChar() == '/') {
                        in.readChar(); // '/' を読み飛ばす
                        qDebug() << "コメント終了 '*/' を検出。";
                        break;
                    }
                }
                continue;
            } else {
                // '/' はコメントではない (例: パスの一部など)
                // 処理を戻すか、通常の文字として扱う
                // この例では、readChar() で進んでしまったので、そのまま続行
                currentWord += '/';
                continue;
            }
        }

        // それ以外の文字は単語の一部として読み込む
        ch = in.readChar();
        currentWord += ch;
        qDebug() << "読み込んだ文字:" << ch << " 現在の単語:" << currentWord;
    }

    file.close();

    return a.exec();
}

data_manual_skip.txt の内容

データ /* コメント */ 値

出力例

--- 手動で空白とコメントをスキップ ---
読み込んだ文字: 'デ' 現在の単語: "デ"
読み込んだ文字: 'ー' 現在の単語: "デー"
読み込んだ文字: 'タ' 現在の単語: "データ"
スキップした空白: ' '
コメント開始 '/*' を検出。
スキップした空白: ' '
スキップした空白: ' '
コメント終了 '*/' を検出。
スキップした空白: ' '
読み込んだ文字: '値' 現在の単語: "データ値"

メリット
非常に柔軟で、どのようなパターンでもスキップロジックを実装できる。 デメリット: コードが複雑になりやすい。パフォーマンスが skipWhiteSpace()operator>> より劣る可能性がある。

  • skipWhiteSpace() が対象としない特定の区切り文字(カンマなど)や、カスタムなスキップパターン (C++コメントなど) の処理、または厳密な文字単位の制御が必要な場合
    QTextStream::peekChar(), QTextStream::readChar(), QChar::isSpace() などを組み合わせて、手動でスキップロジックを実装します。

  • 行単位での処理、行頭/行末の空白処理、コメント行のスキップ
    QTextStream::readLine()QString::trimmed()QString::simplified() を組み合わせるのが効果的です。

  • 最も一般的でシンプルなケース (空白で区切られた単語や数値の読み込み)
    QTextStream::operator>> を使うのが最も簡潔で推奨されます。skipWhiteSpace() を明示的に呼ぶ必要はありません。