LoginSignup
14
13

More than 3 years have passed since last update.

Delphi 10.3 Rio で CSV を処理する

Last updated at Posted at 2018-12-11

はじめに

これは Delphi Advent Calendar 2018 の 12 日目の記事です。

先日、Delphi 10.3 Rio がリリースされ、型推論可能なインライン変数宣言ができるようになりました。

これを使って CSV を処理してみようと思います。

コード

CSV ファイルの読み込み

適当な CSV ファイルがなかったので、Delphi のサンプルデータベースをコンバートして CSV を作りました。
一行目はヘッダです。

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 ファイルの読み込みはこんなコードになりました。

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.

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 ファイルの書き出し

正直、書き出す方はあまり特殊な記述にはなりません。

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 のコンストラクタには以下のようなバリエーションがあります。

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 のクラスメソッド) には以下のようなものがあります。
image.png

TStreamWriter について

TStreamWriter は .NET の StreamWriter クラスをパク...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Rewrite(), Append() / CloseFile() の代わりに使えます。

おわりに

特にオチはないです。

TStreamReader / TStreamWriter は ANSI 版 Delphi では使えないため、CSV 処理には TStringList を使う事が多かったですね...お手軽ですし。でも、古い Delphi 用に AssignFile() / Reset() / Rewrite(), Append() / CloseFile() をラッピングした TStreamReader / TStreamWriter 互換クラスを作っておくと何かと便利かもしれませんね...こんな感じで。

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() があるけれど)、それこそ 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 が便利すぎるのですよねぇ...。

14
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
13