モダンPerlの世界へようこそ

第32回Encode:日本語だけ扱えればよいのではなく

一般的には推奨されないencodingプラグマ

前回取り上げたencodingプラグマは、簡単なjperl用のスクリプトを移植したい場合には便利ですが、perlunifaqというPerl付属のマニュアルにははっきり「Don't use it.」と書いてあるくらい、一般的には使えないプラグマと認識されています。

前回も見たように、encodingプラグマが対応しているのは、ソースコードに埋め込まれている文字列やそれに類する正規表現、そして標準入力からのデータを指定された文字コードからPerlの内部表現に変換し、標準出力へ出力する際には内部表現を指定された文字コードに変換することだけです。ほかのファイル入出力部分や、コマンドラインから受け取った引数、標準エラー出力などの変換は行わないので、ちょっと凝ったことをしようと思うと、結局「外から入ってきたものはデコード、外に出すものはエンコード」という原則にしたがってひとつひとつ処理を追加していかなければなりません(内部表現に変換されてしまっている部分もあるため、すべてをバイナリのまま扱う、ということすらできないのがencodingの泣き所です⁠⁠。

また、encodingプラグマ(やopenプラグマ)が依存しているPerlIO層は、かならずしもすべてのPerlが対応しているわけではありません。PerlIOを使った処理のなかには環境によってレイヤ切り替え時の処理が甘かったり、不安定だったりするものもあるので、一時的な利便性より移植性や安定性のほうが重視されるような場合は、ひとつひとつEncodeで明示的に処理していくほうがおかしな副作用に悩まされなくてよい、ということもできます。

Encodeについてはメンテナである小飼弾氏が折に触れて日本語の解説記事を書いていますし、業務のなかで触れる機会も多いでしょうから概要は先刻ご存じと思いますが、今回はあらためてEncodeの基本をおさらいしておきます。今回も前回同様、例はすべて日本語Windows環境でのものですが、ほかの環境でも大筋は同じですので適宜読み替えていってください。

Encodeの基本

前回もさらりと触れたように、Perlは5.5系列から5.6系列に移行する際のひとつの目玉としてUnicodeへの対応を行いました。ただし、このとき追加されたのはutf8というプラグマだったことからもわかるように、このときの対応はあくまでもUnicodeを使えるようにするのが主眼で、Unicode以外の文字コードへの対応は不十分なものでした。そのギャップを埋めるために登場したのが、Perl 5.8系列でコアモジュール入りしたEncodeです。

Encodeは、ごくおおざっぱにいうと、人間の目には特定の文字コードにしたがっているように見えるものの従来はバイナリとして扱うしかなかったデータを、Perl 5.6でUnicode対応する際に用意された内部表現に変換することで、正しく「文字列」として扱えるようにするものです。5.6系列で導入されたプラグマの名前からも想像がつくように、この内部表現はUTF-8と密接な関係がありますが、ローカルな文字コードをUTF-8という具体的な文字コードに変換するものとは考えないでください。もっと具体的にいうと、Encodeは単にシフトJISのテキストをUTF-8に変換することで「5C問題」を解決するためのものではありません。あくまでもシフトJISとして解釈すれば意味を持つオクテット列をPerl内部で使われている特殊な「文字列」に置き換えることでマルチバイト文字の泣き別れなどを防ぐものです[1]⁠。

このオクテット列と「文字列」の違いは、length()の結果を比べてみるのがわかりやすいでしょう。Windows上で以下のようなスクリプトを用意して実行すると、Perlが認識している両者のデータ長が異なっていることがわかります。decodeは、Encodeがデフォルトでエクスポートしてくれる、オクテット列を「文字列」に変換するための関数でした。最初の引数に文字コード、2番目の引数に変換したいオクテット列を入れると、⁠文字列」に変換された値が返ってきます。

use strict;
use warnings;
use Encode;

my $binary = 'あ';
my $string = decode(cp932 => $binary);

printf "binary length: %d\n", length($binary);  # 2
printf "string length: %d\n", length($string);  # 1

ただし、この「文字列」はあくまでもPerlの内部表現です。外部のOSやファイルシステムにとっては意味のあるものではありませんので、標準出力のようにPerlが直接管理していない場所ではそのまま使うことはできません。無理矢理使おうとすると、Perlのほうで「Wide character in print(ワイド文字がprintに含まれています⁠⁠」といった警告を出して、内部表現を無理矢理OSなどが理解できるバイナリに直してしまいます。このとき、そのバイナリが運よく人間の目にも適切な意味をもっているように見えてしまう環境もありますが、日本語Windows環境の場合はそうではないので、そのままコンソールに出力すると文字化けが起こります。

use strict;
use warnings;
use Encode;

my $binary = 'あ';
my $string = decode(cp932 => $binary);

print $string;  # Wide character in print...

この文字化けを解消するには、前回も見たように、出力の際にencodeを使って内部表現を適切な文字コードにあわせたバイナリに戻す必要があるのでした。encodeもEncodeがデフォルトでエクスポートしてくれる関数で、最初の引数に文字コード、二番目の引数に「文字列」を渡すと、正しい文脈に変換されたバイナリを受け取ることができます。

use strict;
use warnings;
use Encode;

my $binary = 'あ';
my $string = decode(cp932 => $binary);

print encode(cp932 => $string);

Encodeのむずかしさ

では、このような場合はどうなるでしょうか。

use strict;
use warnings;
use Encode;

my $binary = 'あ';
my $string = decode(cp932 => $binary);

$string .= 'い';

print encode(cp932 => $string);

このスクリプトを実行すると、$stringには「あ」という内部表現であらわされた「文字(列⁠⁠」のうしろにシフトJIS (CP932)のバイナリが(強制的に「文字列」扱いされて)つなげられた、おかしな「文字列」が格納されます。もちろんそのおかしな「文字列」は、もとが壊れているのですから、いくら変換したところでCP932として正しい出力にはなりません。

期待通りの出力を得るには、⁠い」のほうもきちんと「文字(列⁠⁠」にしてから結合する必要があります。

use strict;
use warnings;
use Encode;

my $binary = 'あ';
my $string = decode(cp932 => $binary);

$string .= decode(cp932 => 'い');

print encode(cp932 => $string);

Encodeのむずかしさは、このようにdecodeしていないバイナリが自動的に「文字列」に昇格したり、先ほどの例のようにencodeされていない「文字列」が自動的にバイナリに降格したりしてしまうところにあります。

前回から再三書いているように、この「文字列」はあくまでもPerlの内部表現なので、文字コードの情報は持っていません(decodeは「文字コードを取り除く」という意味でした⁠⁠。また、バイナリのほうも、Perlからしてみれば単なるオクテットの羅列にすぎません。その配列に意味を見いだせるのは人間だけです。その文脈(文字コード)を伝える機会が失われるのですから、自動変換が行われたときに従来の日本語環境で文字化けが生じるのは当然のことでしょう。

ただし、自動変換は迷惑なだけではありません。たとえば、$stringに結合するものが改行をあらわすバイナリだったらどうでしょう。ここでは便宜上「\n」と表記しますが、このバイナリは、たいていの文字コードで同じ意味を持ちます。逆の言い方をすれば、文字コードという文脈を取り払っても困らない「文字列」に近いものともいえます。そのようなものを結合するときにまでいちいちdecodeしなければならないようではいささか手間です。自動変換があるおかげで、日本語の「文字列」に改行を追加することが簡単にできるようになっているのです。

use strict;
use warnings;
use Encode;

my $binary = 'あ';
my $string = decode(cp932 => $binary);

$string .= "\n"; # ≒ $string .= decode(cp932 => "\n");

print encode(cp932 => $string);

また、あいにく日本語Windows環境の場合は異なりますが、世の中にはPerlの内部表現を強制的にバイナリ扱いしても人間の目には問題なく見えてしまう環境というものも存在します。ASCII文字だけですべてが事足りてしまう環境も含めれば、むしろそちらのほうが主流といってもよいでしょう。そのような環境では、いちいちencodeを通さなくても、内部表現をそのままバイナリ扱いできたほうが手軽なはずです。

use strict;
use warnings;
use Encode;

my $binary = 'abcde';
my $string = decode(cp932 => $binary);

print $string; # ≒ print encode(cp932 => $string);

あらかじめ問題が起こらないようにする

とはいえ、いちいちデータにマルチバイト文字が含まれているかどうかを調べてdecodeやencodeの必要性を判断するのはばかげています。使い捨てのスクリプト、特定の環境でしか動かす予定のないアプリケーションならともかく、モジュールなどの再利用性を考えるなら、どんな環境でも正しく動かせるコーディングをしておくにこしたことはありません。

そのためによく使われるのが、⁠外から入ってきたデータはすぐに内部表現化して、アプリケーション内部では常に内部表現のまま扱い、外に出す直前にバイナリ化する」というやり方です。

あるいは、⁠文字コードを意識するのはファイルや標準入力からデータを取り込むとき、あるいはファイルや標準出力にデータをはき出すときだけ。それ以外のところではいっさい文字コード(や、文字の種類)を見てはいけない」といったほうがわかりやすいでしょうか。

もっと具体的にいうと、以下のものについてはdecodeします。

  • スクリプトにべた書きされているマルチバイト文字
  • read系のコマンド(のような書き方を含む)の返り値
  • qx// (``)の返り値
  • @ARGVや%ENVといったシステム依存の特殊な変数
  • その他、システムコールを利用しているネットワーク/データベース系の各種コマンドの返り値

encodeを通すべきものとしては以下のものがあります。

  • print/write系コマンドの引数
  • dieやwarnの引数
  • open系のコマンドやrename、unlinkのようにファイル/ディレクトリ名を指定する各種コマンドの引数
  • systemやexec、qx//の引数
  • システム依存の特殊変数を更新するときはその値

これらはモジュールのなかに隠蔽されていることも多いので、実際にはそれほど単純な話ではありませんが、出来のよいCPANモジュールであればふつう内部で文字コードや文字種別をいじっている場合にはそれとわかる名前がついているか、ドキュメントにその旨書かれています。特に記載がなければひとまずそのモジュールは文字コードまわりの処理はしていないものと考えて、最終的にPerl以外のシステムを経由しそうな入出力まわりの値については値の受け渡しをするときにEncodeの関数を通しておけばよいでしょう。もちろんなかには例外もありますので、可能であればモジュールの内部をひとつひとつ確認しておくにこしたことはありませんし、ほかにもXSを利用して直接CのAPIを叩いているようなものについてはdecode/encodeが必要になりますが、そのあたりは上の応用ですので適切に処理してください[2]⁠。

find_encoding

ここまではdecode/encodeを関数的に利用してきましたが、文字コードの変換にはそれなりのコストがかかります。あらかじめ文字コード表をメモリに読み込んでおけば高速化は期待できますが、使いもしない文字コードの分までメモリに入れておくのは無駄なことです。そのため、Encodeではfind_encodingという関数を使って必要な文字コードだけを読み込み、オブジェクト化できるようになっています(decode/encode関数も内部的にはこのfind_encodingを利用してオブジェクトを生成しています⁠⁠。

use strict;
use warnings;
use Encode;

my $cp932 = find_encoding('cp932');

my $binary = 'あ';
my $string = $cp932->decode($binary);

print $cp932->encode($string);

find_encodingを使うと、この程度の例でも筆者の環境では5~6倍速くなりました。使い捨てのスクリプトやワンライナーではさほどありがたみはありませんが、永続的な環境で同じ文字コードの変換を何度も繰り返すのであればさらに差は広がるでしょう。

また、find_encoding(や、それを利用しているdecode/encode関数)は、文字コードの表記のゆれもよしなに判別してくれます。日本語の文字コードについてはEncode::JPに簡単なまとめがありますので、読んだことがなかった方は一度目を通しておいてください。

文字コードがわからない場合

Encodeは文字コードを変換する際にそれぞれの文字コードにあわせたオブジェクトを生成するため、コーディングの際にはすべての入出力について、どの文字コードを使えばよいかわかっている必要があります(未知の文字コードが指定されると、find_encodingはオブジェクトを生成できず、エラーを出して死んでしまいます⁠⁠。

ときには外からどのような入力がくるかわからない場合もありますが、コンソールアプリケーションであれば宮川達彦氏のTerm::Encodingというモジュールを利用すれば標準入出力のエンコーディングを判定できます。ウェブアプリケーションであれば(運がよければ)Content-Typeヘッダなどから文字コードの情報を取得することもできるでしょう。

どうしてもほかに手段がなければ、Encodeに付随するEncode::Guessというモジュールを使って文字コードを推測する手もあります。

たとえば、japaneseディレクトリには(どの文字コードで書かれているかはともかく)日本語で書かれていることだけはわかっている雑多なテキストファイルが格納されているとしましょう。最近のテキストエディタであれば文字コードの自動判別くらいしてくれるでしょうが、grepのような小さなツールを活用しようと思うと、やはり端末で標準的に使われている文字コードに揃えておきたいところです。

最低限の用事が足りればよいなら、このような感じになるでしょうか。ここでは横着をしてファイル名の処理を省略していますが、杓子定規に書くなら、globで得られたファイル名はバイナリの状態ですから一度$term->decodeし、read_fileやwrite_fileに渡すときに同じく$term->encodeするべきところです。

use strict;
use warnings;
use Encode;
use Encode::Guess qw/euc-jp shiftjis 7bit-jis/;
use Term::Encoding;
use File::Slurp;

my $term = find_encoding(Term::Encoding::get_encoding());

foreach my $file (glob 'japanese/*.txt') {
    my $content = decode('Guess', read_file($file));
    write_file($file, $term->encode($content));
}

また、本当に文字コードが正しく推測できるか不安な方は、decodeまわりをこのように変えてください。guess_encodingは、find_encodingに似ていますが、正しく推測できた場合は文字コードのオブジェクトを、推測に失敗した場合は候補を列挙した文字列を返すので、refで条件分岐すれば処理を中断できます(ここも、理屈の上ではdieの引数は$term->encodeしておくべきですが、$fileも$guessもバイナリのままなのがわかっているので省略しています⁠⁠。

foreach my $file (glob 'data/*.txt') {
    my $content = read_file($file);
    my $guess = guess_encoding($content);
    die "$file is $guess" unless ref $guess;
    write_file($file, $term->encode($guess->decode($content)));
}

なお、文字コードを推測する場合は、なるべく大きな塊にまとめて(できれば上記のようにファイルを丸ごと読み込んで)推測するのが吉です。ファイルを一行ずつ処理したいからといって一行ずつ文字コードを推測していくと、短い行の判定を誤ったときに、ひとつのファイルに複数の文字コードを持つ部分ができてしまい、エディタなどの自動変換がきかなくなってしまうことがあります。どうしても一行ずつ処理しなければならないような場合は、まとめて読み込んで文字コードを判別してから各行に分割したり、あらかじめ十分な量を先読みして文字コードを判別してから、あらためてファイルを開き直すなどすればよいでしょう。

from_toは避ける

この例のように「文字列」を利用して何かをするのではなく、単に文字コードを変換したい場合は、from_toというショートカットを使いたくなるかもしれません。

ただし、from_toはデフォルトではエクスポートされませんし、バイナリから(内部表現を経由して)別のバイナリに変換するだけなので、先に紹介したEncodeを使うときの原則からは外れてしまいます。このように読み込んだ内容をただ書き出すようなごく単純な用途であればまだしも、読み込んだ内容をさらに修正したいのであれば、バイナリの状態に戻してから編集するより、⁠文字列」の状態で編集してから最後にエンコードしたほうが効率的です。また、from_toの返り値は変換したあとのバイナリではないため、デバッグ用途でprint文などの引数をラップする用途には使いづらい、という問題もあります。from_toを使うくらいなら、多少記述量は増えますが、その場でdecodeしたものをencodeしなおしたほうがよいでしょう。

use strict;
use warnings;
use Encode;

my $binary = encode(utf8 => "\x{5abe}");
print encode(cp932 => decode(utf8 => $binary));    # OK。副作用なし
print Encode::from_to($binary, 'utf8' => 'cp932'); # ×。副作用あり

EncodeのラッパとしてのJcode

「encode(cp932 => decode(utf8 => $string))」という書き方では冗長だと思う方は、いまは単なるEncodeのラッパとなってしまったJcodeを利用する手もあります。jcode.plから引き継いだconvert関数は、内部的でEncodeのfrom_toを使っているので副作用があり、使いどころは限られますが、jcode関数を利用してオブジェクトを生成するやり方であれば副作用もありません。以下の例では明示的にもとのエンコーディングを指定していますが、省略すれば内部で自動的に文字コードを判定してくれます。

use strict;
use warnings;
use Encode;
use Jcode;

my $binary = encode(utf8 => "\x{5abe}");

print jcode($binary, 'utf8')->sjis;            # OK。副作用なし
print jcode($binary)->sjis;                    # OK。文字コードは自動判別
print Jcode::convert($binary, 'sjis', 'utf8'); # △。副作用あり

伝統的なJcodeの使い方ではEncodeのfrom_toの場合と同じくバイナリから(内部表現を経由して)バイナリへと変換するだけなので、Encodeを使うときの原則からは外れてしまいますが、Jcodeには正規表現のラッパなども用意されているので、上手に使うとEncodeの冗長さを隠しつつ、必要な部分だけ内部表現を使って処理することができるようになります。時と場合に応じて使い分けてください。

おすすめ記事

記事・ニュース一覧