今日はPerlを使って、Amazonの読者レビューの文字化けを解読してみよう。
(※注意、このブログ例によって終盤で急に腰砕けになります!)
コンピューターである文字列を表示しようとして、別の文字列が表示されて意味がわからなくなってしまうことを文字化けと言う。
文字化けはネットワークのエラーなどで偶発的、部分的に化ける場合と、ファイルの文字コード系を間違えてまるごと化ける場合がある。
今回は後者について研究する。

コンピューターの中で文字は数字に変換されて格納されている。
コンピューターは数値しか処理できず、あらゆる情報は数値にまず変換され、その数値が処理される。
このように情報を数値に変換したものをコード(code、符号)と言う。
文字を数値に変換したものが文字コードだ。
文字を文字コードに変換する方式(対応関係)のことを文字コード系という。
これにはいくつか種類があり、日本では長年Shift_JISというのが主流だったが今後はUnicodeの一種であるUTF-8が主流になると思われる。
ある文字コード系で変換したファイル(コードの集まり)を別の文字コード系で読み込むと文字化けが起きる。
面白いことに、この手の文字化けは長年IT業界にいるとなんとなく何コードを何コードで読んだか見分けがついていく。
いま、「こんにちは〜」という文字列があったとする。
これをUTF-8で保存する。

20150814_1

このファイルを、無理やりShift_JISとして開く。
現在使っているサクラエディタの場合、ファイル=>開き直す=>SJISとして開くを選択する。

20150814_2

化ける。

20150814_3

これをプロはパッと見ただけで「ああUTF-8のファイルをShift_JISで開こうとしているなあ〜」と分かる。

「こんにちは」の「こ」という字はUTF-8で0xE38193、「ん」は0xE38293になる。これは、このファイルをダンプ(16進数表示)すると分かる。
ここではxdump.exeというソフトを使ってみる。

20150814_4

この1字目の「こ」の最初の2バイト、0xE381はShift_JISで「縺」という字になる。
これは「もつれる」という言葉の最初の字だ。
次は「縺れる〜」というファイルをShift_JISで保存し、ダンプしたものだ。

20150814_5

「縺」という字が0xE381であることが分かる。
お分かりだろうか。
UTF-8で「こ」という字をエンコード(encode、情報を数値に変換すること、符号化)すると0xE38193になる。
Shift_JISで「縺」という字をエンコードすると0xE381になる。
UTF-8で保存した日本語のファイルを誤ってShift_JISとして解釈すると縺がやたら多くなる。
ほかに繧繝(うんげん)という言葉の繧と繝も多くなる。
イトヘンの難しい漢字が出てくると「ああ、UTF-8をShift_JISで読んでる〜」と分かるようになってくる。

他に、Shift_JISをISO-2022-JPで読むとやたら$が多くなる、EUC-JPで読むとやたら半角カナが出てくるなどのパターンがある。
もっとも、こんなの、どうしようもなく覚えてしまうものであり、改めて勉強する必要はない。

さて、このパターンはなんだろうか。

20150814_6

やたらã(aにチルダが乗った字)が出てくる。
これは0xE3であり、ISO-8859-1(いわゆるLatin1)で解釈している。

これは、PerlでUTF-8の文字列をエンコードしないでそのまま表示したときに出てくる。

以下のPerlプログラムをUTF-8で保存する。
#! /usr/bin/perl
#
# latinBake.pl -- ラテン文字に文字化け

binmode STDOUT, ":encoding(UTF-8)";

print "こんにちは〜\n";
実行してみる。
[perl]$ ./latinBake.pl
ã\201\223ã\202\223ã\201«ã\201¡ã\201¯ã\200\234
ãや¡(スペイン語で使われる逆感嘆符)が出てくるから、やはりLatin1で表示されているようだ。

ファイルコードを見てみる。
[perl]$ latinBake.pl | hexdump
0000000 c3 a3 c2 81 c2 93 c3 a3 c2 82 c2 93 c3 a3 c2 81
0000010 c2 ab c3 a3 c2 81 c2 a1 c3 a3 c2 81 c2 af c3 a3
0000020 c2 80 c2 9c 0a
0000025
[perl]$
UTF-8の0xc3a3というのは、UnicodeのU+00E3で、ãのことだ。

Unicode Character 'LATIN SMALL LETTER A WITH TILDE' (U+00E3)

これをやめるには、utf8プラグマモジュールというのを使う。
#! /usr/bin/perl
#
# latinBake.pl -- ラテン文字に文字化け(しない)

binmode STDOUT, ":encoding(UTF-8)";

use utf8;

print "こんにちは〜\n";
実行してみる。
[perl]$ latinBake.pl
こんにちは〜
あっさり直った。

use utf8;を付けると、このプログラムがUTF-8であると宣言され、二重引用符""で囲まれた文字列が、UTF-8内部文字列というものにアップグレードされる。
俗にいう、utf8フラグというものが立つ。
これによって「こんにちは〜」が6文字のUTF-8文字列であると解釈され、すべて日本語の仮名文字であるから24ビット(3オクテット)ずつが1文字であると解釈される。
ではなぜ「binmode STDOUT, "encoding(UTF-8)"」というものが必要なのだろうか。
UTF-8であると分かってはいるのだから、標準出力のエンコードをわざわざ教えなくてもいいのでは?
では取り除いて(注釈化して)みよう。
#! /usr/bin/perl
#
# latinBake.pl -- ラテン文字に文字化け(しないが不具合が・・・)

#binmode STDOUT, ":encoding(UTF-8)";

use utf8;

print "こんにちは〜\n";
実行。
[perl]$ latinBake.pl
Wide character in print at ./latinBake.pl line 9.
こんにちは〜
警告が表示される。
「printでWide文字が表示された」というものだ。
これは、UTF-8内部文字列をエンコードしないで、そのまま外界に放流してるけどいいんですか、というものだ。

ここでいうエンコードとは、PerlのEncodeモジュールのencode関数が行う動作で、UTF-8内部文字列を、外界に出すファイル用の文字コードに変換することだ。
binmodeをファイルハンドル(ここではSTDOUT=標準出力)に掛けると、指定された文字コード系(ここではUTF-8)に変換される。
もともとUTF-8だった文字コードをUTF-8にencodeすると何が起こるかというと、UTF-8内部文字列がそうではなくなる(utf8フラグが落ちる)。

警告ぐらいさせておけばいいじゃん、という気もするが、ここはやはりbinmodeを書きたい。

ところで、最初の
#! /usr/bin/perl
#
# latinBake.pl -- ラテン文字に文字化け

binmode STDOUT, ":encoding(UTF-8)";

print "こんにちは〜\n";
というバージョンだが、use utf8;をつけず、binmodeも取ってしまえば、これまた正常に動作する。

#! /usr/bin/perl
#
# latinBake.pl -- ラテン文字に文字化け(しない)

#binmode STDOUT, ":encoding(UTF-8)";

print "こんにちは〜\n";
実行する。
[perl]$ latinBake.pl
こんにちは〜
これはどういうことか。
use utf8;を取ってしまうと、二重引用符の間の字がUTF-8内部文字列にならない。
ではどうなるかというと、昔ながらの8ビット1文字の、Latin1の文字として処理される。
で、それをそのままencodeせずに外界に放流すると、Latin1の文字列のつもりでãから始まる文字コードがそのまま放流される。
ところが、今使っているMacのターミナルは、文字コードをUTF-8で解釈する設定になっているので、そのまま「こんにちは〜」と表示されるのだ。

つまり、UTF-8で保存されたPerlのプログラムの中に"こんにちは〜"と日本語文字列を二重引用符で囲んで書いて、それを文字化けせずに表示する方法は:

(1)use utf8;も、binmode STDOUT, "encoding(UTF-8)"も書かない
 =>二重引用符の中の文字列は1バイト1文字のLatin1で処理され、そのまま表示される。
   ターミナルがUTF-8設定であれば、そのままこんにちは〜と表示される

(2)use utf8;も、binmode STDOUT, "encoding(UTF-8)"も書く
 =>二重引用符の中の文字列は1文字1文字のUTF-8で処理される(UTF-8内部文字列になる)。
   出力されるときはUTF-8内部文字列でなくなる(utf8フラグが落ちる)が、UTF-8でエンコードされて出力される。
   ターミナルがUTF-8設定であれば、そのままこんにちは〜と表示される

(1)の方が面倒がなくて良さそうだが、substr関数やlength関数で長さに1を指定すると1文字ではなくて1バイト(8ビット)になってしまうので、Perlの強力な文字列処理を活かすには、(2)の方が良さそうに思える。

さて、最初の、Latin1がターミナルに表示されるプログラムに戻る。
#! /usr/bin/perl
#
# latinBake.pl -- ラテン文字に文字化け

binmode STDOUT, ":encoding(UTF-8)";

print "こんにちは〜\n";
実行結果はこうであった。
[perl]$ latinBake.pl
ã\201\223ã\202\223ã\201«ã\201¡ã\201¯ã\200\234
この、「use utf8;」がないプログラムから表示される、Latin1になってしまったUTF-8のファイルを、解読する方法はあるのだろうか。
これは、一度UTF-8として読み込んで、Latin1として書き出してやればいいような気がする。
#! /usr/bin/perl
#
# u8_to_latin.pl -- UTF-8のファイルをLatin1に変換

binmode STDIN, ":encoding(UTF-8)";
binmode STDOUT, ":encoding(ISO-8859-1)";

use utf8;

print while <STDIN>;
実行してみる。
[perl]$ latinBake.pl | u8_to_latin.pl
こんにちは〜
あはは、見事にきれいに出力される。
|はパイプで、前のプログラムの標準出力を標準入力に得て実行するというUNIXの機能である。

これはどういうことだろうか。

UTF-8で「こ」という漢字の1バイト目は0xE3で、ISO-8859-1(Larin1)ではãになる。
UTF-8で「ã」は0xc3a3の2バイトになる。
だから、latinBake.plが出力した「こ」の1バイト目は、ターミナルではãに見え、hexdumpすると0xc3a3になった。

u8_to_latin.plは、UTF-8のファイルを標準入力から入力してUTF-8内部文字列に変換し、それをまたISO-8859-1にエンコードして標準出力に出力する。
よって、「ã」のUTF-8コードである0xc3a3を0xE3に変換する。
これは「こ」の最初の8ビットである。
これ以降も同じ処理が行われるから、Latin1に化けたUTF-8が正しいUTF-8に解読されるのだ。

さて、この「本来UTF-8なのにLatin1で解釈し、それをUTF-8に変換した、やたらãが出てくるタイプの文字化け」だが、身近に見ることが出来る。
Amazonのレビューである。

57

上記は、アイラ・レヴィン著「ローズマリーの息子」というひどい小説に寄せられたレヴューだが、見事に化けていて、レヴューワーの怒りがいまいち伝わってこない。
実は、拙著「文字コード【超】研究」にも同じタイプの文字化けのレヴューが寄せられていて、それを教材にすると面白いと思ったのだが、その文字化けレヴューはなんとなく直ってしまっていた。
さて、上のレヴューを解読してみよう。

まず、ブラウザーからエディターへコピペする。
04
文字の量が増えている。
Latin1のコード範囲ではない不正な文字が、ブラウザーでは隠されていたのが、Emacsの機能で\201のように8進数表示されているのだ。

では早速u8_to_latin.plを掛けてみる。

07
あー、ダメだ。
まず\???というのは、もとのソースにある字でLatin1の範囲にない字がそのまま8進数で表示されている。
8進数で表示されるのはEmacsのサービス機能で、実際にはそのバイナリーがそのまま入っている。

次に
"\x{201e}" does not map to iso-8859-1 at ./u8_to_latin.pl line 10, <STDIN> line 1.
のような警告と、文中に埋め込まれた\x{201e}のような字は、そのようなUnicodeはISO-8859-1の範囲にない、と言われている。

これ、よくよく見てみると、二重引用符"、一重引用符'、ハイフン-のような字が入っている。
たぶんこれらは、方向のある引用符や長いハイフン、短いハイフンのような字(のLatin1に当たるバイナリー)を、すべてASCIIの直立した引用符とハイフンマイナスに正規化(?)してしまったから、UTF-8のうちの1オクテットだけが変わってしまったために起きた文字化けだ。

これは機械的には戻せない。もともと”、"、“、あるいは„のような引用符のいずれが"に変わったのかはわからないからだ。
ひとつひとつプチプチと推測していけばいいのかもしれないが、そこまでぼくもヒマじゃないので今日のところはこれで断念する。
まあ、あらすじを書いてあるレビューであったということはなんとなく分かった。