はじめに
皆さんは Delphi でのテキストファイルの読み書きに何を使ってますか?TStringList ですか?TFileStremですか?昔ながらの Read(ln) / Write(ln) でしょうか?
今回は Unicode 版 Delphi であればもれなく使える TStreamReader と TStreamWriter のお話です。
TStreamReader / TStreamWriter
.NET 互換の TStreamReader / TStreamWriter は Delphi 2009 で実装されました。
- system.io.streamreader (learn.microsoft.com)
- system.io.streamwriter (learn.microsoft.com)
- System.Classes.TStreamReader (DocWiki)
- System.Classes.TStreamWriter (DocWiki)
TStreamReader / TStreamWriter は TStringList と違ってバッファをメモリに溜め込まないので大きなファイルを処理するのに向いています。
.NET 由来ではありますが Pascal の伝統的なファイル操作に似ている所もあって、実際に使ってみると意外と馴染みます。
基本的な TStreamReader / TStreamWriter の使い方
■ TStreamWriter の基本的な使い方
TStreamWriter の方から説明します。最も簡単な使い方は次のようになります。実行すると UTF-8 のテキストファイルが生成されます。
uses
..., Classes;
var
Writer: TStreamWriter;
begin
Writer := TStreamWriter.Create('hello.txt'); // 上書きモード
try
Writer.WriteLine('Hello,');
Writer.WriteLine('world.');
finally
Writer.Free;
end;
end;
TStreamWriter のコンストラクタ (パラメータとしてファイル名を受け付ける)
パラメータ | #1 | #2 | #3 | #4 |
---|---|---|---|---|
名前 | Filename | Append | Encoding | BufferSize |
型 | string | Boolean | TEncoding | Integer |
デフォルト | (なし) | False | TEncoding.UTF-8 | 4096 1 |
コンストラクタ Create()
の 2 番目のパラメータとして True を指定すると追記モードになります (デフォルトで False です)。
// 追記モード, UTF-8
Writer := TStreamWriter.Create('hello.txt', True);
コンストラクタ Create()
の 3 番目のパラメータとして TEncoding を渡して文字コードを指定する事もできます。
// 上書モード, UTF-8
Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8);
注意点ですが、上書きモード (正確にはストリームポインタがファイルの先頭にある状態) かつ Encoding パラメータに TEncoding.UTF8 が指定された場合には BOM が書き込まれます。つまり、次の 2 つは同等ではありません。
// 上書きモード, UTF-8 (BOM なし UTF-8)
Writer := TStreamWriter.Create('hello.txt');
// 上書きモード, UTF-8 (BOM あり UTF-8)
Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8);
ストリームの先頭へ移動すればこの問題を回避できます。ストリームのサイズを 0 に設定しないと、何も書き込まれなかった場合に 3 バイトのファイルができてしまいます。
// 上書きモード, UTF-8 (BOM あり UTF-8 だけど BOM を書き込まない)
Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8);
Writer.BaseStream.Seek(0, TSeekOrigin.soBeginning); // ストリームの先頭に移動
Writer.BaseStream.Size := 0; // ストリームのサイズを 0 にする
逆に BOM を書き込まないコンストラクタを使っている状況で BOM を書き込むには次のようにします。
var
Writer: TStreamWriter;
BOM: TArray<Byte>;
begin
// 上書きモード, UTF-8 (BOM なし UTF-8 だけど BOM を書き込む)
Writer := TStreamWriter.Create('hello.txt');
BOM := Writer.Encoding.GetPreamble; // 文字エンコーディングのプリアンブルを取得
Writer.BaseStream.WriteBuffer(BOM, Length(BOM)); // プリアンブルを書き込む
...
TStreamWriter のプロパティ
プロパティ | 型 | デフォルト | 説明 |
---|---|---|---|
AutoFlush | Boolean | True | 自動でフラッシュ (ファイルへの書き出し) するか? |
BaseStream | TStream | 書き込みに使われている TStream | 書き込みに使われている TStream |
Encoding | TEncoding | TEncoding.UTF8 | 文字エンコーディング |
NewLine | string | sLineBreak | 改行文字 |
TStreamWriter のメソッド
メソッド | 説明 |
---|---|
Close() | TStreamWriter を閉じる。書き込みバッファにデータがあればフラッシュされる。このメソッドはデストラクタで呼ばれ、明示的に呼んでも TStreamWriter のインスタンスは破棄されない |
Flush() | フラッシュ (ファイルへの書き出し) を行う。AutoFlush プロパティが True の場合には呼び出す必要がない |
Free() | TStreamWriter を破棄する。デストラクタで Close() が呼ばれる |
OwnStream() | 書き込みに使われている TStream のオーナーを TStreamWriter に設定する |
Write() | データを文字列として書き込む |
WriteLine() | データを行の終端文字で終わる文字列として書き込む |
■ TStreamReader の基本的な使い方
次は TStreamReader です。最も簡単な使い方は次のようになります。実行すると (BOM なし) UTF-8 のテキストファイルを読み込みます。
uses
..., Classes;
var
Reader: TStreamReader;
LineStr: string;
begin
Reader := TStreamReader.Create('hello.txt');
try
while not Reader.EndOfStream do
begin
LineStr := Reader.ReadLine;
...
end;
finally
Reader.Free;
end;
end;
TStreamReader のコンストラクタ (パラメータとしてファイル名を受け付ける)
パラメータ | #1 | #2 |
---|---|---|
名前 | Filename | DetectBOM |
型 | string | Boolean |
デフォルト | (なし) | True |
パラメータ | #1 | #2 | #3 | #4 |
---|---|---|---|---|
名前 | Filename | Encoding | DetectBOM | BufferSize |
型 | string | TEncoding | Boolean | Integer |
デフォルト | (なし) | TEncoding.UTF-8 | False | 4096 1 |
コンストラクタ Create()
の DetectBOM パラメータに True を指定すると UTF-8 (BOM あり) を検出して読み込めます。つまり、次のような記述であれば、BOM の有無を気にせず UTF-8 形式のファイルを読み込めます。
// UTF-8, BOM 自動検出
Reader := TStreamReader.Create('hello.txt', True);
第 2 パラメータ Encoding に TEncoding が指定可能なオーバーロードされたコンストラクタがあります。
// 日本語環境では恐らく Shift_JIS
Reader := TStreamReader.Create('hello.txt', TEncoding.Default);
Shift_JIS を強制するには次のようにします。
var
Reader: TStreamReader;
LineStr: string;
Enc: TEncoding;
begin
// Shift_JIS
Enc := TEncoding.GetEncoding(932);
//Enc := TEncoding.GetEncoding('shift_jis'); // XE2 以降は名前でも OK
Reader := TStreamReader.Create('hello.txt', Enc);
try
while not Reader.EndOfStream do
begin
LineStr := Reader.ReadLine;
Writeln(LineStr);
end;
finally
Reader.Free;
Enc.Free;
end;
end;
コンストラクタの Encoding パラメータに TEncoding を指定し、DetectBOM パラメータに True を設定すると UTF-8 (BOM あり) だった場合には UTF-8 (BOM あり) で、それ以外の場合には指定された文字エンコーディングで読み込みます。
// 文字エンコーディング指定, BOM 自動検出
Reader := TStreamReader.Create('hello.txt', TEncoding.Default, True);
パラメータ数によって DetectBOM のデフォルト値が異なる事に注意が必要です(理屈は解りますけれど...)。混乱を防ぐために、パラメータ DetectBOM は明示的に指定した方がいいのかもしれません。
パラメータ数 | DetectBOM |
---|---|
パラメータが 1 つ (Filename) |
True |
パラメータが 2 つ (Filename, DetectBOM) |
指定した DetectBOM |
パラメータが 2 つ (Filename, Encoding) |
False |
パラメータが 3 つ (Filename, Encoding, DetectBOM) |
指定した DetectBOM |
パラメータが 4 つ (Filename, Encoding, DetectBOM, BufferSize) |
指定した DetectBOM |
Delphi XE7〜10.2 Tokyo で、TEncoding を指定し、かつ DetectBOM を有効にしたコンストラクタを使って BOM あり UTF-8 ファイルを読み込むとエラーになる事があるようです。
根本的な解決方法はありませんが、問題が起こる環境では自前で BOM を判定すればいいだけなので、解っていれば対処は難しくないと思います。
See also:
TStreamReader のプロパティ
プロパティ | 型 | デフォルト | 説明 |
---|---|---|---|
BaseStream | TStream | 読み込みに使われている TStream | 読み込みに使われている TStream |
CurrentEncoding | TEncoding | - | 現在の文字エンコーディング |
EndOfStream | Boolean | - | ストリームの終端ならば True |
TStreamReader のメソッド
メソッド | 説明 |
---|---|
Close() | TStreamReader を閉じる。このメソッドはデストラクタで呼ばれ、明示的に呼んでも TStreamReader のインスタンスは破棄されない |
DiscardBufferedData() | バッファされているすべてのデータを破棄する |
Free() | TStreamReader を破棄する。デストラクタで Close() が呼ばれる |
OwnStream() 2 | 読み込みに使われている TStream のオーナーを TStreamReader に設定する |
Peek() | ストリームポインタを変更せずに、次の文字を取得する。ストリーム終端ならば -1 が返る |
Read() | ストリームポインタを変更し、次の文字を取得する。ストリーム終端ならば -1 が返る |
ReadBlock() | 一連の文字を読み込む |
ReadLine() | 1 行分の文字列を読み込む |
ReadToEnd() | 行末までの文字列を読み込む |
Rewind() 3 | バッファされているすべてのデータを破棄し、ストリームポインタをストリームの先頭へ移動する |
Rewind() は古いバージョンには存在しませんが、次のコードと同等です。
Reader.DiscardBufferedData;
Reader.BaseStream.Seek(0, TSeekOrigin.soBeginning);
■ コンストラクタに TStream を指定する場合
TStreamReader / TStreamWriter には TStream をパラメータとして受け付けるオーバーロードされたコンストラクタがあります。
TStreamWriter のコンストラクタ (パラメータとして TStream を受け付ける)
パラメータ | #1 | #2 | #3 |
---|---|---|---|
名前 | Stream | Encoding | BufferSize |
型 | TStream | TEncoding | Integer |
デフォルト | (なし) | TEncoding.UTF-8 | 4096 1 |
TStreamReader のコンストラクタ (パラメータとして TStream を受け付ける)
パラメータ | #1 | #2 |
---|---|---|
名前 | Stream | DetectBOM |
型 | TStream | Boolean |
デフォルト | (なし) | True |
パラメータ | #1 | #2 | #3 | #4 |
---|---|---|---|---|
名前 | Stream | Encoding | DetectBOM | BufferSize |
型 | TStream | TEncoding | Boolean | Integer |
デフォルト | (なし) | TEncoding.UTF-8 | False | 4096 1 |
パラメータに TStream が渡された場合、デフォルトでは TStreamReader / TStreamWriter を破棄しても TStream は破棄されません。
例えば IOUtils.TFile 4 を使ってファイルストリームを作って渡す場合には、ファイルストリームも破棄する必要があります。
uses
..., IOUtils;
...
Writer := TStreamWriter.Create(TFile.Create('hello.txt'));
try
Writer.WriteLine('Hello,');
Writer.WriteLine('world.');
finally
Writer.BaseStream.Free; // コンストラクタに渡された TStream を破棄
Writer.Free;
end;
コンストラクタに渡された TStream のオーナーを OwnStream() 2 を用いて TStreamReader / TStreamWriter へと変更すれば TStreamReader / TStreamWriter が破棄されたと同時に破棄する事ができます。
uses
..., IOUtils;
...
Writer := TStreamWriter.Create(TFile.Create('hello.txt'));
try
Writer.OwnStream; // Stream のオーナーを TStreamWriter に
Writer.WriteLine('Hello,');
Writer.WriteLine('world.');
finally
Writer.Free; // デストラクタで Close() が呼ばれ、Stream が破棄される。
end;
Delphi 2009 には IOUtils が存在しないので TFile が使えません。代わりに TFileStream を使って下さい。10.1 Berlin 以降だとバッファ付きの TBufferedFileStream 5 も使えます。
// for Delphi 2009
Writer := TStreamWriter.Create(TFileStream.Create('hello.txt', fmCreate or fmOpenWrite));
try
Writer.WriteLine('Hello,');
Writer.WriteLine('world.');
finally
Writer.BaseStream.Free;
Writer.Free;
end;
See also:
- System.IOUtils.TFile (DocWiki)
- System.Classes.TFileStream (DocWiki)
- System.Classes.TBufferedFileStream (DocWiki)
■ ファイルへの書き出しタイミング
小さなデータの書き込みが大量に行われる場合には、AutoFlush
プロパティを False に設定し、任意のタイミングで Flush()
を使ってファイルに書き出す方がパフォーマンスの向上につながります。
uses
..., Classes;
var
Writer: TStreamWriter;
begin
Writer := TStreamWriter.Create('hello.txt'); // 上書きモード
try
Writer.AutoFlush := False; // 自動フラッシュしない
// 処理
Writer.Flush; // ファイルに書き出し
finally
Writer.Free; // デストラクタにより Close() が呼び出され、
end; // Flush() が実行される
end;
AutoFlush = False の時、一度も Flush() を呼び出さなかったとしても、デストラクタで Close() が呼ばれ暗黙的に Flush() が実行されます。
扱うファイルにもよりますが、AutoFlush = True の場合でも、BufferSize を大きくするとパフォーマンスが向上する事があります。但し、コンストラクタにパラメータを 4 つ指定するため、UTF-8 の場合には BOM に注意する必要があります。
// 上書きモード, UTF-8 (BOM あり UTF-8), バッファ 8 倍
Writer := TStreamWriter.Create('hello.txt', False, TEncoding.UTF8, 32768);
try
Writer.WriteLine('Hello,');
Writer.WriteLine('world.');
finally
Writer.Free;
end;
ログのようなものは自動フラッシュ (AutoFlush = True) で都度書き出した方がいいとは思いますが...。
■ TStreamReader / TStreamWriter を標準入出力に割り当てる (Windows)
次のコードでコンソールアプリケーションで TStreamReader / TStreamWriter を標準入出力に割り当てる事ができます。
uses
..., Classes, Windows;
var
Reader: TStreamReader;
Writer: TStreamWriter;
begin
Reader := TStreamReader.Create(THandleStream.Create(GetStdHandle(STD_INPUT_HANDLE)));
Writer := TStreamWriter.Create(THandleStream.Create(GetStdHandle(STD_OUTPUT_HANDLE)));
try
Writer.WriteLine('Hello,world.');
Reader.ReadLine;
finally
Reader.BaseStream.Free;
Reader.Free;
Writer.BaseStream.Free;
Writer.Free;
end;
end;
■ TStreamWriter の Write() / WriteLine()
TStreamWriter の Write() / WriteLine() には多くのオーバーライドされたメソッドが存在するため、文字列へと変換する機会はそうそうないかと思います。
// Write()
procedure Write(Value: Boolean); override;
procedure Write(Value: Char); override;
procedure Write(const Value: TCharArray); override;
procedure Write(Value: Double); override;
procedure Write(Value: Integer); override;
procedure Write(Value: Int64); override;
procedure Write(Value: TObject); override;
procedure Write(Value: Single); override;
procedure Write(const Value: string); override;
procedure Write(Value: Cardinal); override;
procedure Write(Value: UInt64); override;
procedure Write(Value: TCharArray; Index, Count: Integer); override;
// WriteLine()
procedure WriteLine; override;
procedure WriteLine(Value: Boolean); override;
procedure WriteLine(Value: Char); override;
procedure WriteLine(const Value: TCharArray); override;
procedure WriteLine(Value: Double); override;
procedure WriteLine(Value: Integer); override;
procedure WriteLine(Value: Int64); override;
procedure WriteLine(Value: TObject); override;
procedure WriteLine(Value: Single); override;
procedure WriteLine(const Value: string); override;
procedure WriteLine(Value: Cardinal); override;
procedure WriteLine(Value: UInt64); override;
procedure WriteLine(Value: TCharArray; Index, Count: Integer); override;
書式付き Write() / WriteLine()
Format()
と同じ書式文字列とオープン配列コンストラクタをパラメータとして渡せる Write() / WriteLine() メソッドが用意されています。
// Write()
procedure Write(const Format: string; Args: array of const); override;
// WriteLine()
procedure WriteLine(const Format: string; Args: array of const); override;
標準手続きの Write() / Writeln() の可変パラメータと似たような記述が可能となっています。
// Write()
Writer.Write('%s_%.3d.txt', ['LOG', rev]);
// WriteLine()
Writer.WriteLine('%.4d: %s', [Id, Name]);
See also:
■ TStrem.Seek() によるストリームポインタの移動
関連しますが、TStrem.Seek() の 2 番目のパラメータ (Origin) には TSeekOrigin 列挙型を指定して、巨大なファイルでも問題なく移動できるようにすべきです。
具体的には soBeginning
などではなく、TSeekOrigin.soBeginning
のように明示します。
Reader.BaseStream.Seek(FileSize, soBeginning); // NG
Reader.BaseStream.Seek(FileSize, TSeekOrigin.soBeginning); // OK
TSeekOrigin.
で修飾すると常に Int64 の方の Seek() が選択されるからです。
function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual;
function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual;
See also:
おわりに
TStringList が便利だからと、なんでもかんでも TStringList でやっているとパフォーマンスの低下につながる事があります。
ちょっとクセがあったりもしますが、テキストファイルの読み書きを TStreamReader / TStreamWriter に置き換えて高速化が行えないか検討してみてはいかがでしょうか?
TStreamReader / TStreamWriter の使い方は書籍『OBJECT PASCAL HANDBOOK』でも触れられています。
- 「18.2 最新のファイルアクセス」,『OBJECT PASCAL HANDBOOK Delphi 11 Alexandria のための Object Pascal プログラミング完全ガイド』, pp.599-607
- 「18.2 最新のファイルアクセス」,『OBJECT PASCAL HANDBOOK マルチデバイス開発ツール Delphi のためのプログラミング言語完全ガイド』, pp.541-548
余談
本記事のコードは可能な限り古いバージョンでも通るように記述しましたが、Delphi 10.3 以降のインライン変数宣言と型推論を使えばもっと簡潔に書けます。
最も簡単な使い方は次のようになります
まぁ、with 文を使えばもっと簡単になりますけれど。
with TStreamWriter.Create('hello.txt') do
try
WriteLine('Hello,');
WriteLine('world.');
finally
Free;
end;
with TStreamReader.Create('hello.txt') do
try
while not EndOfStream do
begin
var LineStr := ReadLine;
...
end;
finally
Free;
end;
See also:
- 標準ルーチンと入出力 (Qiita)
- <12> テキストファイルの入出力 (標準 Pascal 範囲内での Delphi 入門) (Qiita)
- Delphi 10.3 Rio で CSV を処理する (Qiita)
- 【Delphi】文字列とファイルと複数行文字列 [小ネタ] (Qiita)