CSV を処理する
「何を今更」感がありますが、本当に CSV を処理できていますか?
問題提起
普通、Delphi で CSV 処理と言うと TStringList を 2 つ使うのが一般的です。例えば、Delphi 2006 以前だと...
var
i, l: Integer;
SL, dFields: TStringList;
begin
SL := TStringList.Create;
dFields := TStringList.Create;
try
SL.LoadFromFile('C:\DATA\TEST.CSV');
for i:=1 to SL.Count-1 do
begin
dFields.CommaText := SL[i];
// ここに処理
end;
finally
dFields.Free;
SL.Free;
end;
end;
|
こんな感じになります。ところが、やってみると解りますが、フィールドを半角 SP でも分割してくれちゃいます。カンマだけでフィールド分割して欲しいので、Delphi 2006 またはそれ以降では、
var
i, l: Integer;
SL, dFields: TStringList;
begin
SL := TStringList.Create;
dFields := TStringList.Create;
try
dFields.Delimiter := ',';
dFields.StrictDelimiter := True;
SL.LoadFromFile('C:\DATA\TEST.CSV');
for i:=1 to SL.Count-1 do
begin
dFields.DelimitedText := SL[i];
// ここに処理
end;
finally
dFields.Free;
SL.Free;
end;
end;
|
TStrings.Delimiter / TStrings.DelimitedText / TStrings.StrictDelimiter を利用します。めでたし、めでたし。
...とは行かない事があります。うまく行かない例を挙げてみましょう。
- 空行でエラーになる (テキストエディタで編集されてしまった場合によくある)
- Excel で保存すると、行末のデータの入っていないフィールドが省略される事がある
- フィールドに改行が含まれるとエラーになる
前 2 つは以下のようにして対処できます。
var
i, l: Integer;
SL, dFields: TStringList;
begin
SL := TStringList.Create;
dFields := TStringList.Create;
try
dFields.Delimiter := ',';
dFields.StrictDelimiter := True;
SL.LoadFromFile('C:\DATA\TEST.CSV');
for i:=1 to SL.Count-1 do
begin
// 空行だったら抜ける
if SL[i] = '' then
Continue;
// デリミタで分割する
dFields.DelimitedText := SL[i];
// 足りないフィールドを埋める (20 フィールドの場合)
for l:=dFields.Count + 1 to 20 do
dFields.Add('');
// ここに処理
end;
finally
dFields.Free;
SL.Free;
end;
end;
|
さて、フィールドに改行が含まれる場合はどうしましょうか?例えば Excel の場合、フィールド内での改行は 0x0A で、行の改行は 0x0D 0x0A ですが、CSV の仕様としては "フィールド内での改行" / "行の改行" 共に 0x0D 0x0A にする事も可能です。最初、TMemoryStream に読み込んでプリプロセスとしてフィールド中の改行をエスケープしますか?まぁ、場合によってはそれもアリっちゃアリですが...。
そもそも...
CSV って "Comma Separated Values" の略というのはご存じの事でしょう。カンマ区切りテキストですね。Character Separated Value?そんな後付け設定の事は知りません (ならなんで "文字区切りテキスト" と言わないんだい?)。CSV には他にもお約束があります。
- フィールドはカンマで区切る
- 1 行目はヘッダの場合がある
- フィールド文字列中に以下の文字が含まれる場合には前後をクォート文字で括る
- 空白文字
- カンマ (区切り文字)
- クォート文字 (多くは ダブルクォーテーション)
- 改行文字 等の制御文字
- フィールド文字列中にクォート文字が存在する場合には "クォート文字×2" へ変換する
- 後方のフィールドに値が入っていなければカンマごと省略可能
ものは試しです。ちょっと意地の悪い CSV をサンプルで用意してみました (sample_csv.zip)。これをちゃんと処理できますか?
Excel はこれをちゃんと処理します。逆を言えば、"Excel で編集された CSV はこの意地悪な形式になる事がある"という事です。
"ひとつの解答"
例外処理で悩まなくて済むようにするには dbGo (ADO Express) を使うのが最も簡単です。Delphi XE Starter をお持ちの方には何の解決にもなっていなくてスミマセン。
"例の意地悪な CSV (sample_csv.zip)" を読み込ませてみましょう。フォームには TStringGrid と TButton が一つづつ貼ってあるものとします。
uses
..., ADODB;
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
ADOC: TADOConnection;
ADOQ: TADOQuery;
ExtProps: String;
FullPathName, FilePath, FileName: String;
begin
// ファイル名の設定
FullPathName := 'C:\DATA\SAMPLE.CSV';
FilePath := ExcludeTrailingPathDelimiter(ExtractFilePath(FullPathName));
FileName := ExtractFilename(FullPathName);
// dbGo の設定
ADOC := TADOConnection.Create(Self);
ADOQ := TADOQuery.Create(Self);
try
// コネクション文字列の生成
// (OLE DB プロバイダ = MSDASQL)
ExtProps := '';
ExtProps := ExtProps + 'DRIVER={Microsoft Text Driver (*.txt; *.csv)};'; // ドライバ名
ExtProps := ExtProps + Format('DefaultDir=%s;', [FilePath]); // CSV のあるフォルダ
ExtProps := ExtProps + 'HDR=Yes;'; // 1 行目がヘッダか?
ExtProps := Format('Extended Properties="%s";', [ExtProps]);
ADOC.ConnectionString := 'Provider=MSDASQL;' + ExtProps; // OLE DB プロバイダ
ADOQ.Connection := ADOC;
// 接続
ADOC.Connected := True;
// CSV に対してクエリ発行
with ADOQ do
begin
SQL.Clear;
SQL.Add('Select * From "' + FileName + '"');
Open;
// StrinGrid の行数を設定
if IsEmpty then
StringGrid1.RowCount := 2
else
StringGrid1.RowCount := RecordCount + 1;
// ヘッダを読む
for i:=0 to Fields.Count -1 do
StringGrid1.Cells[i, 0] := ADOQ.Fields[i].FieldName;
// データを読む
i := 1;
while not EOF do
begin
StringGrid1.Cells[0, i] := FieldByName('FIELD_01').AsString;
StringGrid1.Cells[1, i] := FieldByName('FIELD_02').AsString;
StringGrid1.Cells[2, i] := FieldByName('FIELD_03').AsString;
StringGrid1.Cells[3, i] := FieldByName('FIELD_04').AsString;
StringGrid1.Cells[4, i] := FieldByName('FIELD_05').AsString;
Inc(i);
Next;
end;
Close;
end;
// 切断
ADOC.Connected := False;
finally
ADOQ.Free;
ADOC.Free;
end;
end;
|
理解しやすいようにソースコードは長めになっていますが、そんなに難しいものではありません。dbGo のコンポーネントをフォームに貼るのであればもっと簡単になるハズです。
実行結果がコレです。フィールド中の空白/カンマ/エスケープされたクォーテーション/改行、省略されたフィールド等を考慮したコーディングは一切していませんが、行もフィールドもズレる事なく読み込めているのがお判り頂けると思います。
「複数行になってないじゃねーか」と仰るかもしれませんが、StringGrid が改行で表示してくれないからこうなっています。このままだと "FieldByName('FIELD_02').AsString" で取得できるデータは 0x0A 区切りになっていますので、実用的にするためには StringReplace() で 0x0D,0x0A に置換するか、TStringList に放り込んで取り出すといった処理が必要ですが、ここまでやれば残った処理は些細なものです。少なくとも行とフィールドがズレてさえなければ後はどうにでもできます。
CSV に対して SQL を投げているので、当然 Where 句も使えますし、Order By 句も使えます。
SQL.Clear;
SQL.Add('Select * From "' + FileName + '"');
SQL.Add('Where ');
SQL.Add(' [FIELD_01] > :_FIELD_01 ');
Parameters.ParamByName('_FIELD_01').Value := 3;
Open;
...
|
フィールド名は "[]" で括らなければなりません。パラメータも普通に使えます。実行結果は以下のようになります。
条件を絞って取り込んだり (空行を弾くとか)、フィールドを入れ替えて取り込んだり、フィールドを並べ替えて取り込んだりできます。同様のテキスト処理は BDE でも可能ですが、これだけのためだけに BDE を配布しなくてはならないのはどうかと思いますのでオススメはしません。
また、CSV 処理に使える "OLE DB プロバイダ" には、
がありますが、このうち Jet に関しては 64bit サポートがありません (サポート予定もありません) ので注意が必要です。ちなみに...同様のコードで、コネクション文字列を変更するだけで XLS や XLSX を読み出す事も可能です。ADO コネクション文字列の詳細に関しては NYORO PRESS さんの "ADO (ActiveX Data Object)" に詳細が書かれています。
考察
TStringList の挙動を知るにつれ CSV 処理クラスを書きたくなる衝動に駆られてしまうと思います。そしてクラスを書き上げていざ使ってみる段になって、「これだったら dbGo (ADO Express) で処理する方が手っ取り早くね?」と感じてしまうのです。
先に書いたように dbGo (ADO Express) で処理すれば、抽出 / 並び替えを先に行ってから必要な部分だけを取り込めますので、殆どの場合、処理そのものを書き直さず SQL だけ書き換えれば済みますからね。
プログラマとしてはこの手段を採る事に少々釈然としない感がありますが、「データを CSV でくれ」と言われた場合には 9 割以上の確率で Excel によるデータ入力が行われるのが実情であり、"Excel が吐く CSV と親和性が高いのは ADO による処理" というのもまた確かなのです。
...ただ、Excel が吐く CSV はフィールド中の改行が 0x0A、行改行が 0x0D,0x0A である事は先にも書きましたが、CSV の仕様をクラスプラットフォームで考えた場合、
- 文字コードは UTF-8
- フィールド中の改行は 0x0A
- 行改行も 0x0A
というのもアリですので、"プラットフォームに依存しない CSV 処理クラス (または DBX4 ドライバ等)" が欲しいのは欲しいんですよね...。
ついでに TSV
TSV は "Tab Separated Values" の略です。タブ区切りテキストです...が、TSV もひっくるめて CSV と呼ぶ事があります ("Character Separated Values" という苦しい言い訳が用意されています)。
- フィールドはタブで区切る
- 1 行目はヘッダの場合がある
- フィールド文字列中に以下の文字が含まれる場合には前後をクォート文字で括る
- 空白文字
- カンマ
- クォート文字 (多くは ダブルクォーテーション)
- 改行文字等の制御文字
- フィールド文字列中にクォート文字が存在する場合には "クォート文字×2" へ変換する
- 後方のフィールドに値が入っていなければタブごと省略可能
TSV のフォーマットはフィールドセパレータが単純にタブに置き換わっただけです...なので、フィールド中にカンマが存在する場合には CSV 同様に前後をクォート文字で括る必要があります。
タブ区切りテキストをクリップボードに読み込ませると、そのまま Excel や Ooo-Calc に貼り付ける事ができるため、場合によっては CSV よりも重宝する事があります。ファイルへのエクスポートは CSV 形式、クリップボードへは TSV 形式で出力するようにしておくとアプリケーションの応用範囲が広がります。