Perl の文字列エンコーディングの話
ハァイ,先日 Plack::Request::WithEncoding というモジュールをリリースしました,@moznion です.皆様いかがお過ごしでしょうか.
さて,このモジュールを書いてて Perl の文字列エンコーディングに関する知識を幾ばくか深めましたので,共有したいと思います.まあ大体の皆さんは「そんなこと知ってるぜ!! 常識だろ!」という趣だと存じ上げますが……
ただまあ,「文字化けしてつらい!!」みたいなのは誰しも通る道だと思いますので記しておくこととします!
TL;DR
入り口で decode して,内部ではすべて flagged utf8 で扱い,出口で encode する.これがすべてです!とにかくこの基本方針をまもっていれば幸せになれます.
ぶっちゃけ,上記のエントリ良すぎるのでそれ読めば良いです.
以下はもう蛇足ですね!
注意
本エントリで示すソースコードでは,紙面の都合上
use strict;
use warnings;
が省略されていますが,存在するものとして読み下して下さい.
use strict
しないと世界が滅びます.
このエントリで説明しないこと
- Perl とは何か
- 文字コードとは何か
Perl の処理系における文字列の扱い
Perl における文字列の扱いは,大きく2つに分けると「Perl 内部表現」と「バイト列 (latin-1)」に分類出来ます.
Perl 内部表現はその名の通り Perl が内部的に利用・保持する文字列のことです.よく“内部表現”だとか“内部文字列”だとか“flagged utf8”だとか呼ばれていますが,本エントリではわかりやすいので“内部表現”と呼ぶことにします.
内部表現ではない文字列に関して,Perl は バイト列 (latin-1) として扱います.
エンコーディング
本エントリで述べる「エンコード」は内部表現を指定したエンコーディング方式によりバイト列に変換する処理, 「デコード」はバイト列を内部表現に変換する処理のことを指します.
Perl では文字列のエンコード及びデコード処理を行う際には Encode モジュールを利用します.
本エントリではエンコード処理を行うメソッドである encode()
と デコード処理を行うメソッドである decode()
について説明します.
Encode::decode('Encoding method', 'なんらかの文字列')
decode()
メソッドは 'Encoding method' で表現されているバイト列を 「内部表現」に変換 します.
例えば以下のような感じです.
use Encode;
my $decoded = Encode::decode('EUC-JP', $str); # <= $str には EUC-JP でエンコードされた文字列が入っている
# <= だから decode() の第1引数は 'EUC-JP'
こうすると,'EUC-JP' でエンコードされている $str
の内容 (バイト列) が内部表現に変換され,その内部表現が $decoded
に格納されます.
Encode::encode('Encoding method', 'なんらかの文字列')
encode()
メソッドは「内部表現」を 'Encoding method' でエンコード し,バイト列に変換します.
例えば以下のような感じです.
use Encode;
my $decoded = Encode::decode('EUC-JP', $str); # <= $str には EUC-JP でエンコードされた文字列が入っている
my $encoded = Encode::encode('cp932', $decoded); # <= $decoded は内部表現
こうすると,内部表現である $decoded
が 'cp932' でエンコードされたバイト列に変換されて $encoded
に格納されます.
use utf8
Perl には use utf8
というプラグマがあります.
これはソースコードが 'utf8' によりエンコードされていることを伝えるプラグマです.
とは言ってもこの説明だと何が起こるのかがわかりにくいので,あけすけに説明しますと,
use utf8
してから文字列リテラルを使って文字列を作ると,その時点でその文字列は内部表現になります.
(他にも正規表現でユニコード使えるようになったり,関数名や変数名にユニコード使えるようになったりしますが
本筋から外れるのでここでの説明は割愛します)
注意としては,コードが書かれているファイル自体のエンコーディング方式が utf-8 である必要があります.
なので,ファイルエンコーディングが utf-8 である場合,
use Encode;
my $decoded = Encode::decode("utf-8", "あなたとJava");
my $encoded = Encode::encode("utf-8", $decoded);
print $encoded; # <= あなたとJava
というコードと
use utf8;
use Encode;
my $decoded = "あなたとJava";
my $encoded = Encode::encode("utf-8", $decoded);
print $encoded; # <= あなたとJava
というコードは等価です.
結局どーすりゃ良いのさ
入り口で decode して,内部ではすべて flagged utf8 で扱い,出口で encode する.これがすべてです!とにかくこの基本方針をまもっていれば幸せになれます.
まさにこれに尽きます.
そもそも,なぜ入り口で decode して,内部ではすべて内部表現で取り扱わなければならないのかというと, Perl の外の世界には様々なエンコーディング方式があって,混沌としており怖い世界な訳ですよ.
そういう多種多様様々なエンコーディング方式をそれぞれケアするのは大変なので, Perl ではそれらを統一的に扱えるように一度「内部表現」というものに落としこんでから, そこでゴニョゴニョ操作して,エンコードしてバイト列化してやってから外の世界に 持って行ってやるという感じですね.
あと,ここ最近は utf-8 以外でファイル保存する人とかいないし,面倒が減るから,
use utf8
プラグマは常に有効にしておけば良いのではないかという意見もありますね!
よくあるエラーや文字化け
(以降の例ではファイルエンコーディングは utf-8 であることが前提です)
内部表現をそのまま出力しようとしている
use Encode;
my $decoded = Encode::decode("utf-8", "あなたとJava");
print $decoded; # <= Warning!
あるいは
use utf8;
my $decoded = "あなたとJava";
print $decoded; # <= Warning!
みたいにしてやると,Wide character in print
という警告メッセージが出ます.
これは内部表現をそのまま外に出力しようとしている為に発生します.
なのでこれは以下のように encode して,内部表現ではなくしてやる必要があります.
(「内部表現ではなくすること」を,「UTF8フラグを落とす」と表現する事もあります.
UTF8 フラグについては後述します)
use Encode;
my $decoded = Encode::decode("utf-8", "あなたとJava");
my $encoded = Encode::encode("utf-8", $decoded);
print $encoded; # <= "あなたとJava"
おかしなエンコーディング方式で decode を試みている
use Encode;
my $decoded = Encode::decode("EUC-JP", "あなたとJava");
my $encoded = Encode::encode("utf-8", $decoded);
print $encoded; # <= 化ける!
こうすると文字化けします.それもそのはず,ファイルが utf-8でエンコードされている ので
"あなたとJava" は utf-8 になっています.それをこのコード例のように "EUC-JP" という違う
エンコーディングで decode しようとすると化けてしまいます.
解決策としては当然ですが,適切なエンコーディング方式で decode してやると良いです.
また, utf-8以外でエンコーディングされているファイル に対して use utf8
し,
なおかつ文字列にマルチバイト文字が含まれていると例外が発生します;
use utf8;
my $str = "あなたとJava"; # <= ここで警告が出る
use utf8
の注意の部分にも書きましたが,use utf8
する場合は,ソースコードのファイルエンコーディングも
utf-8 でなければなりません.
内部表現と,内部表現ではない文字列を連結させている
use Encode;
my $raw = "あなた";
my $decoded = Encode::decode("utf-8", "とJava");
my $concatenated = $raw . $decoded;
print Encode::encode("utf-8", $concatenated); # <= 文字化けする
まあ化けますよね! という感じです.説明は tokuhirom さんのブログが詳しいのでそちらを読んだほうが明らかに良いです.
Perl で utf8 化けしたときにどうしたらいいか - blog.64p.org
補足的情報
UTF8フラグ
“UTF8フラグ”とは,「内部表現になっているかどうか」を判断するフラグです. UTF8フラグが立っていればそれは内部表現であり,立っていなければその文字列はバイト列 (latin-1) です.
ここからはあんまり知らなくても良い情報です.CPAN モジュールを記述する人は気にするべきでしょう.
さてちょっと分かりにくいのですが,UTF8フラグが立っている文字列 (つまり内部表現) と
我々が日常使っているutf-8バイナリは 別物です.気をつけましょう.
UTF8フラグ,名前が良くなくて,本来なら「内部表現フラグ」とかそういう名前にするべきだと思うんですけど,
まあそうなっちゃってるから仕方がないので,そういうものだと諦めて下さい.
Devel::Peek
UTF8フラグが立っているか立っていないかは一見するとわかりませんが,Devel::Peek というモジュールを利用すると それを確認することができます.使い方としては以下のような感じです.
use Encode;
use Devel::Peek;
my $str = Encode::decode("utf-8", "あなたとJava");
Devel::Peek::Dump($str);
これを実行すると以下のような結果を得られます;
SV = PV(0x9fe5628) at 0x9ff7788
REFCNT = 1
FLAGS = (PADMY,POK,pPOK,UTF8)
PV = 0xa045ac8 "\357\277\275\357\277\275\357\277\275\312\244\357\277\275\357\277\275\357\277\275Java"\0 [UTF8 "\x{fffd}\x{fffd}\x{fffd}\x{2a4}\x{fffd}\x{fffd}\x{fffd}Java"]
CUR = 24
LEN = 28
色々と情報が記されていますが,ここで注目すべきは FLAGS
の部分です.上の例では FLAGS に UTF8 が記されており,
これは UTF8 フラグが立っている,つまり $str は内部表現になっていることを表しています.
一方で,
use Encode;
use Devel::Peek;
my $decoded = Encode::decode("utf-8", "あなたとJava");
my $encoded = Encode::encode("utf-8", $decoded);
Devel::Peek::Dump($encoded);
というコードを実行すると以下のような結果が得られるでしょう.
SV = PV(0x9d1e540) at 0x9d307d8
REFCNT = 1
FLAGS = (PADMY,POK,pPOK)
PV = 0x9da3a98 "\357\277\275\357\277\275\357\277\275\312\244\357\277\275\357\277\275\357\277\275Java"\0
CUR = 24
LEN = 28
この結果には FLAGS に UTF8 フラグが示されていない為,これは $encoded は内部表現ではないことがわかりますね.
※あとお気づきとは思いますが,2つの例では PV の値が違いますね.片方は内部表現 (つまり UTF8 フラグが ON) で,
もう片方は utf-8 でエンコードされた文字列 (つまり我々が普段使う utf-8 バイナリ) です.ね,別物でしょ?
これが Perl のエンコーディング!!!
なんだか複雑ですね!!!!!
ただまあ,慣れればどうということはないでしょう......多分.
この記事が Perl のエンコーディングの理解の一助になれば幸いです.
あとお気づきでしょうが,tokuhirom さんのブログをこのエントリでは3回も引用しています.
詰まった時に何を読めば良いかは,聡明な皆様だったらもうお分かりですね!
それはそうと
私事で申し訳ありませんが,先日わたくし退学に失敗いたしましたので,
退学に失敗したことを記念しまして,「退学失敗Night」というイベントが開催されます.
おそらく渋谷の HUB でただただ飲む会になると思いますので,お暇でしたらご参加下さい.