DelphiにてCSVパーサーの実装
CSVは単純なフォーマットに見えて、実際の実装では様々な考慮が必要になります。今回は、引用符で囲まれたフィールドや特殊文字を正しく処理できる堅牢なCSVパーサーの実装について解説します。
実装の要件
このパーサーは以下の要件を満たすように設計されています:
基本的なカンマ区切りの処理
引用符で囲まれたフィールドの対応
フィールド内のエスケープされた引用符("")の処理
不正なフォーマットの検出とエラー処理
コードの詳細解説
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パーサーの実装では、単純な文字列分割以上の考慮が必要です。状態管理とエラー処理を適切に行うことで、より堅牢なパーサーを実現できます。このコードは実務で使用できる品質を目指して実装されています。