見出し画像

DelphiにてCSVパーサーの実装

CSVは単純なフォーマットに見えて、実際の実装では様々な考慮が必要になります。今回は、引用符で囲まれたフィールドや特殊文字を正しく処理できる堅牢なCSVパーサーの実装について解説します。

実装の要件

このパーサーは以下の要件を満たすように設計されています:

  1. 基本的なカンマ区切りの処理

  2. 引用符で囲まれたフィールドの対応

  3. フィールド内のエスケープされた引用符("")の処理

  4. 不正なフォーマットの検出とエラー処理

コードの詳細解説

function ParseCSVLineStrict(const Line: string): TStringList;
{ 
  【仕様】
   - 1行分の CSV データをパースして TStringList にフィールドごと格納します。
   - RFC 4180 的なルール:
     1. フィールドが " で始まれば、引用符で囲まれたフィールドとして扱う。
        - 終端の " が見つかるまで取り込み、内部の "" を実際の " (一つ) として扱う。
        - 終端の " が存在しない場合はエラー。
     2. フィールドが " で始まらなければ、引用符なしフィールドとして扱う。
        - 途中で " が出現したら不正 (エラー)。
     3. カンマ(,) が出現したらフィールドを区切る (引用符外にいるときだけ)。
     4. 1行末まで読んで、最後のフィールドも TStringList に追加。
     5. 不正構文があった場合は例外を発生させる。呼び出し側で try..except で捕捉しエラー処理を行う。
}
var
  i, Len: Integer;
  Ch: Char;
  CurrentField: string;
  InQuotes: Boolean;             // 現在 " の中(引用符付きフィールド中)か?
  StartedWithQuote: Boolean;     // このフィールドが " で始まったかどうか
begin
  Result := TStringList.Create;
  CurrentField := '';
  InQuotes := False;
  StartedWithQuote := False;
  i := 1;
  Len := Length(Line);

  while i <= Len do
  begin
    Ch := Line[i];

    // ===================================
    // (1) フィールドの最初に " で始まる → 引用符付きフィールド開始
    // ===================================
    if (not InQuotes) and (CurrentField = '') and (Ch = '"') then
    begin
      InQuotes := True;
      StartedWithQuote := True;
      Inc(i);
      Continue;
    end;

    // ===================================
    // 引用符付きフィールド(InQuotes = True)の処理
    // ===================================
    if InQuotes then
    begin
      if Ch = '"' then
      begin
        // 次の文字がまた " なら "" → 実際の " として扱う
        if (i < Len) and (Line[i+1] = '"') then
        begin
          CurrentField := CurrentField + '"'; // 1つの " をフィールドに追加
          Inc(i, 2);                          // "" の2文字分消費
        end
        else
        begin
          // それ以外なら引用符が終わり → フィールド終端
          InQuotes := False;
          Inc(i); // 終了クオートを消費
        end;
      end
      else
      begin
        // 引用符内の通常文字として取り込む
        CurrentField := CurrentField + Ch;
        Inc(i);
      end;
    end
    // ===================================
    // 非引用符フィールド(InQuotes = False)の処理
    // ===================================
    else
    begin
      if Ch = ',' then
      begin
        // フィールド区切り
        Result.Add(CurrentField);
        CurrentField := '';
        StartedWithQuote := False;
        Inc(i);
      end
      else if Ch = '"' then
      begin
        // 非引用符フィールド途中に " が出現 → RFC では不正
        // (※もっとゆるいパースをしたいならここを変更)
        if CurrentField <> '' then
        begin
          Result.Free;
          raise Exception.CreateFmt(
            'CSV パースエラー: フィールド途中で不正な引用符が出現しました: [%s]',
            [Line]
          );
        end
        else
        begin
          // もし CurrentField が空でここに来た → 
          // これは「次のフィールドが引用符付きフィールドの開始」と解釈する手もある。
          // しかし一般的には「カンマを挟まずに " が出てきたら不正」とする場合が多い。
          // RFC 4180 的には「フィールド先頭から始まる場合のみ引用符フィールド」。
          InQuotes := True;
          StartedWithQuote := True;
        end;
        Inc(i);
      end
      else
      begin
        // 通常文字
        CurrentField := CurrentField + Ch;
        Inc(i);
      end;
    end;
  end;

  // ループ終了 → 最後のフィールド追加
  Result.Add(CurrentField);

  // 引用符が閉じていない
  if InQuotes then
  begin
    Result.Free;
    raise Exception.CreateFmt(
      'CSV パースエラー: 引用符が閉じられていません: [%s]',
      [Line]
    );
  end;
end;

主要な処理パターン

1.引用符開始の検出
2.引用符内の処理

  • エスケープされた引用符("")の検出と処理

  • 引用符の終わりの検出

3.引用符外の処理

  • カンマによるフィールドの区切り

  • 不正な引用符の検出

エラー処理

パーサーは以下の場合にエラーを発生させます:
1.フィールド中に不正な引用符が現れた場合

raise Exception.CreateFmt('CSV パースエラー: 不正な引用符が含まれています: %s', [Line]);

2.引用符が閉じられていない場合

if InQuotes then
  raise Exception.CreateFmt('CSV パースエラー: 引用符が閉じられていません: %s', [Line]);

使用例

var
  Parser: TStringList;
begin
  try
    Parser := ParseCSVLineImproved('field1,"quoted,field",field3');
    // 結果: ['field1', 'quoted,field', 'field3']
  finally
    Parser.Free;
  end;
end;

実装のポイント

1.状態管理の重要性:

  • 引用符の内外で異なる処理を行うための状態管理

  • フィールドの開始状態の追跡
    2.エラー処理

  • 不正なフォーマットの早期検出

  • 詳細なエラーメッセージの提供
    3.メモリ管理:

  • TStringListの適切な解放

  • エラー発生時のリソース解放

まとめ

CSVパーサーの実装では、単純な文字列分割以上の考慮が必要です。状態管理とエラー処理を適切に行うことで、より堅牢なパーサーを実現できます。このコードは実務で使用できる品質を目指して実装されています。


いいなと思ったら応援しよう!