Perlによる日本語コード変換のメモ(第二版)

前に書いたときと時代背景も知識も大幅に違うので、書き直してみることにしました。Perl 5.8以降を前提として考えます。

この文章で書く(つもりの)こと

jcode.pl時代の文字セットの扱い

まずは、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も単なるオクテット列として扱っています。

utf8時代の考え方

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とPerlIOレイヤー

上記の考え方をもとにすれば、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

当然と言えば当然なのですが、このアップグレードの際には、オクテット列が実際のところいったい何の文字セットであったかは考慮されません。というと優しいですが、実際には 強引にlatin-1だと考えて変換してしまいます。このために、盛大に文字化けが発生する訳です。

ほんの一つの変数の影響で、出力全体がおかしくなるといった事もよく起きます。utf8フラグが疎まれるのも、ルールに則っていない変数がたった一つ入り込むだけで、破壊的な結果をもたらすからだと思われます。何せ全て化けたりするので、ショックが大きいものです。

文字化け発生時の対処

文字化けが起きたら、最初に疑うのが上記の自動アップグレードです。ほぼ99%、何かしらルールを無視する変数が紛れているので、これを突き止めるのが基本的な対処方法になります。

突き止める方法は、地道に変数をダンプして調べるしかありません。変数が文字列(内部表現)かそうでないかは、utf8パッケージに含まれるis_utf8関数を使用して調べることが出来ます。

utf8::is_utf8( $utf8 );   # => 1
utf8::is_utf8( $octets ); # => 0

原因となる変数を特定出来たら、あとはそれをどうにかするだけです。「どうにか」にもいろいろあり、自分で触れる範囲ならばきちんとルールに従うように修正する、修正不能なモジュールの中とかなら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の文字セット指定は、これまで(Jcode.pmやjcode.pl)とはちょっと違っているので気をつけましょう。日本語ならEncode::JPのperldocに書いてあります。

Jcode.pm

最後に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があるので、併せて書き直してみた次第です。

何かの参考になれば幸いです。

履歴

2009.09.08
第二版
2005.06.08
メンテナンス
2004.07.31
初版