BLOGTIMES
2012/06/03

ISO-2022-JP と CP50220 と Encoding::UndefinedConversionError

  ruby  charset 
このエントリーをはてなブックマークに追加

Ruby は 1.9 から文字エンコード周りが変更されているので、先月も Encoding::CompatibilityError ではまってしまいましたが、今回は Encoding::UndefinedConversionError というエラーにどっぷりはまってしまいました。メールスプールのファイルを読み込むプログラムが書きたかったのですが、メールが厄介なのはファイルを開いてヘッダを調べてみないと文字コードが判別できないということです。バイト列から文字コードを自動的に検出すればいいのかもしれませんが、それはそれで結構手間がかかる*1ようです。

具体的に躓いたのは下記のような、所謂機種依存文字を含むようなメールでした。

test.eml

$ cat test.eml | nkf -w Message-ID: <4FCAFC7A.7020105@example.jp> Date: Sun, 03 Jun 2012 14:56:10 +0900 MIME-Version: 1.0 Subject: test Content-Type: text/plain; charset=ISO-2022-JP Content-Transfer-Encoding: 7bit 11壱①Ⅰ

ISO-2022-JP じゃなくて CP50220 を使おう

結論だけ最初に述べると、メールのヘッダで ISO-2022-JP と指定されている場合には、Ruby ではエンコーディングとして CP50220 を使う必要があるということです。
これはちょうど Shift_JISとCP932 の違いの話の ISO-2022-JP 版ですね。

以下、作業メモ

ASCII-8BIT の罠

Ruby ではバイナリモードでファイルを読み込むと Encoding::ASCII_8BIT になります。
エンコードが Encoding::ASCII_8BIT の状態だと encode! に何を渡してもエラーを吐かず、変換もされないようです。
具体的には、下記のコードを実行すると、gsub! がちゃんと機能せず、表示すると内容が JIS のままということが分かります。

hoge1.rb

#!/bin/env ruby # -*- coding: utf-8 -*- str = '' open('test.eml', 'rb'){ |f| str = f.read } str.encode!(Encoding::UTF_8) p str.encoding str.gsub!(/1/,'2') puts str
$ ./hoge1.rb #<Encoding:UTF-8> Message-ID: <4FCAFC7A.7020105@example.jp> Date: Sun, 03 Jun 2012 14:56:10 +0900 MIME-Version: 1.0 Subject: test Content-Type: text/plain; charset=ISO-2022-JP Content-Transfer-Encoding: 7bit 1#10m-!-5

※最後の行は文字化けしています。

force_encoding を使って文字コードを設定してみる

String の中身を変更せずに、エンコーディングの認識だけを強制的に変更するには、 String#force_encoding*2 を使えばよいので、プログラムを少し書き換えて、メールのヘッダ部分から charset=ISO-2022-JP のような部分を探して、その部分のエンコードを設定してやることにします。(本当はもうちょっとまともなコードを書きましたが、ここでは大幅に簡略化しています。)

hoge2.rb

#!/bin/env ruby # -*- coding: utf-8 -*- str = '' open('test.eml', 'rb'){ |f| str = f.read } encoding = nil str.match(/charset=(.+)/){ |m| encoding = m[1].strip } str.force_encoding(encoding) if encoding p str.encoding str.encode!(Encoding::UTF_8) p str.encoding str.gsub!(/1/,'2') puts str

これで一件落着かと思っていたのですが、ところが今度は Encoding::UndefinedConversionError が出て動きません。
エラー中の "\xAD\xA1" という部分は ① にあたる部分のようです。

$ ./hoge2.rb #<Encoding:ISO-2022-JP (dummy)> ./hoge2.rb:12:in `encode!': "\xAD\xA1" to UTF-8 in conversion from ISO-2022-JP to stateless-ISO-2022-JP to EUC-JP to UTF-8 (Encoding::UndefinedConversionError) from ./hoge2.rb:12:in `<main>'

CP50220 をつかうことに

上記のエラー自体は①がUTF-8に変換出来ないというのは分かるのですが、わざわざヘッダで指定されている文字コード(ISO-20220-JP)を指定すると変換エラーでコケるという動作がイマイチ腑に落ちません。そのまましばらく悶々としていたのですが、冷静になってよく考えてみると、昔、Shift_JISとCP932 の違いの話とか、EUC-JP と EUC-JP-ms の違いでハマったことを思い出したので、 ISO-2022-JP のMS仕様があるのではと思って調べてみたところこれがビンゴでした。

cp50220 - Legacy Encoding Project

Windows Codepage 50220
Windows での ISO-2022-JP の一種
Outlook Express, Internet Explorer で使われている
Unicode から cp50220 への変換時に、JIS X 0201 片仮名は JIS X 0208 の片仮名に置換される
想定する使用用途
  Outlook Expressが送ってきたメールを①(まる1)などの、拡張文字も含めて正しく処理したいとき

ということで、 Encoding を調べてみると Encoding::CP50220*3 というのがあったので、これを使ってみます。

hoge3.rb

#!/bin/env ruby # -*- coding: utf-8 -*- str = '' open('test.eml', 'rb'){ |f| str = f.read } str.force_encoding(Encoding::CP50220) p str.encoding str.encode!(Encoding::UTF_8) p str.encoding str.gsub!(/1/,'2') puts str
$ ./hoge3.rb #<Encoding:CP50220 (dummy)> #<Encoding:UTF-8> Message-ID: <4FCAFC7A.7020105@example.jp> Date: Sun, 03 Jun 2012 14:56:10 +0900 MIME-Version: 1.0 Subject: test Content-Type: text/plain; charset=ISO-2022-JP Content-Transfer-Encoding: 7bit 12壱①Ⅰ

これでやっと望む動作になりました。
メールのヘッダに ISO-2022-JP と書いてあったり、SJIS と書いてあるときは要注意なんですね。

苦労しましたが、今回の件でRuby 1.9 のエンコード周りはかなり理解できました。


トラックバックについて
Trackback URL:
お気軽にどうぞ。トラックバック前にポリシーをお読みください。[policy]
このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/4990
Trackbacks
このエントリにトラックバックはありません
Comments
愛のあるツッコミをお気軽にどうぞ。[policy]
古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
コメントはありません
Comments Form

コメントは承認後の表示となります。
OpenIDでログインすると、即時に公開されます。

OpenID を使ってログインすることができます。

Identity URL: Yahoo! JAPAN IDでログイン