こんにちは。ECナビラボの須藤(@ajiyoshi)と申します。
唐突です が、Perl(5.8以降)で文字化けに悩んだことはありませんか?私はあります。
そして、よくわからないまま"Perl 文字化け"などで検索すると、次のような単語が洪水のごとく溢れていて、なんだかよく分からなかったりしないでしょうか。
- 'flagged UTF8'
- Encode::decode
- Encode::decode_utf8
- Encode::encode
- Encode::encode_utf8
- 'Wide character in print at hoge.pl line xx.'
- use utf8
- open my $fh, '<:encoding(utf8)', $file
- binmode
- 「入り口で decode、内部でflagged UTF8、出口でencode」
- utf8::is_utf8
- utf8::upgrade
- utf8::downgrade
- utf8::encode
- utf8::decode
さらになんだかよくわからないままEncode::decodeしたりEncode::encodeしたりutf8::is_utf8で調べた りuse utf8をつけたりはずしたりしてるうちになんか動くようになったりして分からないままそのままにしていませんか?
この記事は そういう方のために書かれています。今回こそは、ちゃんと理解してみませんか。
内部文字列とUTF8バイナリは違う
Perl で文字化けしないために必要なことは、ほぼこの原則だた1点に集約されます。
内部文字列とUTF8バイナリをきちんと区別する
「ちょっと待って!」あなたは言います。「その内部文字列とかUTF8バイナリとかってのは一体何?」
この文書で使う用語の定 義:
- 内部文字列
- 内部文字列とはPerlが内部的に保持している文字列の表現のこと。普通は'flagged UTF8'と呼ばれる。
- UTF8バイナリ
- UTF8バイナリとは文字列をUTF8エンコーディングにより表現したバ イナリ列のこと。
Perlの内部文字列の表現としてUTF8が採用されていることからか、ユニコード文字列として特別扱いを 受ける内部文字列は'flagged UTF8'と呼ばれます。特にPerlのユニコード事情に詳しくない人にとって、この呼び方はエンコーディ ング方式としてのUTF8そのもの
や、UTF8 エンコードされたバイナリ列
と混同しやすいと私は思います。その上、Perlが内 部文字列をどのように表現しているのかについて、ほとんどの場合 プログラマが知る必要はないのです。無用な混乱を避けるため、この文書では「内部文字列」と呼ぶことにします。
内部文字列とUTF8バイ ナリは全く異なります。Perlはいまやユニコードを知っており、内部文字列の文字境界を知っています。length 関数に内部文字列を渡すと、文字列に含まれる文字の数が返ります。一方でlength関数にUTF8バイナリを渡すと、それがバイナリとして何バイトで表 現されるかを返します。例えば「ECナビ」は4文字ですが、それをUTF8で表現すると8バイト必要になります。
内部文字列とUTF8 バイナリは比較できません。論理的には同じ文字列を表現しようとしている内部文字列とUTF8バイナリを eq で比較しても結果は偽です。
内 部文字列とUTF8バイナリを結合しようとするのもよくないアイディアです。違うものなのだから当然ですね。sjisなデータとeuc-jp なデータをナイーブに結合しようとするとどうなるでしょうか?普通は文字化けします。内部文字列とUTF8バイナリを結合しようとすると同じように文字化 けします。おそらくあなたが目にするPerlの文字化けの半分くらいはこれが原因です。
Encode::decode - 内部文字列を作り出す
ではその内部文字列というのはどうすれば得られるのでしょうか。
それには Encode::decodeを使います。
CやJava風の型記法でシグネチャを表現すると次のようになります。
String Encode::decode(Encoding enc, byte[] bin);
String Encode::decode_utf8(byte[] bin);
文字コードencでエンコーディングされたバイナリ配列binから、内部文字列を作り出すのがdecode()の役割です。あらかじめエンコー ディングがUTF8であると決まりきっている場合もあるため、UTF8バイナリから内部文字列を作り出す decode_utf8 も用意されています。
use strict;
use warnings;
use Encode qw(decode_utf8);
my $binText = "ECナビ";
my $strText = decode_utf8($binText);
warn length($binText);
warn length($strText);
このコードをUTF8で保存して実行すれば、すでに述べたlengthの挙動を確かめることができます。
Encode::encode - 内部文字列を特定のエンコーディングのバイナリ表現に変換する
内部文字列というのはPerl内部でのお話です。現実の要件では EUC-JPやshiftjis、UTF8など、特定のエンコーディングを使っ てテキストを書き出したり、DBに格納する必要があるでしょう。これにはEncode::encodeを使います。
byte[] Encode::encode(Encoding enc, String str);
byte[] Encode::encode_utf8(String str);
UTF8で書いたコードで、出力はEUC-JPで行う例を示します。
use strict;
use warnings;
use Encode qw(decode_utf8 encode);
my $binText = "ECナビ";
my $strText = decode_utf8($binText);
#出力はEUC-JPにしたい
print encode('euc-jp', $strText);
use utf8 とは何か。
use utf8とは何でしょうか?Perlのドキュメントには「スクリプトがUTF8で書かれていることをPerlに伝えるためだけに使え」とかスピリチュアル なことが書いてあります。
そういうものだとして、Perlにそれを伝えるとPerlはどう振舞うのでしょうか?
大きく 2点、おまけが1点あります。
use strict;
use Encode;
#こいつはUTF8バイナリ
my $binPrice = '1,234円';
{
use utf8;
# 文字列リテラルは内部文字列を作り出す
my $strPrice = '1,234円';
# 正規表現リテラルがユニコードを認識する
$strPrice =~ tr/0-9,/0-9,/;
print Encode::encode_utf8($strPrice);
}
おまけについては特に述べません。そういうのを好む人もいれば、忌み嫌う人もいるでしょうから。
出力ではエンコーディングを明 示しよう
内部文字列とUTF8バイナリは違うことを覚えたあなたは文字化けを見ることも減り、use utf8 を使えばいちいちdecodeしなくても内部文字列を得られてだいぶ幸せになりました。
ところがPerlがこんな文句を言う事があるのに 気づきました。この警告は何を意味しているのでしょうか?
'Wide character in print at hoge.pl line xx.'
これは、典型的には内部文字列をそのままprintに渡した場合に起きる警告です。
use utf8;
#'Wide character in print at hoge.pl line xx.'
print '代表取締役丹野修一';
内部文字列をPerlがどのように表現しているかについて、あなたが(賢明なことに)何も知らないのであれば、こんなことをしようとは思わない でしょう。だってuse utf8しているあなたのコードでは、いまや文字列リテラルは内部文字列であり、それが物理的にどういう具合に表現されているか知らないのだから、そのま まprintしたところでそれをコンソールとかDBとかブラウザとかの他の誰かが読むことができるなんてありえそうもないことです。
書 き出すというのは、誰かに読んでもらうことを期待しているのだから、あなたと読み手との間で合意が取れているその文字エンコーディングを Perlに教えてあげるべきです。
use utf8;
use Encode;
#UTF8でエンコーディングすることになっている
print Encode::encode_utf8('代表取締役丹野修一');
何も指定しなければ、readやprintや<STDIN>などはバイナリのまま読み書きしようとします。つまり、read や<$fh>は(内部文字列ではなく)バイナリ列を返すし、printには適切なエンコーディングで符号化されたバイナリ列を渡すべきです。 そうじゃないと、10年前に書かれていまや誰も中身を把握していないスクリプト(そいつはエクセルCSVに何かぞっとするようなことをして、データを EUC-JPに変換した上でDBに何かしてるみたいで、もちろんuse strictなんてないし、Jcode.pmがどうとか書いてあります)が、Perlを新しくした瞬間に動かなくなってしまいそうです。
チー ム全員がPerlの文字列について正しく理解しているのでない限り私はおすすめしませんが、「このファイルハンドルから読み取るデータは必 ずUTF8なので、読み取ったデータを自動的に内部文字列に変換してほしい」「このファイルにはEUC-JPで書き出すので、内部文字列をprintした ら自動的にEUC-JPに変換した上で書き出して欲しい」などとPerlに伝えることができます。
use strict;
use warnings;
my $path = shift;
#open時にエンコーディングを指定する
open(my $fh, '<:encoding(utf8)', $path) or die;
#内部文字列
my $strLine = readline($fh);
close($fh);
STDIN、STDOUTのようなすでに開いているファイルハンドルに同じことをするときは、binmode関数を使います。これもチーム全員 がPerlの文字列について正しく理解しているのでない限り混乱の元だと私は思いますので、サンプルコードは割愛します。
まとめ
ここまでくれば、モダンPerlの文字列に関する一般則、
「入口でdecode、自分のプログラムでは内部文字列を使い、出口で encode」
の意味も理解できたのではないでしょうか。decodeは文字コードの妥当性を検証した上で内部文字列を作りだしますの で、入り口で decodeしてやれば例えば冗長なUTF8符号化による微妙な脆弱性も防ぐことができます。
落穂ひろい
To use utf8 or not utf8.
あなたはuse utf8すべきでしょうか?
use utf8の隠れた副作用として、"あなたのチームのPerlの文字列についての理解度を試す"があると私は思います。ちゃんと分かってないと、たいてい嫌 なことがおきます。
チームのPerl錬度が低いなら、「基本的にはuse utf8せずにどうしても必要な部分だけuse utf8する」というのもひとつのポリシーではないかと思います。