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