文字化け問題 ── Perlの文字列の実装に迫る
次のテーマは文字列です。文字化けは,プログラマの意図したエンコーディングと実際のエンコーディングが異なるときに起きる現象で,どんなプログラミング言語でも問題になります。ただPerlではv5.8.0以降,さらにテキスト文字列という概念ができたため,プログラマの意図をコードに落とす際に少し工夫が必要です。この概念を正しく理解していないと,エンコーディングが正しく見えるときでも文字化けが起きることがあるのです。そこで本節では,文字化けをデバッグするという目的のもと,perlの文字列の実装に迫ります。
まず,よくある文字化けを再現するコードがリスト7です。CPANモジュールのEncode::Locale(注6)をインストールしておいてください。また,ファイルはUTF-8で保存してください。
リスト7 hello-mojibake.pl
use strict;
use warnings;
use feature 'say';
use utf8;
use Encode;
use Encode::Locale;
my($name) = @ARGV;
say encode(locale => "こんにちは $name さん");
このスクリプトをhello-mojibake.plという名前で保存し,perl hello-mojibake.pl パールとして実行すると,文字化けが発生します。
- 注6)
- ターミナルのエンコーディングを自動的に識別するモジュールです。
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です。
リスト8 hello-ok.pl
use strict;
use warnings;
use utf8;
use feature 'say';
use Encode;
use Encode::Locale;
my($name) = map { decode(locale => $_) } @ARGV;
say encode(locale => "こんにちは $name さん");
次のように実行してみましょう。
$ perl hello-ok.pl パール
こんにちは パール さん
今度は正しく表示されました。外部環境からの入力値はdecode()し,出力値にencode()する。これを守っていれば基本的に文字化けは起きません。しかしながら,モジュールによっては実装があいまいなことがあり,そのモジュールが何を受け付けて何を返すのかがはっきりしておらず,そういう場合はその場しのぎのencode() / decode()が必要になることもありえます。そしてそのようなときに,内部構造を調べることで解決の糸口を見つけることができるのです。
それでは,テキスト文字列やバイト列とは何なのか,内部構造を調べてみましょう。

