ãだらけの文字化けはなぜ起こるか

この記事がはてなダイアリー上で化けないかどうか不安だが。

>>> u"こんにちは世界"
u'\u3053\u3093\u306b\u3061\u306f\u4e16\u754c'
>>> u"こんにちは世界".encode("utf-8")
'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf\xe4\xb8\x96\xe7\x95\x8c'
>>> [unichr(ord(c)) for c in u"こんにちは世界".encode("utf-8")]
[u'\xe3', u'\x81', u'\x93', u'\xe3', ... , u'\x8c']
>>> "".join(_)
u'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf\xe4\xb8\x96\xe7\x95\x8c'
>>> print _
こんにちã

あ、やっぱダメだったか。じゃあ画像で。

つまり、日本語をUTF-8エンコードした際に1バイト目に来ることが多い\xe3周辺のコードが、ユニコード(UCS-2)ではアクセント記号のついたアルファベットに使われているから、UTF-8エンコードしたバイト列をユニコード文字列だと誤解して使用したときや、入力されるバイト列がlatin-1でエンコードされていることを期待しているライブラリにUTF-8エンコードしたバイト列を入れた場合などによく見られる。

>>> print u"こんにちは世界".encode("utf-8").decode("latin-1")
(上と同じ文字化け)


詳しい話を聞いてみると、urllib.quoteされた文字列('%E3%83%9B%E3%82%B2'とか)をGAEのStringPropertyに入れて、それから出してurllib.unquoteしてjsonで出力しようとしたら化けた、ということらしい。

'%E3%83%9B%E3%82%B2'はアスキーの範囲に収まっているんだが、GAEはそんなこと知らないからこれを適当なエンコーディングを仮定してUnicode文字列に変換してしまう。なので取り出したときにu'%E3%83%9B%E3%82%B2'になっている。次にこれをurllib.unquoteすると、unquoteが%E3などのアスキー文字列からバイト列を作り出す関数であるにも関わらず入力がUnicode文字列なのでUnicode文字列にして返してしまう。

>>> urllib.unquote(u'%E3%83%9B%E3%82%B2')
u'\xe3\x83\x9b\xe3\x82\xb2'

UTF-8エンコードされたバイト列が欲しかったのに、変なユニコード文字列ができてしまった。質問者はそこでstrを挟んでみたが、エラーメッセージが出たので混乱してしまった。

>>> urllib.unquote(str(u'%E3%83%9B%E3%82%B2'))
'\xe3\x83\x9b\xe3\x82\xb2'

よく読んでみると、エラーが出ていたのはこの部分ではなくjsonで出力する部分であり、エラーの内容は「UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 1」であった。このエラーは「バイト列をユニコード文字列に変換したかったんでasciiコーデックを仮定して変換しようとしたけどダメだった」って言っているわけで、要するに我々は'\xe3\x83\x9b\xe3\x82\xb2'がUTF-8エンコードされていることを知っているけど処理系は知らないので教えてやる必要があるってことだ。

>>> '\xe3\x83\x9b\xe3\x82\xb2'.decode("utf-8")
u'\u30db\u30b2'

これで万事解決。

何らかの問題に遭遇して、手当たりしだいに思いついたことを試しても解決しない状態ってのは、たいがい2つ以上の問題が重なっていて1つ解決してもゴールに近づいたことが分からないから発生するんだ。そういう場合は全体が成功したか失敗したかじゃなくて、問題を細かく分割してどこまでは成功しているのかを見るべきなんだ。