前に書いたときと時代背景も知識も大幅に違うので、書き直してみることにしました。Perl 5.8以降を前提として考えます。
まずは、jcode.plなどが全盛だった時代を思い出してみます。jcode.plの時代は、Unicodeはほぼ使われておらず、JIS, Shift_JIS, EUC_JPあたりの変換を行う事が主でした。実際のコードを見て、扱われ方を考察してみましょう。
#!/usr/bin/perl
# jcode.plを用いたコード変換
require 'jcode.pl';
open my $r, '<sjis.txt';
open my $w, '>euc.txt';
while(my $data = <$r>){
jcode::convert(*data, 'euc');
print $w $data;
}
close $w;
close $r;
exit;
とかとか。この例では、dataの中をeucに変えている訳ですが、変換処理自体はjcode.plが行っているのみで、それだけの話です。dataの中はオクテット列であり、Perlも単なるオクテット列として扱っています。
Encode.pm時代、あるいはuse utf8全盛時代(そんなのがあるかは別として)では、上記のような考え方が一新されています。この、一新された考え方を理解しておく事こそが、へんてこな文字化けを回避する、あるいは解決するコツになります。
まず、use utf8すると、Perlは文字列を「内部表現」という形で扱うことになります。「内部表現」は、「utf8フラグ」がついたutf8の文字列です。これまで(jcode.pl時代には)Perlは外から来た文字列をただのオクテット列として扱っていて不干渉でしたが、use utf8を行うと、「文字列」をそれ以外と区別した特別なものとして扱う,という事です。
そこで、外で使う文字列と、「内部表現」との変換が必要になります。Encode.pmをuseすると使えるようになる、encodeやdecodeは、そういった内部表現とのやり取りをする関数になります。
ちょっと図示してみましょう。外から取得した文字列を変換して、また外に出力するものを考えます。
まずはjcode.pl時代の考え方です。
文字列(オクテット列) -> jcode::convert -> 文字列(オクテット列)
一方、utf8時代では、
文字列(オクテット列) -> decode -> 文字列(内部表現) -> encode -> 文字列(オクテット列)
コードにしてみると、jcode.plの場合
jcode:convert( *var, 'euc' );
print $var;
一方encode/decodeの場合は
my $string = decode( 'shiftjis', $var );
print encode( 'euc-jp', $string );
あるいは短く
print encode( 'euc-jp', decode( 'shiftjis', $var ) );
encodeやらdecodeが、感覚と逆になっていると感じた事はないでしょうか。もし感じたことがあるなら、暗黙に「内部表現に」「内部表現から」というモノが隠れている事を覚えておくと良いでしょう。
というわけで、use utf8している環境では、内部表現と外が明確に分かれています。このルールを侵犯して、内部表現のまま出力しようとすると、「Wide charactor in ...」といったwarningで知らせてくれます。
上記の考え方をもとにすれば、3変数openやらなんやらといった新しい構文でやっていることも自ずと理解出来ます。3変数openでは、openする際に予めオクテット列と内部表現の変換ルールを指定してある、ということです。
#!/usr/bin/perl
# Encode.pmを用いたコード変換
use Encode;
open my $r, '<:encoding(shiftjis)', 'sjis.txt';
open my $w, '>:encoding(euc-jp)', 'euc.txt';
while(my $data = <$r>){
print $w $data;
}
close $w;
close $r;
exit;
openの時点で変換ルールが決まっているので、decodeやencodeをかまさなくても、そのまま内部表現として利用することが出来ます。このとき実際に変換処理を担当しているのがPerlIOレイヤーと呼ばれるものです。
文字列(オクテット列) -> PerlIOレイヤー -> 文字列(内部表現) -> PerlIOレイヤー -> 文字列(オクテット列)
3変数openはopen時にIOレイヤーを決め、binmodeではあとからレイヤーを変更する事が出来ます。再三登場する、入出力の間に1枚噛んでいるようすが、Perlにおける多言語処理の考え方をよく表していると言えるでしょう。
わざわざ内部表現にするのは、面倒な事ばかりのようですが、もちろん良いこともあります。
例えば「1文字」を数えられるようになります。これまでは正規表現などを使い、うまいことやって「1文字」を数えたりしていましたが、内部表現にしておけば、それがutf8の文字列である事は約束されているので、「うまいこと」やる必要はありません。(そもそもlengthとかで返ってくるのが「文字数」になります。バイト数を数えたいときには余計なお世話ですが。)
また、正規表現なども多バイト文字をそのまま使えます。Perlコード中に埋め込まれるリテラルもすべて内部表現として扱われているので、日本語だろうとなんだろうと、そのまま書けばそのまま動くということになるのです。とても普通の事ですが、これまでは結構面倒なこともありましたよね。あの厄介な、文字境界を考慮した正規表現だとかなんだとかとは、もうおさらばです。
さて、基本的には上記の考えを飲み込んでおけばOKなのですが、とっても親切かつ厄介な機能があります。「自動アップグレード」です。
例えば、内部表現になっている文字列と、ただのオクテット列を連結するときなどに、これが起きます。
utf8::is_utf8( $utf8 ); # => 1
utf8::is_utf8( $octets ); # => 0
# このとき
$mixed = $utf8 . $octets;
utf8::is_utf8( $mixed ); # => 1
当然と言えば当然なのですが、このアップグレードの際には、オクテット列が実際のところいったい何の文字セットであったかは考慮されない(というかPerlは知らないのでlatin-1だと考える事にする)ので、勝手な変換をされて盛大に化ける訳です。ほんの一つの変数の影響で、出力全体がおかしくなるといった事もよく起きます。utf8フラグが疎まれるのも、一つフラグのたっていない変数が紛れ込むだけで、破壊的な結果をもたらすからだと思われます。
まずはおさらいとして、use utf8した環境では、「外から持ってきたオクテット列はいったん内部表現に」「外に出すときは内部表現からオクテット列に」変換してやるのがルールです。文字化けが発生するとき、このルールを侵犯しているひとがどこかにいるということになります。
解決のためには、とりあえずどいつがutf8フラグを持っていてどいつが持っていないのかを調べてゆく必要はあります。調べる方法は、上でもちょっと使っていますが、utf8パッケージに含まれるis_utf8関数です。戻り値が真なら内部表現になっています。
原因となる変数を特定出来たら、あとはそれをどうにかするだけです。「どうにか」にもいろいろあり、自分で触れる範囲ならばきちんとルールに従うように修正する、修正不能なモジュールの中とかならdecodeやutf8::upgradeなどを使って連結前にフラグを調整する、といったことを行います。
jcode.plやJcode.pmでは、とにかく文字列を投げ込んでやればいい感じに判定して変換してくれましたが、encode/decodeでは関数の引数に文字セットを指定しているので、推定どうするんだって話になるかもしれません。文字セットの推定は、Encode::Guessモジュールを使います。ちょっとここにも抜粋しておきます。
use Encode;
use Encode::Guess qw( euc-jp shiftjis 7bit-jis );
print encode( "euc-jp", decode( 'Guess', $input ) );
あるいはguess_encoding関数がexportされているので、これを使うと推定結果をオブジェクトとして取得することが出来ます。
my $guess = guess_encoding($input, qw/euc-jp shiftjis 7bit-jis/);
ところで、Encodeの文字セット指定はこれまでとちょっと違っているので気をつけましょう。(と終わり間際に書く…。)日本語ならEncode::JPのperldocを読めば書いてあります。
最後にJcode.pmのこともちょっとだけ触れておきます。
現行のJcode.pmはEncode.pmのラッパーになっていますが、次のような事をする場合
Jcode->new($string)->utf8;
結果は内部表現ではなくてオクテット列です。
なので、encodeの代わりには使えますが、decodeの代わりには(そのままでは)ならないという事は、覚えておいた方が良いかも。そのかわり、encode/decodeの場合はひと手間必要だった文字コードの推定を、Jcodeは自動でやってくれるので、その辺は便利ですね。もちろん、推定しなくてもわかっているなら、指定しておいた方が安全です。
どうでもいいけどコンストラクタのエイリアスとしてjcode関数がexportされるので、それを使って書くとほんのちょっと気持ちいいです。
jcode($string)->utf8;
ただ、Jcodeは内部的にeucで持っているらしいところがちょっと気になるんですが…詳しく調べてない。誰かに聞きゃ良いんだけど。
まとめとしては、次のようなものでしょうか。
Perlでの多言語処理では、これが「たった一つの冴えたやりかた」です。この考え方をきちんと頭に入れ、そして徹底する事が、問題解決のキーになるでしょう。
前の文章を書いていた頃は、思えばまだはてなに入社していないどころかそれよりずっと前で、オブジェクト指向すら良く分かってなかったです。当時は懐かしい感じのPerl/CGIコードしか書けず、utf8まわりには盛大にハマりました。よくもまぁ頑張ったものだ。というわけで、前の文章は要するに「若気の至り」な文章でした。読み返してみると、なかなか紆余曲折していて、当時の混乱が目に浮かぶようです。
今だと、より良い解説はたくさんあるので、これまで放置していましたが、意外とここがブックマークされていたりとかして、いつかどうにかしなきゃなぁと感じていました。ということで、YAPC::Asia 2009があるので、併せて書き直してみた次第です。
何かの参考になれば幸いです。