Rubyでinvalidなバイト列を含むUTF-8文字列を扱う

Ruby 1.9系ではStringオブジェクトにエンコーディング情報が付加されていて、マルチバイトや複数エンコーディングを扱う日本人としては嬉しい限りだけど、時々エンコーディング関連で例外が発生して落ちすぎるぐらい落ちるように感じるときがある。ちょっとイラッと来るけど、考えてみたら、例外を出さずに処理を進めてしまうことの弊害のほうが大きいだろうから、これはありがたいこと。

外部のHTMLを読み込んで正規表現でマッチするコードで、次のようなエラーが出た。

invalid byte sequence in UTF-8

これは文字通り、UTF-8的におかしなシーケンスがあるということ。問題のHTMLを調べてみたら、UTF-8のHTMLの中に、Shift_JISが混じってしまっていた。どうも、JavaScriptで突っ込んだ文字列らしい……。いかにもありそうなことだ。どの程度の頻度で世の中にそういうWebページがあるのかというのは興味のある問題だけど、今は解決策がほしいので、それを調べた。

「String#encode」には、未定義文字や不正な文字を別の文字に置き換えるオプションがある。これを使えばinvalidなUTF-8のinvalidな部分だけを「?」に置き換えるようなことができる。

> RUBY_VERSION
 => "1.9.2" 
> str = "abc#{0xff.chr}efg"
 => "abc\xFFefg"
> str.encoding
 => #<Encoding:ASCII-8BIT>
> str2 = str.encode("UTF-8")
Encoding::UndefinedConversionError: "\xFF" from ASCII-8BIT to UTF-8
        from (irb):8:in `encode'
        from (irb):8
        from /Users/yarb/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `<main>'
> str2 = str.force_encoding("UTF-8")
 => "abc\xFFefg" 
> str2.encoding
 => #<Encoding:UTF-8> 
> str2 =~ /e/
ArgumentError: invalid byte sequence in UTF-8
        from (irb):11
        from /Users/yarb/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `<main>'
> str3 = str2.encode("UTF-16BE", :invalid => :replace, :undef => :replace, :replace => '?').encode("UTF-8")
 => "abc?efg" 
> str3 =~ /e/
 => 4
> 

すでにUTF-8エンコーディング情報がついた文字列について、str.encode("UTF-8")としても何も処理をしないので、いったん別のエンコーディング(UTF-16BE)に変換している。