はじめに
これは Delphi Advent Calendar 2018 の 12 日目の記事です。
先日、Delphi 10.3 Rio がリリースされ、型推論可能なインライン変数宣言ができるようになりました。
これを使って CSV を処理してみようと思います。
コード
CSV ファイルの読み込み
適当な CSV ファイルがなかったので、Delphi のサンプルデータベースをコンバートして CSV を作りました。
一行目はヘッダです。
"NAME","SIZE","WEIGHT","AREA"
"Angel Fish",2,2,"Computer Aquariums"
"Boa",10,8,"South America"
"Critters",30,20,"Screen Savers"
"House Cat",10,5,"New Orleans"
"Ocelot",40,35,"Africa and Asia"
"Parrot",5,5,"South America"
"Tetras",2,2,"Fish Bowls"
Delphi のサンプルデータベースは 10.3 Rio だと
C:\Users\Public\Documents\Embarcadero\Studio\20.0\Samples\Data
に格納されています。
10.3 Rio っぽい (?) CSV ファイルの読み込みはこんなコードになりました。
program ReadCSV;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Classes, System.SysUtils;
begin
var Reader := TStreamReader.Create('animals.csv', TEncoding.Default, True); // 'animals.csv' を開く。BOM を調べてエンコーディングを自動選択する。BOM がなかったら TEncoding.Default (つまりは Shift_JIS) で開く。
try
Reader.ReadLine; // ヘッダ行を読み飛ばす
while not Reader.EndOfStream do // EOF になるまで読む
begin
for var Field in Reader.ReadLine.Split([','], '"') do // 読んだ行をカンマ区切りで分割したコレクションにする (クォーテーション考慮)
Writeln(Field.DeQuotedString('"')); // 読んだフィールドがダブルクォーテーションで括られていたらそれを外す
Writeln; // 空の改行
end;
finally
Reader.Free;
end;
Readln; // 何かキーが押されるまで待つ
end.
TStreamReader のコンストラクタには以下のようなバリエーションがあります。
constructor Create(Stream: TStream); overload;
constructor Create(Stream: TStream; DetectBOM: Boolean); overload;
constructor Create(Stream: TStream; Encoding: TEncoding; DetectBOM: Boolean = False; BufferSize: Integer = 4096); overload;
constructor Create(const Filename: string); overload;
constructor Create(const Filename: string; DetectBOM: Boolean); overload;
constructor Create(const Filename: string; Encoding: TEncoding; DetectBOM: Boolean = False; BufferSize: Integer = 4096); overload;
実行すると、
Angel Fish
2
2
Computer Aquariums
Boa
10
8
South America
Critters
30
20
Screen Savers
House Cat
10
5
New Orleans
Ocelot
40
35
Africa and Asia
Parrot
5
5
South America
Tetras
2
2
Fish Bowls
こんな感じになります。テキスト分割処理に TStringList を使ったバージョンは以下のようになります。
program ReadCSV;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Classes, System.SysUtils;
begin
var Reader := TStreamReader.Create('animals.csv', TEncoding.Default, True); // 'animals.csv' を開く。BOM を調べてエンコーディングを自動選択する。BOM がなかったら TEncoding.Default (つまりは Shift_JIS) で開く。
var Fields := TStringList.Create;
try
Fields.StrictDelimiter := True; // デリミタ (TStringList.Delimiter) でのみ文字列を分割する。クォート文字 (TStringList.QuoteChar) は考慮される。
Reader.ReadLine; // ヘッダ行を読み飛ばす
while not Reader.EndOfStream do // EOF になるまで読む
begin
Fields.DelimitedText := Reader.ReadLine; // 読んだ行をカンマ区切りで分割したコレクションにする (クォーテーション考慮)
for var Field in Fields do
Writeln(Field);
Writeln; // 空の改行
end;
finally
Fields.Free;
Reader.Free;
end;
Readln; // 何かキーが押されるまで待つ
end.
テキスト分割処理は TStringList に任せた方が解りやすいコードになるかと思います。テキスト分割にかかわる TStringList のプロパティは以下の通りです。
プロパティ | 初期値 | 説明 |
---|---|---|
Delimiter | , (カンマ) | 区切り文字 |
QuoteChar | " (ダブルクォーテーション) | クォート文字 |
StrictDelimiter | False | 区切り文字でのみ分割。 False だと空白文字でも分割する。 |
なお、10.1 Berlin より、一部のプロパティは Options プロパティで指定するようになっており、StrictDelimiter もその一つとなっています。つまり、
Fields.StrictDelimiter := True;
は
Fields.Options := Fields.Options + [soStrictDelimiter];
このようにも書けるようになっています。
TStreamReader について
TStreamReader は .NET の StreamReader クラスをパク...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Reset() / CloseFile() の代わりに使えます。
そういえば 10.3 では Rewind() なんてメソッドが追加されています。10.2 Tokyo 以前では ファイルの先頭に移動したい場合には、
Reader.DiscardBufferedData;
Reader.BaseStream.Seek(0, TSeekOrigin.soBeginning);
なんて事をやる必要がありました (多分) が、10.3 Rio だと、
Reader.Rewind;
って書くだけでよくなりました。
CSV ファイルの書き出し
正直、書き出す方はあまり特殊な記述にはなりません。
program WriteCSV;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Classes, System.SysUtils, System.Types;
begin
var Writer := TStreamWriter.Create('animals2.csv', False, TEncoding.UTF8);
try
var StrArr: TStringDynArray;
SetLength(StrArr, 4);
// ヘッダ
StrArr[0] := 'NAME'.QuotedString('"');
StrArr[1] := 'SIZE'.QuotedString('"');
StrArr[2] := 'WEIGHT'.QuotedString('"');
StrArr[3] := 'AREA'.QuotedString('"');
Writer.WriteLine(String.Join(',', StrArr));
// データ
StrArr[0] := 'Angel Fish'.QuotedString('"');
StrArr[1] := '2';
StrArr[2] := '2';
StrArr[3] := 'Computer Aquarium'.QuotedString('"');
Writer.WriteLine(String.Join(',', StrArr));
finally
Writer.Free;
end;
end.
TStreamWriter のコンストラクタには以下のようなバリエーションがあります。
constructor Create(Stream: TStream); overload;
constructor Create(Stream: TStream; Encoding: TEncoding; BufferSize: Integer = 4096); overload;
constructor Create(const Filename: string; Append: Boolean = False); overload;
constructor Create(const Filename: string; Append: Boolean; Encoding: TEncoding; BufferSize: Integer = 4096); overload;
ファイル名をパラメータとして取るコンストラクタの 2 番目の引数は Append で、ファイルを追記するかどうかのフラグです。False (上書き) がデフォルトです。3 番目の引数はエンコーディングで、指定しないと OS デフォルト (日本語なら Shift_JIS) の設定になります。
QuotedString は String 型へのヘルパーです (System.SysUtils.TStringHelper のオブジェクトメソッド)。
StrArr[0] := '"' + 'Angel Fish' + '"';
ではいかんのか?と思われるかもしれませんが、そのロジックだと文字列にダブルクォーテーションが含まれる場合に困ります。
Join は String 型へのヘルパーです (System.SysUtils.TStringHelper のクラスメソッド)。クラスメソッドなので、**"型名.クラスメソッド"**の形式で呼び出せます。
var LineStr: string := String.Join(',', StrArr); // OK
var LineStr: string := TStringHelper.Join(',', StrArr); // NG
TStringHelper は String 型のヘルパーなので、"String.クラスメソッド" で呼び出します。"TStringHelper.クラスメソッド" では呼び出せません。
余談ですが、10.3 Rio の String 型のクラスメソッド (TStringHelper のクラスメソッド) には以下のようなものがあります。
TStreamWriter について
TStreamWriter は .NET の StreamWriter クラスをパク...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Rewrite(), Append() / CloseFile() の代わりに使えます。
おわりに
特にオチはないです。
TStreamReader / TStreamWriter は ANSI 版 Delphi では使えないため、CSV 処理には TStringList を使う事が多かったですね...お手軽ですし。でも、古い Delphi 用に AssignFile() / Reset() / Rewrite(), Append() / CloseFile() をラッピングした TStreamReader / TStreamWriter 互換クラスを作っておくと何かと便利かもしれませんね...こんな感じで。
unit uStreamReaderWriter;
interface
type
TStreamReader = class
private
F: TextFile;
function GetEndOfStream: Boolean;
public
constructor Create(const Filename: string);
destructor Destroy; override;
procedure Close;
function Read: Integer;
function ReadLine: string;
property EndOfStream: Boolean read GetEndOfStream;
end;
TStreamWriter = class
private
F: TextFile;
public
constructor Create(const Filename: string; Append: Boolean = False);
destructor Destroy; override;
procedure Close;
procedure Flush;
procedure Write(Value: Char);
procedure WriteLine(const Value: string);
end;
implementation
{ TStreamReader }
procedure TStreamReader.Close;
begin
System.CloseFile(F);
end;
constructor TStreamReader.Create(const Filename: string);
begin
System.AssignFile(F, Filename);
System.Reset(F);
end;
destructor TStreamReader.Destroy;
begin
Self.Close;
end;
function TStreamReader.GetEndOfStream: Boolean;
begin
result := System.Eof(F)
end;
function TStreamReader.Read: Integer;
var
c: Char;
begin
System.Read(F, C);
result := Ord(C);
end;
function TStreamReader.ReadLine: string;
var
Buf: String;
begin
System.Readln(F, Buf);
result := Buf;
end;
{ TStreamWriter }
procedure TStreamWriter.Close;
begin
System.CloseFile(F);
end;
constructor TStreamWriter.Create(const Filename: string; Append: Boolean);
begin
System.AssignFile(F, Filename);
if Append then
System.Append(F)
else
System.Rewrite(F);
end;
destructor TStreamWriter.Destroy;
begin
Self.Close;
end;
procedure TStreamWriter.Flush;
begin
System.Flush(F)
end;
procedure TStreamWriter.Write(Value: Char);
begin
System.Write(F, Value);
end;
procedure TStreamWriter.WriteLine(const Value: string);
begin
System.Writeln(F, Value);
end;
end.
XE3 よりも前のバージョンには String 型のヘルパーがないので Split() は使えませんが (XE 以降には SplitString() があるけれど)、それこそ TStringList を使って処理をすればいいかと思います。
program ReadCSV;
{$APPTYPE CONSOLE}
uses
SysUtils, Classes,
uStreamReaderWriter;
var
Reader: TStreamReader;
Fields: TStringList;
Field: String;
begin
Reader := TStreamReader.Create('animals.csv');
Fields := TStringList.Create;
try
Fields.StrictDelimiter := True;
Reader.ReadLine;
while not Reader.EndOfStream do
begin
Fields.DelimitedText := Reader.ReadLine;
for Field in Fields do
Writeln(Field);
Writeln;
end;
finally
Fields.Free;
Reader.Free;
end;
Readln;
end.
なんだかんだで TStringList が便利すぎるのですよねぇ...。