まだあった1.9.3-p0化にともなうトラブル

Ruby 1.9.3に移行したところ不正なマルチバイトシーケンスであるという例外が発生するようになった。

というのはtypoのバグを放置していた自分のせいである。

最近どうだったのかは分からないが、以前のtypoはtrackbackなどの処理で長い文字列をマルチバイトシーケンスを無視して切り詰めていた。このため不正なマルチバイトシーケンスが生じることがあり、それらの文字列はそのままDBに入れられていた。こうした状況に気付かないままRuby 1.9系に移行してしまったため、特定のページを表示させようとすると不正なシーケンスであるという例外が発生する結果となる。

具体的には"あいうえ\xE3..."といったもので、エンコーディングはUTF-8。不正部分を取り除いて保存し直してやればよいのだが意外と手間取った。

Rubyの多言語機能を使えば、不正なマルチバイトシーケンスを無視してエンコーディング変換させることができる。これにより正しいシーケンスのみを残した文字列を生成できるのではないかと考えた。だが、これは単純には実行できなかった。次のようになる。

str = "あいうえ\xE3..."
str.valid_encoding? #=> false
str.encode('UTF-8', 'UTF-8', :invalid => :replace, :replace => '') #=> "あいうえ\xE3..."

変換前のエンコーディングがUTF-8なのだが、UTF-8→UTF-8では変換が行われないということだ。次のように一度他のエンコーディングに変換すると期待通りとなる。

str.encode('EUC-JP', :invalid => :replace, :replace => '').encode('UTF-8') #=> "あいうえ..."

今回のケースでは、日本語であることが分かっており、まずまずUTF-8→EUC-JP→UTF-8でも問題なかろうという予想もついていた。件数も少ないし、これでもよいといえばよいのだが——やっぱりちょっと手を抜きすぎているように思える。

そこで、同じやり方をIconvで行うとどうなるかを試してみた。

require 'iconv'
Iconv.iconv('UTF-8//IGNORE', 'UTF-8', str).first #=> "あいうえ..."

こちらは期待通りの結果が得られた。Iconvについては「iconv will be deprecated in the future, use String#encode instead」とサポート終了が予告されているのだが、このようなケースではまだ使いでがある。(あまり一般的なケースではないだろうが。)

次に正規表現を使うことを考えた。鬼車のドキュメントによれば[[:print:]]により正しい文字にマッチさせられるはずだ。

str.sub(/\A([\s[:print:]]+).*/, '\1...') #=> ArgumentError: invalid byte sequence in UTF-8

残念。入力に不正なシーケンスが含まれていると扱えないようだ。だが、これに関していろいろ試しているうちに次の動作に気付いた。

str.encode('UTF-8','UTF-8').sub(/\A([\s[:print:]]+).*/, '\1...') #=> "あいうえ..."

変換元、変換先に同じエンコーディングを指定してencodeすることでvalid_encoding? #=> trueとなる。(少なくとも今回のケースでは。)ちなみに変換元を指定しないと「ArgumentError: invalid byte sequence in UTF-8」が発生する。

とまあ、そんなような経緯の後、Iconvを使ってエラー状態から復帰することにした。

追記(一夜明けて)

EUC-JPではなくてUTF-16あたりを介せばよかっただけではないかと今になって気付いた。

"あいうえ\xE3...".encode("UTF-16", :invalid => :replace, :replace => '').encode("UTF-8") #=> "あいうえ..."

それにしてももう少し簡単にできないだろうか。