この2日間*1ほど、EUC-JPなデータおよびスクリプトをUTF-8なものに変更する作業などしていたわけで。
EUC-JPと言っても、実際には丸付き数字とかそういうのが山ほど入っている、綺麗とはいえない内容で。これをつつがなくUTF-8に変更するのに一苦労。
過去にこの辺を書いたおかげで、それに気が付くのに時間はかからなかったけど、それでもJcode.pmとEncode.pmの挙動の違い(というか、使い勝手の違い)には泣かされた。
おまけに、元のスクリプト、use strictもやってなかったりする、いい加減古いものだったりしたし。
ということで、はまった内容をメモっておくことにする。
先に結論だけ書いておくと
- 機種依存文字が入っているときは、EUCJPMSとCP932が必須
- 入力はすべてdecodeして、処理したら全てencodeして出力。内部コードは絶対に内部にとどめる。
- 少なくとも1つの入力は1つの文字コードで統一。それが何かは事前に調べておく。
- でも、波ダッシュ問題はかんべんな。
ということで。
(1)Jcodeでeuc→sjisするのと、Encodeでutf-8→sjisするのは違う。
gdbmで保存しているのを、先々RDBMSで管理したいなあ。ついてはこの機会に、EUCで保存されているものをUTF-8にしたいなあと思った。
euc→utf-8は簡単なはずだ。$utf8=jcode($euc)->utf8;でも、Encode::from_to($euc2utf8,'euc-jp','utf-8');でもいいはずで、あっという間に終わった。
ところが、ここからShift_JISに変換すると挙動が違う。
具体的には丸数字。まあ丸数字をShift_JISとか言われるともにょっとする人はいるんだろうけど、現実問題使われてるんだからしょうがない。
で、Jcodeのsjis->euc,euc->sjisだと、丸数字は適当に変換されてるんだな。
これがEncodeだと、丸数字が含まれているほうのcp932を使って、Encode::from_to($utf82sjis,'UTF-8','cp932');とかしてもうまく動かない。
よく見たら、euc→utf-8の時点で、丸数字が保存されていないのね。たしかにそう言うものかもしれない。なので、MS由来な文字が入っているEUCからUTF-8の変換には、Encode::EUCJPMSを使って、Encode::from_to($euc2utf8,'eucJP-ms','utf-8');してあげないとだめ。
(2)内部コードは内部コードであって、UTF-8ではない。
最初から入力がUTF-8しかない環境で、イチからUTF-8でコードを書いて、というところではuse utf8;しておけばほとんど問題がない、というのはその通りで、これまでもそれでやってきた。
なんで問題がなかったかというと、まあ出力の前にShift_JIS(cp932)にエンコードしてHTML::Templateつかって書き出してたから。
HTML::Templateとutf-8については、若干面倒な話がある。詳細はリンク先のほうにもあるとおり、
use strict; use Encode; use HTML::Template; use utf8; my $template = HTML::Template->new( filename => 'utf8.tmpl', filter => sub{ my $text_ref = shift; $$text_ref = decode( 'utf8', $$text_ref ); # 内部形式にデコード } ); : : print encode( 'utf8', $template->output );
こんな感じでやればいいわけだけど。*2
まあ内部形式というのが正直よくわかっていなかった、というのもある。いろいろ検索してもuse utf8;とか理解できるかというとそうとは限らなかったり。今回なんとなく思ったのは、ソースがutf-8で保存されている前提では、
use strict; use Encode; use utf8; my $string ="日本語文字列";
と、
use strict; use Encode; my $string = decode('utf-8',"日本語文字列");
は、同じものなのかな、ということだった。*3
内部コード、というのが実際にUTF-8で書かれているかどうか、というのは実際に使うときにはどうでもいいんだな、というか。
あとは、内部コードに変換するか、変換しないか、ということだ。
日本語の置換とか結合とか、そういう文字列操作が絡む場合は、内部コードに変換したほうがいい。
そうでない場合、たとえば上記HTML::TemplateでShift_JISなHTMLを出したい、というときは、
use strict; use Encode; use HTML::Template; use utf8; my $template = HTML::Template->new( filename => 'sjis.tmpl' ); $template->param('word'=>encode('sjis',"日本語文字列")); print $template;
とする手もあるわけだ。
まあでもこれも実際には内部コードを使っている*4ので混乱の元になるかもしれない。
(3)内部コードを使うことにすれば、Encode::from_toは要らなくなる。
個人的に日本語のコード変換には今まで、
use strict; use Jcode; my $utf8 = jcode($sjis)->utf8; print $utf8; print jcode($utf8)->sjis; #出力がShift_JISの場合
を多用してきた。これ、元の$sjisを破壊しないのが好きでね。その点で、Encode::from_toって元のものそのものが変化するのがどうしても好きになれなかった。
でも、変換先が内部コードだ、という前提であれば、
use strict; use Encode; my $internal = decode('Shift_JIS',$sjis); my $internal = decode('EUC-JP',$euc); print encode('utf-8',$internal); #出力がUTF-8の場合 print encode('cp932',$internal); #出力がWindows系Shift_JIS(cp932)の場合
という感じで、それほど実感が変わらずに使えるということだ。
(4)でも、自動判別はなんとかしないといけない。
残る問題は、文字コードの自動判別にある。Encode::Guessで自動判別が出来ることになっているのだけど、jcode.plとかJcode.pmに比べて安全側に振ってあるせいか、仕様が微妙におかしいのか、自動判別に失敗することがけっこうあった。
ファイルから読み込む場合でもそうだけど、ほとんどの場合、一つのストリームから来る文字コードは1つと決めうちしていいことがほとんどだ。ファイルだったら一度全て読み込んでからEncode::Guess使えばいいのだろうけど、CGI.pmでパラメータ毎に分解すると、どうしても文字数が短くなって自動判別に失敗する可能性が上がるのが面倒くさい。
ここは同様に、
use strict; use CGI; my $cgi= CGI->new(); my $guess = Encode::Guess::guess_encoding(join("",values($cgi->vars));
とかすればいいんだろうか。
こういう場面に会わないことを祈りたい。
(5)波ダッシュと全角チルダを判断することは(素人には)難しい。
で、ここまでやって"〜"が文字化けしたときには泣きそうになった。
しかも、全てUTF-8で保存して、CP932で出力して、でうまくいってるところで、戯れにShift_JISで出力したら文字化けしないという。じゃあ、Shift_JISでやるか、と思ったら今度は別の"〜"が文字化けして。
表題にもあるとおり、ある"〜"が文字化けして、別の"〜"が文字化けしないというのは、波ダッシュと全角チルダの扱いが怪しい。怪しいのは怪しいんだけど、これ見た目同じに見えるんだよね。
とうとう、Devel::Peekで内部コード覗いちゃったりしたよ。そうしたら、やっぱり波ダッシュと全角チルダが混在していて。
で、解決策ですが、丸数字を(Windows下の)Shift_JISで使う、という前提がある以上、CP932でデコードして問題がないようにしないといけない、ということで、しょうがないから全角チルダに統一です。
use strict; my $value = decode('UTF-8',$db_value); $value =~ s{\x{301c}}{\x{ff5e}}g;
すいませんが、この話は暫定。キーボード入力の"〜"が全ての場面で全角チルダになってるかどうかとか確認が取れていないので、これを今後もやっていく必要があるのかどうかは未確認、ということで。
というか、この最後の「全角チルダ」問題って、金曜日のPHPもくもく勉強会で解決に向けていろいろやってたんだよなあ。PHP1mmも関係ないっていうツッコミも多々あると思いますが、ここでいうPHPは「Perl Han-characters Processing」の略称ということでお願いします。