Perl 5.8ではEncode.pmが標準モジュールとなり、多バイト文字を標準で簡単に扱えるようになりました。が、jcode.plやJcode.pmを用いたコード変換の経験があると、逆にこれが仇となって文字化けの嵐に遭います。(私の場合そうだったというだけで、普通はそんなこと無いのかもしれないのですが。)漸く最近になって基本的な考え方が飲み込めるようになって来たので、この辺をまとめておこうと思います。なお、本メモは新たにPerl 5.8.x的なPerlスクリプトを書く際に気をつけることをまとめたものであり、基本的には既存のスクリプトにPerl 5.8 + Encode.pmを適用するためのものではありません。また、utf8でスクリプトを書くことを前提にしています。が、どちらにせよ、この知識は参考になると思います。(たぶん。)
本文冒頭ですが、まず参照先を示しておきます。以下のドキュメントにはこれまでにさんざんお世話になりました。ありがとうございます。
以上を踏まえた上で、本文ではスクリプト全体の処理の流れと、Perl 5.8での文字コードの考え方の自分なりの解釈をメモしておきます。なお、間違いや理解不十分が大いに考えられますので、びしばし突っ込んでやってください。宜しくお願いいたします。
これまで、文字コードを変換する際には、jcode.plでは例えばjcode::convert(*data, 'euc');のように関数を用いました。これは非常に直感的で、分かりやすい方法だったと思います。Jcode.pmも、オブジェクトとしても扱える点が違いますが、同様の処理が行なえます。
Perl 5.8 + Encode.pmを用いても同様のコード変換が出来ますが、若干印象が異なるはずです。例えば$data = encode('euc-jp', decode('Guess', $data));のように変換することが出来ますが、書式からいったんデコードを行ない、その後さらにエンコードしていることが分かるでしょう。では何にデコードし、何をエンコードしているのか。このことがPerl 5.8で文字列をどのように処理しているかを知る手がかりです。(ちなみにfrom_to関数を用いることで、よりconvertに近い印象となりますが、本質的には違っています。)
utf8を含むスクリプトを書く際には、use utf8;を行ないます。これはスクリプトがutf8で記述されていることを宣言するものです。具体的には、内部に記述されるリテラルにutf8フラグが立てられます。このutf8フラグというものは、そのデータがutf8であることを宣言するもので、例えばlengthを用いて文字数を数えた場合に、多バイト文字であっても1文字を1とカウントしてくれます。一文字ずつ処理したい場合等に大変楽になります。Perl 5.8では、文字列に内部向けのフラグを付加し、処理の手間を減らしている訳です。内部向けの文字列を、内部表現と呼ぶことにします。
スクリプト内部では、基本的に多バイト文字列は全て内部表現に変換されているとします。つまり、外部の入出力にどのような文字コードを使おうが構わないが、スクリプト内部で扱う際には内部表現に統一するというのが、Perl 5.8の基本的なスタンスです。これは、これまでの日本語処理と大きく違う点となります。
このことが分かりやすく示されるのが、3変数のopen関数です。PerlIOのレイヤーを用いるための書式ですが、特定のエンコーディングのファイルを開く際にこれを用います。例として、Shift_JISファイルをEUC-JPに変換し保存することを考えてみましょう。
まずはjcode.plを用いた場合です。以下のようなスクリプトが考えられます。
#!/usr/bin/perl
# jcode.plを用いたコード変換
require 'jcode.pl';
open READ, '<sjis.txt';
open WRITE, '>euc.txt';
while(my $data = <READ>){
jcode::convert(*data, 'euc');
print WRITE $data;
}
close WRITE;
close READ;
exit;
データを読み込み、jcode::convertでEUC-JPに変換し、記録用のファイルに書き込んでいるだけです。
同じことをPerl 5.8 + Encode.pmを用いて書くと、次のようになります。
#!/usr/bin/perl
# Encode.pmを用いたコード変換
use Encode;
open READ, '<:encoding(shiftjis)', 'sjis.txt';
open WRITE, '>:encoding(euc-jp)', 'euc.txt';
while(my $data = <READ>){
print WRITE $data;
}
close WRITE;
close READ;
exit;
単純にコピーしているだけのようですが、ちゃんと変換されます。処理の流れとしては、レイヤーで文字コードが指定されていることにより、ファイルを開き読み込んだ時点で自動的にその文字コードから内部表現に変換され、書き込む時点で内部表現から指定の文字コードへ変換されているのです。従って、明示的に文字コードを示しておきさえすれば、手動で変換する必要は無いのです。
これは、知っていれば大変便利ですが、知らないと大変危険です。というのは、もし知らずに手動で変換した場合、多重の変換が行われて文字化けを起こすからです。しかし、これまでのやり方に慣れていると、非常に陥りやすい罠なのではないかと感じています。
#!/usr/bin/perl
# 多重に変換するためおかしくなる
use Encode;
open READ, '<:encoding(shiftjis)', 'sjis.txt';
open WRITE, '>:encoding(euc-jp)', 'euc.txt';
while(my $data = <READ>){
$data = encode('euc-jp', decode('shiftjis', $data));
print WRITE $data;
}
close WRITE;
close READ;
exit;
実際には、こうした間違いは『Wide character in ...』等と言ったエラーを出します。ただ、これは構文エラーではなく実行エラーなので、処理が途中で止まってしまったり等の余計な問題を引き起こしかねませんので要注意です。3変数openが面倒でopenプログマにコード指定を仕込んだ場合には、より一層気をつけておかなければなりません。見た目はこれまで通りなのに、挙動は全く違ってしまうのですから。
逆に、これまでどおり文字をオクテット列として扱っておき、(jcode::convertのように)オクテット列→オクテット列の変換を施すのであれば、コードを指定しないように注意しておかなければなりません。
#!/usr/bin/perl
# オクテット列からオクテット列へ変換する
use Encode;
open READ, '<:bytes', 'sjis.txt';
open WRITE, '>:bytes', 'euc.txt';
while(my $data = <READ>){
$data = encode('euc-jp', decode('shiftjis', $data));
print WRITE $data;
}
close WRITE;
close READ;
exit;
ここではencodeとdecodeを用いましたが、from_toを用いるとよりこれまでのイメージに近いかも知れません。
標準入力は、一応オクテット列ということで良さそうですが、Perl 5.8でutf8プログマを利用すると標準入出力にもutf8を使うことが前提となるらしかったりと何やら危険な香りです。(Perl 5.8.1でこのようなことはなくなったそうですが。)これはbinmodeでSTDINのコードを指定してしまえばおしまいですが、一つ問題があり、例えばPerl/CGIでは、どのようなエンコードでデータが送られてくるか何かしらの確証がある訳ではないので、分からないことを前提に処理を進める必要があってコードが指定出来なかったりします。この場合は、やはりbinmodeを用いて、標準入力を念のため明示的にオクテット列に指定しておきます。
#!/usr/bin/perl # STDINはオクテット列のまま扱う use Encode; binmode STDIN, ':bytes'; ...
この後、改めてdecode関数を用い、内部表現に変換してやるという算段です。(必要に応じて、データの分解やURLデコード等はあらかじめ行ないます。)
#!/usr/bin/perl
# decodeを用いデータから文字コードを推測した上で内部表現化する
use Encode;
use Encode::Guess qw/ shiftjis euc-jp 7bit-jis /;
binmode STDIN, ':bytes';
my $buffer;
read STDIN, $buffer, $content_length;
$buffer = decode('Guess', $buffer);
...
なお、入力される文字コードがあらかじめ分かっているならば、binmodeでその文字コードを指定しておけば、推定に伴う不確かさはなくなります。
#!/usr/bin/perl # 入力がShift_JISであると分かっているならば use Encode; binmode STDIN, ':encoding(shiftjis)'; my $buffer; read STDIN, $buffer, $content_length; # $bufferは既に内部表現化されている ...
さらに、出力時にも要注意です。内部表現はutf8なのですが、そのまま出力してはいけません。3変数openなどできちんとコード指定している場合は特に問題ありませんが、これが問題になるのは標準出力に出す際です。utf8プログマで標準出力のコードもutf8にセットされるPerl 5.8ならば問題にならないでしょうが、セットされていなければ(Perl 5.8.1ではセットされないので)例のごとく『Wide character in ...』のエラーになります。これもbinmodeで、確実にコード指定しておかなければなりません。
# 出力にutf8を使うとしても… binmode STDOUT, ":encoding(utf-8)"; # 或いは binmode STDOUT, ":utf8"; # と宣言する<>
出力の際には、入力されたデータを整形するために、複数の文字列を結合する場面が多々あると思います。この時には、結合する文字列が内部表現なのかオクテット列なのか、きちんと把握しておかなければなりません。内部表現とオクテット列を結合すると、オクテット列は自動的に内部表現にアップグレードされます。オクテット列が日本語を表すものであったなら、多重の変換により文字化けの原因となります。
#!/usr/bin/perl
# 変換の際に行番号を付ける
use Encode;
open READ, '<:bytes', 'sjis.txt';
open WRITE, '>:bytes', 'euc.txt';
my $i = 1;
while(my $data = <READ>){
$data = encode('euc-jp', decode('shiftjis', $data));
print WRITE $i++ . "行目: $data";
#『行目』は内部表現、$dataはオクテット列なので、$dataが内部表現にアップグレードされる
}
close WRITE;
close READ;
exit;
もちろんこのままのスクリプトでは、内部表現を直接出力することになるのでエラーが出ますが、構わずに処理後のeuc.txtを見ると見事に文字化けすることが分かるでしょう。当然標準出力する際にも同様の事柄が起き得ます。文字列が内部表現と見なされているか否かはutf8フラグの有無で調べられますので、不安なものはis_utf8等の関数で調べておきましょう。
Perl 5.8 + Encode.pmを用いて日本語を扱う際の注意事項は、文字列内容(内部表現なのかオクテット列なのか)に気をつけておくこと、変換されるタイミングを良く心得ておくこと、という2点がとにかく重要でしょう。
ファイルオープンが行なわれるのは、目に見えるものだけでなく、利用したモジュールの内部で行なわれてしまうもの等様々ありますので、おかしな挙動にであったらこうしたものにも目を向けてやると、解決の糸口になるかも知れません。
