# Delphi 10.3 Rio で CSV を処理する --- tags: Delphi programming embarcadero objectpascal created_at: 2018-12-09 updated_at: 2020-03-11 --- # はじめに これは [Delphi Advent Calendar 2018](https://qiita.com/advent-calendar/2018/delphi) の 12 日目の記事です。 先日、Delphi 10.3 Rio がリリースされ、型推論可能なインライン変数宣言ができるようになりました。 - [インライン変数宣言 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/Rio/ja/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E5%A4%89%E6%95%B0%E5%AE%A3%E8%A8%80) - [型推論 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/Rio/ja/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E5%A4%89%E6%95%B0%E5%AE%A3%E8%A8%80#.E3.82.A4.E3.83.B3.E3.83.A9.E3.82.A4.E3.83.B3.E5.A4.89.E6.95.B0.E3.81.AE.E5.9E.8B.E6.8E.A8.E8.AB.96) これを使って CSV を処理してみようと思います。 # コード ## CSV ファイルの読み込み 適当な CSV ファイルがなかったので、Delphi のサンプルデータベースをコンバートして CSV を作りました。 一行目はヘッダです。 ```text:animals.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 ファイルの読み込みはこんなコードになりました。 ```pascal:ReadCSV.dpr 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. ``` TStreaReader のコンストラクタには以下のようなバリエーションがあります。 ```pascal 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; ``` 実行すると、 ```text 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 を使ったバージョンは以下のようになります。 ```pascal 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 もその一つとなっています。つまり、 ```pascal Fields.StrictDelimiter := True; ``` は ```pascal Fields.Options := Fields.Options + [soStrictDelimiter]; ``` このようにも書けるようになっています。 - [System.Classes.TStrings.Options (DocWiki: en)](http://docwiki.embarcadero.com/Libraries/en/System.Classes.TStrings.Options) ### TStreamReader について TStreamReader は .NET の StreamReader クラスを~~パク~~...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Reset() / CloseFile() の代わりに使えます。 そういえば 10.3 では Rewind() なんてメソッドが追加されています。10.2 Tokyo 以前では ファイルの先頭に移動したい場合には、 ```pascal Reader.DiscardBufferedData; Reader.BaseStream.Seek(0, TSeekOrigin.soBeginning); ``` なんて事をやる必要がありました (多分) が、10.3 Rio だと、 ```pascal Reader.Rewind; ``` って書くだけでよくなりました。 - [System.Classes.TStreamReader (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TStreamReader) - [StreamReader Class (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamreader?view=netframework-4.7.2) ## CSV ファイルの書き出し 正直、書き出す方はあまり特殊な記述にはなりません。 ```pascal:WriteCSV.dpr 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 のコンストラクタには以下のようなバリエーションがあります。 ```pascal 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 のオブジェクトメソッド)。 ```pascal: StrArr[0] := '"' + 'Angel Fish' + '"'; ``` ではいかんのか?と思われるかもしれませんが、そのロジックだと**文字列にダブルクォーテーションが含まれる場合**に困ります。 - [Comma-Separated Values (Wikipedia)](https://ja.wikipedia.org/wiki/Comma-Separated_Values) Join は String 型へのヘルパーです (System.SysUtils.TStringHelper のクラスメソッド)。クラスメソッドなので、**"型名.クラスメソッド"**の形式で呼び出せます。 ```pascal var LineStr: string := String.Join(',', StrArr); // OK var LineStr: string := TStringHelper.Join(',', StrArr); // NG ``` TStringHelper は String 型のヘルパーなので、**"String.クラスメソッド"** で呼び出します。"TStringHelper.クラスメソッド" では呼び出せません。 余談ですが、10.3 Rio の String 型のクラスメソッド (TStringHelper のクラスメソッド) には以下のようなものがあります。 ![image.png](./images/771f475f-c4b1-c917-0628-03e9ee19d0e9.png) - [System.SysUtils.TStringHelper (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.TStringHelper) ### TStreamWriter について TStreamWriter は .NET の StreamWriter クラスを~~パク~~...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Rewrite(), Append() / CloseFile() の代わりに使えます。 - [System.Classes.TStreamWriter (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TStreamWriter) - [StreamWriter Class (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter?view=netframework-4.7.2) # おわりに 特にオチはないです。 TStreamReader / TStreamWriter は ANSI 版 Delphi では使えないため、CSV 処理には TStringList を使う事が多かったですね...お手軽ですし。でも、古い Delphi 用に AssignFile() / Reset() / Rewrite(), Append() / CloseFile() をラッピングした TStreamReader / TStreamWriter 互換クラスを作っておくと何かと便利かもしれませんね...こんな感じで。 ```pascal:uStreamReaderWriter.pas 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()](http://docwiki.embarcadero.com/Libraries/ja/System.StrUtils.SplitString) があるけれど)、それこそ TStringList を使って処理をすればいいかと思います。 ```pascal 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 が便利すぎるのですよねぇ...。