入出力(6) 改行コードとbinmode

Perl はテキスト処理に長けた言語、という印象がありましたが、バイナリを扱う事も可能です。
ただ、バイナリファイルに関連付けられたファイルハンドルからバイナリを読み込む場合や、ファイルハンドルに対してバイナリを出力する場合、注意しなくてはいけないのが「モード」の違いです。
Perl でファイルを開いた場合、ファイルハンドルは通常「テキストモード」というモードに設定されています。このモードでは「改行コードの変換」が自動的に行なわれます。改行コードといえば、Windows では「CR+LF」(\r\n)を使い、UNIX 系OSでは「LF」(\n)が、Mac OSでは「CR」(\r)が一般的に使われますね。(^_^)


試しに以下のようなプログラムを作成してみました。

use strict;
 
open OUT, "> textmode.txt" or die "Can't open textmode.txt: $!\n";
print OUT "Apple\n";
print OUT "Banana\n";
print OUT "Orange\n";
close OUT;

実行すると、「textmode.txt」を作成して、3行のテキストを出力します。
Windows 環境で実行して作成された textmode.txt を、バイナリエディタで見てみると、中身は以下のようになっていました。

41 70 70 6C 65 0D 0A 42 61 6E 61 6E 61 0D 0A 4F  Apple..Banana..O
72 61 6E 67 65 0D 0A                             range..

バイナリとテキストを照らし合わせてみると、改行コードとして「0D 0A」、つまり「CR+LF」が使われている事がわかります。スクリプト中では「\n」と書いたのに、出力されたのは「\r\n」となっているわけです。
この機構のお陰で、Perl のプログラムは OS 間の改行コードの違いを意識せずに、テキストの入出力ができます。プログラムの内部では「\n」としておけば、間違いないのです。


しかし、バイナリファイルを出力したい場合、この自動変換があると困った事になってしまいます。「0A」、つまり LF をそのまま出力したいにも関わらず、自動的に「0D 0A」(CR+LF)に変換されてしまうので、正常なバイナリファイルを作成できない事があるのです。
例えば 00 から FF までの文字を順番に並べたバイナリファイルを作る為に、以下のコードを書いてみました。

use strict;
 
open OUT, "> test.bin" or die "Can't open test.bin: $!\n";
for my $i (0x00..0xFF) {
  print OUT chr($i);
}
close OUT;

chr 関数は引数に指定した文字コードに対応する文字を返します。「0x」は16進数を表す為のプリフィクスなので、この for ループの $i には 0 から 255(FF)までの数値が順番に入るはずです。
このスクリプトを実行して作成された「test.bin」をバイナリエディタで見ると、以下のようになっています。

00 01 02 03 04 05 06 07 08 09 0D 0A 0B 0C 0D 0E
0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E
1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E
(中略)
CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE
DF E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE
EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE
FF

よく見ると、「09」の次が「0D 0A」となっているのがわかります。本当は「…09 0A 0B…」となって欲しいので、困りますね。(^_^;)


そこで、binmode 関数を使います。この関数を使うと、ファイルハンドルに対して入出力のモードを設定できます。

binmode FILEHANDLE, LAYER
binmode FILEHANDLE
        Arranges for FILEHANDLE to be read or written in "binary" or
        "text" mode on systems where the run-time libraries distinguish
        between binary and text files.

FILEHANDLE にモードを設定したいファイルハンドルを、LAYER には使用するモードを文字列で渡します。バイナリモード、つまり「生のデータ」を扱うには LAYER を省略するか LAYER に ":raw" を指定します。

binmode OUT;

とすれば、バイナリがそのまま読み書きできるわけですね。binmode 関数は、open した後まだなにも入出力していない状態で呼び出す必要があります。
さきほどのコードに書き加えてみました。

use strict;
 
open OUT, "> test.bin" or die "Can't open test.bin: $!\n";
binmode OUT;
for my $i (0x00..0xFF) {
  print OUT chr($i);
}
close OUT;

以下、実行して生成された test.bin をバイナリエディタで開いた様子です。

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F
(中略)
D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF
E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF
F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF

0A をそのまま出力できていますね。(^_^)


なお、最近のバージョンの Perl では binmode でエンコーディングを指定する事もできるようです。例えば ":utf-8" を LAYER として指定すれば、ファイルハンドルが UTF-8 で入出力するようになります。