0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DelphiにてCSVパーサーの実装

Last updated at Posted at 2025-01-24

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?