2010年4月4日日曜日

perl における文字コードについて

Perl における文字コードについて曖昧な理解だったので勉強がてらまとめてみる。

とあるPerlアプリケーションを修正したくて挙動をしらべたところ、length($str)が文字数じゃなくてバイト数を返しているのが原因と分かった。しかし、関連しそうなトピックをググっても、Perl に詳しい人がまとめた解説が多く引っかかるのだけど、基本的なことが分かってない自分には高度過ぎてよくわからない。しかも perl 5.6 以前 -> 5.6 -> 5.8 あたりでドラスティックに変更されているようで、そのへんの事情に疎い自分はどの情報を信じればいいのかよくわからない。(が、一番わかりにくくなっている原因は "UTF8" と "Perl内部エンコーディング" の妙な交換可能性だと思う。まるでCのポインタと配列みたいだ)



というわけで、主にEncode(3pm)で調べた結果をまとめておく。動作確認は perl-5.10.0 で行った。

基本

  1. Perl は専用の内部エンコーディングをもっている。
    Perl プログラムの外部から読み込んだ文字列は Perl 内部エンコーディングに変換して使わない限り、単なるバイト列として扱われる。つまり"こんにちは"という内容のファイルhello.txtを読み込むとき、
    open INPUT, "<hello.txt";
    $str=<input>;
    chomp($str);
    print length($str)."\n";
    

    とかやるとlength()はバイト数を返す(UTF-8だと15ですね。shiftjis,euc-jpだと10,iso-2022-jpだと16くらいとか)。これはプログラム内部で文字列として扱いたい場合には望ましくない挙動かもしれない。そんなときは内部エンコーディングに変換してやるとlength()が5、つまり文字数を返すようになる(方法は後述)。なお、内部エンコーディングは UTF-8 ではない。実際のところ、UTF-8エンコーディングされた文字列+UTF-8フラグが立っているものが内部エンコーディングらしいのだが、内部エンコーディング文字列と一般的に言うUTF-8エンコーディング文字列は別物、と考えるのが妥当だ。

  2. 読み込んだ文字列を内部エンコーディングに変換するには Encode::decode() を使う
    decode(ENCODING, $str); とすれば、$str を元の文字エンコーディング "ENCODING" からPerl内部エンコーディングに変換する。第一引数に指定するのは $str のもともとのエンコーディングだ。外部ファイルを読み込んだのなら、そのファイルのエンコーディングを指定する。たとえば euc-jp なファイルから読み込んだ文字列をPerl内部エンコーディングに変換する場合は、decode("euc-jp", $str);とする。具体的に何が指定できるかは Encode::Supported(3pm) および Encode::JP(3pm) を参照されたい。なお、1.で述べたとおり、Perl内部エンコーディングはUTF-8を含めた一般のエンコーディングとは別物であり、UTF-8のファイルを読み込んだときには $internal = decode("utf8", $str); とするべきだが、こうしてPerl内部エンコーディングに変換した$internalと$strは別物であることに注意が必要である。これはPerlから外部(端末、ファイル)に出力する時にPerl内部エンコーディング文字列 $internal をそのまま出力してはならない、ということを意味する。

  3. Perl内部エンコーディング文字列を外部に出力する場合はEncode::encode()する
    「Perl内部エンコーディング文字列 $internal をそのまま出力してはならない」のであれば、どうすればよいのかというと、 Enocde::encode(3pm)を使う。euc-jpで出力したい場合、$outstr = encode('euc-jp', $internal); とすれば $outstr は euc-jp エンコーディングの文字列が格納される。(これはPerlにとってはただのバイト列になったということを意味するので、length($outstr)などは文字数ではなくバイト数を返すようになる)
以上がPerlにおける文字エンコーディングの基本的な考え方になる。

Q&A

  1. Perl スクリプトそのものに埋め込まれた文字列はどうなってる?
    例えば
    $str = "こんにちは";
    print length($str);  # バイト列が返る
    

    などと書かれている場合を考える。これは Perl スクリプトファイル自体のエンコーディングのバイト列とみなされる(Perl内部エンコーディングとはみなされない)。つまり、例えばUTF-8でかかれたPerlスクリプトであっても、スクリプト内部に埋め込んだ文字列を、バイト列ではなく文字列として扱いたい場合は Encode::decode(3pm)する必要があるわけだが、これが面倒だという人のために(かどうか知らないが)、use utf8; というプラグマが用意されている。スクリプトの冒頭で
    use utf8;
    

    としておけば、埋め込まれた文字列がPerl内部エンコーディング文字列として扱われる。なお、use utf8;はスクリプトがUTF-8で書かれている(のでノータッチでPerl内部エンコーディングとして扱えるよ)とPerlに教える以外の意味はない。例えば外部から文字列を読み込んだら、たとえUTF-8エンコーディングのファイルから読んだ文字列だとしても、それはEncode::decode()しなければPerl内部エンコーディングとしては使えない。

  2. 文字列がPerl内部エンコーディングかどうか調べたい。
    Encode::is_utf8() または utf8::is_utf8() を使うと文字列が内部エンコーディングかどうか調べられる(is_utf8()という名前だが、実際はPerl内部エンコーディングかどうかを調べるということに注意が必要だ)。たとえば、
    #! /usr/bin/env perl
    print utf8::is_utf8("こんにちは") ? "Internal Enc.\n" : "Not internal Enc.\n";
    

    というUTF-8で書かれたスクリプト is_internal.pl を実行すると、
    % nkf -g is_internal.pl
    UTF-8 (LF)
    % ./is_internal.pl
    Not internal Enc.
    

    と表示される。前述のとおり、UTF-8で記述されたスクリプトファイルであっても、そのままでは内部エンコーディングとして扱われないことに注意。use utf8;していた場合、
    % cat is_internal.pl
    #!/usr/bin/env perl
    use utf8;
    print utf8::is_utf8("こんにちは") ? "Internal Enc.\n" : "Not internal Enc.\n";
    % nkf -g is_internal.pl
    UTF-8 (LF)
    % ./is_internal.pl
    Internal Enc.
    

    のようにPerl内部エンコーディングとして扱われることがわかる。

  3. 結局いつ decode()/encode() したらいいの?
    あなたが書いているPerlスクリプト外(端末、ファイル、プロセス等々)から文字列を読み込んだ場合は decode() が必要。読み込んだ文字列をそれらに書き出す場合は encode() が必要。

  4. 読み込んだ文字列をいちいちdecode()する(書き出すときにencode()する)のは面倒なんだけど?
    入力ファイル全体があるエンコーディングであることが確かならば、
    open INPUT, "<:encoding(utf-8)", $file;
    

    などで読み込む文字列を自動的にdecode()することが可能。上の例は入力ファイルが utf8 エンコーディングであることを仮定している。出力時は、
    open OUTPUT, ">:encoding(utf-8)", $file;
    

    とすればよい。

  5. 外部から読み込んだ文字列を自動的に判定することはできない?
    文字エンコーディングの自動判定のためにEncode::Guess(3pm) というインターフェースが用意されている。が、文字エンコーディングの自動判定は非常に難しい問題なので、個人的にはエンコーディングが何であるかはきっちり指定してdecode()するのが良いと思う。

その他、perlunifaq(1)も参考になる。

まとめ
  1. 文字列を読み込んだらEncode::decode()でPerl内部エンコーディングに変換せよ。
  2. 文字列を書き出す場合はEncode::encode()で対象のエンコーディングに変換せよ。(Perl内部エンコーディングを出力してはならない)
  3. Perl内部エンコーディングは一般の文字エンコーディングのどれとも異なる。utf8とも異なることに注意。
  4. Perl内部エンコーディングについてのインターフェースは "utf8" という名前がついていることがあるので注意(man等を良く見てどちらのutf8なのか理解して使うこと)
誤り等あればツッコミをお願いします。

1 件のコメント:

  1. で、肝心のアプリのほうなんだけど、どうも内部では完全にバイト列として扱ってるらしい。問題は文字数のカウントだけなので、カウントするときだけ内部エンコーディングに変換するという戦略も、それはそれでアリだと思う。入出力がほとんどutf8なら、読み書きのときにいつも変換ってのは無駄が多いし。

    返信削除