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

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

この記事を読むのに必要な時間:およそ 6 分

一般的には推奨されない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)。

※1

このような誤解を防ぐため,最近ではこの内部表現がどのようなものかについてはなるべく触れず,Perlにとっては「オクテット列」に見えているか,「文字列」に見えているかだけを意識してもらえるような説明をしていきましょう,ということになっています。

このオクテット列と「文字列」の違いは,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);

著者プロフィール

石垣憲一(いしがきけんいち)

あるときは翻訳家。あるときはPerlプログラマ。先日『カクテルホントのうんちく話』(柴田書店)を上梓。最新刊は『ガリア戦記』(平凡社ライブラリー)。

URLhttp://d.hatena.ne.jp/charsbar/

コメント

コメントの記入