Bulknews::Subtech RSSフィード

2008/02/18 (月)

utf8::is_utf8 considered harmful 14:28  utf8::is_utf8 considered harmful - Bulknews::Subtech を含むブックマーク はてなブックマーク -  utf8::is_utf8 considered harmful - Bulknews::Subtech

弾さんの書いてるのはもちろん間違いではないのだが、ちょっと今回はなしていたのとはポイントが違っていて、

なぜこうなっているかといえば、下位互換性。"Perlの文字リテラルはISO-LATIN-1である"という旧来の常識で書かれたコードがあまりに多かったのでこういう形になりました。

経緯からするとそうなんだけど、chr($n) で $n < 255 のときとそうでないときで utf-8 フラグがつく、つかないが変わる、ということからすると、latin-1 リテラルで Unicode 文字列をあらわす(latin-1 レンジの文字しか含まない場合)というのは今でも、それが perl の正しい文字列の扱い方ということになるとおもう。

そもそものきっかけは、Catalyst の uri_for の引数リストで Perl の Unicode 文字列のみを受け付け、それを utf-8 バイト列にエンコードして URI エスケープするという仕様。それに対して utf-8 バイト列も渡したいという要求があり、

utf8::encode($_) if utf8::is_utf8($_);

というコードを入れて、対応している。これは自分のCPANモジュールなんかでも、「引数は Unicode 文字列または UTF-8 バイト列を渡してくれればいい具合に処理しますよ」という目的でつけていたんだけど、最近やはりこれは間違いである、という認識に達した。

その理由は前述のようにlatin-1 レンジで構成された文字列の場合、処理によっては UTF-8 フラグがついてるかどうかはわからず、utf8::is_utf8 が偽となり、latin-1 でエンコードされてしまうため。

アプリケーションにおいてデータソースが UTF-8 バイト列であれば Encode::decode_utf8 や utf8::decode により UTF-8 フラグをたてることができ(これは弾さんの記事にもあるとおり、該当する文字列がlatin-1 のみで構成されているかどうかは関係なくフラグがたつ)、問題はない。また use utf8; も有効。

問題となるのは例えば以下のような場合、

use HTML::Entities;

my $s1 = "H&eacute;llo";
my $s2 = "H&eacute;llo &#x1234;";

my $t1 = decode_entities($s1);
my $t2 = decode_entities($s2);

warn utf8::is_utf8($t1);
warn utf8::is_utf8($t2);

$t1, $t2 はどちらも HTML を decode する、という処理でえられた変数であるが、$t1 は utf-8 フラグがなく(latin-1 レンジのみで構成されているため)、$t2 には UTF-8 フラグがついている。このような2つの変数に対し、utf8::is_utf8 でフラグをチェックして encode_utf8 するかどうかをきめるというのは間違っている。($t1 は latin-1 エンコーディングのままになってしまう)

よって uri_for などの関数・メソッドは「Unicode 文字列を受け付ける(UTF-8 フラグあり、または latin-1 エンコード)」ものと「バイト列を受け付ける」ものとで別のメソッドにするなり、パラメータで制御するなりする、というのがベストな解法ということになる。

ちなみにこうして得られた $t1, $t2 にたいして強制的にフラグをたてる(文字列が latin-1 レンジのみで構成されるかどうかにかかわらず)方法は utf8::upgrade() が最適。utf8::upgrade() はすでに UTF-8 フラグがついているものに対してはなにもせず、ついてないものについては latin-1 とみなして UTF-8 フラグつきにアップグレード(して内部表現も UTF-8 に変更)する。

このあたりは perlunitut - search.cpan.org に詳しい。最初にこのスライドを見たりMLで読んだときには「そうはいっても UTF-8 フラグでくるか UTF-8 バイト列でくるか、モジュールのバージョンによって違ったりするから判定しなきゃしょうがないよ」とおもっていたんだけど、やはり latin-1 文字列の例で考えると、このドキュメントのいっていることが正しいという結論に最近達しました。

それでは、問答無用にutf8フラグを立てるにはどうしたらよいでしょうか。今のところの公式の推奨は

use Encode;

my $utf8 = decode_utf8($unknown);

これは $unknown が「フラグがついているかどうかわからない文字列」という意味でつかっているのならば、正しくない。decode_utf8 は「UTF-8 もしくは ASCII のバイト列」に対して使うべきであり、「すでに UTF-8 フラグのたった文字列」や「latin-1 のみで構成されたバイト列」に対して実行すると文字化けを起こす。

やはり、プログラムやモジュールでは「この文字列は Unicode 文字列であるかバイト列であるかわからない」という入出力パラメータを扱うべきではなく、つねに「Unicode 文字列」や「バイト列」であることを明確にしておくべき。そうすれば decode_utf8 や utf8::upgrade などで明確にフラグをたてることができる。

Encode::_utf8_on や utf8::encode($_) if utf::is_utf8($_) はアプリケーション内のデータソースがすべてUTF-8バイト列およびUnicodeである、ということが明確である場合を除き、正しくない。(つまり CPAN モジュールなどで使うべきではない)