前回の(1)はこちらから。
文字化け問題 ── Perlの文字列の実装に迫る
次のテーマは文字列です。文字化けは、プログラマの意図したエンコーディングと実際のエンコーディングが異なるときに起きる現象で、どんなプログラミング言語でも問題になります。ただPerlではv5.8.0以降、さらにテキスト文字列という概念ができたため、プログラマの意図をコードに落とす際に少し工夫が必要です。この概念を正しく理解していないと、エンコーディングが正しく見えるときでも文字化けが起きることがあるのです。そこで本節では、文字化けをデバッグするという目的のもと、perlの文字列の実装に迫ります。
まず、よくある文字化けを再現するコードがリスト7です。CPANモジュールのEncode::Locale[6]をインストールしておいてください。また、ファイルはUTF-8で保存してください。
このスクリプトをhello-mojibake.plという名前で保存し、perl hello-mojibake.pl パール
として実行すると、文字化けが発生します。
Perlの文字列 基礎編
実装の詳細に入る前に、まず基本を確認しましょう。Perlの文字列は、概念としては「テキスト文字列」と「バイト列」(またはバイナリ文字列)があります。外部からの入力値は基本的にバイト列で、これをEncodeモジュールでdecode()
したものはテキスト文字列になります。テキスト文字列をencode()
したものはバイト列になります。utf8プラグマ(use utf8;
)は、現在のソースファイルの文字列リテラルのエンコーディングをUTF-8とみなしてdecode()
するので、このプラグマのもとでは文字列リテラルはテキスト文字列になります。
テキスト文字列とバイト列
ここで、テキスト文字列とは、文字どおりテキストのために用意された文字列のことです。私たちがふだん「文字」と呼んでいるものは、特定のエンコーディングに基づいて表現された整数値です。たとえば、「あ」をUTF-8というエンコーディングで表した値は12354
で、これをファイルに保存すると"\xe3\x81\x82"
という3バイトのバイト列になります。しかし、この3バイトのバイト列をPerlが読み込み、テキスト文字列として扱えるように「デコード」という操作を行うことで、Perlの文字列操作関数がこの3バイトのバイナリを1つの文字だと認識してくれるのです。たとえばテキスト文字列に対してはlength("こんにちは")
は、5
を返しますし、uc("ω")
(ギリシャ文字オメガの小文字)は"Ω"
(ギリシャ文字オメガの大文字)を返します。
一方バイト列とは、Perlがその意味を認識できない文字列です。それは、UTF-8でエンコードされた文字列かもしれませんし、CP932でエンコードされた文字列かもしれませんし、画像ファイルを読み込んだデータかもしれません。それをどうやって操作するかは完全にプログラマの手に委ねられています。先の例で言えば、入力値がバイト列であれば、length()
はバイト列の長さを返し、uc()
はおそらく意味のある値を返しません。
Perlはテキスト文字列をどのように操作すべきかを知っているため、Perlスクリプト内ではテキスト文字列を扱うほうが便利でしょう。しかし、外部の環境と文字列をやりとりする場合は、テキスト文字列のままでは渡せません。たとえば、ユーザが使っているターミナルのエンコーディングに合わせてencode()
しなければ、出力する文字列が文字化けする可能性があります。あるいはデータベースやファイルとのデータのやりとりも、それぞれのプロセスや環境に合わせてテキスト文字列をencode()
する必要があります。
文字列を正しく扱う方法
以上を踏まえてプログラマがすべきことをまとめます。外部から来た文字列がテキスト文字列かバイト列かはPerlは知りえないため、プログラマがdecode()
でテキスト文字列であることを確定させます。このとき、プログラマはその文字列のエンコーディングを知っている必要がありますが、その文字列の出自がわかっていればEncode::Localeのようなモジュールでエンコーディングを推論することもできます。このテキスト文字列を外部環境であるターミナルやデータベースに出力するときは、その外部環境にとって適切なエンコーディングでencode()
します。
文字化けを修正する
ここで振り返ってリスト7を見ると、utf8プラグマを使っているのでこのソースファイルのエンコーディングはUTF-8であり、文字列リテラルはテキスト文字列です。また、リテラルを出力する際にターミナルの設定に合わせてencode()
しています。しかし、@ARGVはターミナルから与えられた文字列なのでバイト列です。そこで、@ARGVをdecode()
すれば、このプログラムは正しく動くようになるはずです。それがリスト8です。
次のように実行してみましょう。
今度は正しく表示されました。外部環境からの入力値はdecode()
し、出力値にencode()
する。これを守っていれば基本的に文字化けは起きません。しかしながら、モジュールによっては実装があいまいなことがあり、そのモジュールが何を受け付けて何を返すのかがはっきりしておらず、そういう場合はその場しのぎのencode() / decode()
が必要になることもありえます。そしてそのようなときに、内部構造を調べることで解決の糸口を見つけることができるのです。
それでは、テキスト文字列やバイト列とは何なのか、内部構造を調べてみましょう。
perlの文字列 実装編
さっそくDevel::Peekモジュールで覗いてみましょう。リスト9は各種テキスト文字列をDump()
するものです。
さて、見るべきところはFLAGSとPVフィールドの値です。バイト列とテキスト文字列はともにPOKフラグが立っていますが、テキスト文字列のほうはUTF8フラグが立っています[7]。このUTF8フラグは、このSVがUTF-8からデコードしたテキスト文字列であることを示しています。ただし、この2つの文字列のCレベルでのバイナリ表現は同じです。それがPVフィールドの"\343\201\202"\0
です。UTF8フラグ付きのテキスト文字列のほうはさらにUnicode表現も出力されていますが、内部表現としてはUTF-8でエンコードされたバイト列に等しいのです。
UTF8フラグ
この2つの文字列の違いはUTF8フラグだけですが、perlは明確に別のものとして扱います。たとえば、$a eq decode_utf8($a)
は偽になりますし、length()
もバイト列では3を返し、テキスト文字列では1を返します。それでは結合するとどうなるでしょうか。バイナリ表現が同じなのだから、結合に成功してもよさそうなものです。しかし実際にはリスト7で見たように文字化けを引き起こします。
では、内部構造はどうなっているのでしょうか。リスト9のmy $a = " あ";
の直後にDump $a . decode_utf8($a)
を実行してみた結果が図3です。まず、UTF8フラグが立っているので、テキスト文字列扱いとわかります。PVフィールドをよく見ると、後半に"\343\201\202"
というバイナリ表現があり、リスト9のテキスト文字列と同じ状態です。しかし、それより前のバイナリ"\303\243\302\201\302\202"
はもともとの「あ」のUTF-8表現とは別のものになっています。テキスト文字列部分はそのまま残っているということは、perlがバイト列に何らかの加工を施したうえで結合し、その結果文字化けが起きたということでしょうか。
文字列結合演算子の実装
これはperlの実装を見てみましょう。結合演算子はpp_hot.c
[8]にあるpp_concat()
という関数です。必要なところだけリスト10に抜粋します。
関係ないところは省略していますが、重要なのは288行目のsv_utf8_upgrade_nomg()
とここを通る条件です。これは、結合演算子の左辺値と右辺値のUTF8フラグの状態(SvUTF8(sv)
)が異なるとき(lbyte !=rbyte
)、左辺値がバイト列であれば(if (lbyte) ...
)、戻り値(TARG
)にsv_utf8_upgrade_nomg()
という操作をするという意味です[9]。左辺値と右辺値のUTF8フラグの状態は異なるので、このとき右辺値のUTF8フラグは立っています。
sv_utf8_upgrade_nomg()
はPerlからもutf8::upgrade()
という関数で呼ぶことができます[10]。そしてこの関数は、引数のUTF8フラグが立っていれば何もせず、UTF8フラグが立っていなければ引数をLatin-1(ISO/IEC 8859-1)でエンコードされたバイト列と見なし、それをUTF-8バイト列に変換したうえでUTF8フラグを立てる、つまりLatin-1としてデコードします。UTF-8でエンコードされた文字列をLatin-1としてデコードするため、文字化けを引き起こすというわけです。
デフォルトエンコーディングとしてのLatin-1
なぜPerlのデフォルトのエンコーディングはLatin-1なのでしょうか。これは、Perlは当初Latin-1を一般的に使用していた欧米で広く使われていたためです。現在は世界的にUTF-8が広く使われるようになっていますが、互換性のためにデフォルトのエンコーディングはLatin-1のままになっています。
したがって、「外部からの入力値はバイト列」という説明は正確ではありません。外部からの入力値は、テキスト文字列なのかバイト列なのか確定していない状態で、これをどのように扱うかはプログラマに委ねられているのです。ただし、decode()
せずにテキスト文字列として扱う場合、そのエンコーディングはLatin-1とみなされるので、日本語環境でUTF-8やShift_JISなどを使うとエンコーディングの不一致により文字化けが起きるということなのです。