Ruby での、文字コード変換ができない文字の置換について

Ruby 1.9 から文字列にエンコード情報が付与されるようになっており、 String#encode などでエンコード変換を行うことができます。

その際に変換できないバイト列があると、 Encoding::InvalidByteSequenceErrorEncoding::UndefinedConversionError が発生しますが、 String#encode の際にオプションを指定することでその箇所の置換を行い、例外を発生させずに処理を進めることができます。

#!/usr/local/bin/ruby
# vim:set fileencoding=UTF-8:
# †

s = "\u301C" # 波ダッシュ
s.encoding   #=> Encoding::UTF_8

begin
    s.encode(Encoding::CP932)
rescue => e
    "Error: #{e.class.to_s}: #{e.message}"
end
#=> Error: Encoding::UndefinedConversionError: U+301C from UTF-8 to Windows-31J

begin
    s.encode(Encoding::CP932, {
            :invalid => :replace,
            :undef   => :replace,
            :replace => ""})
rescue => e
    "Error: #{e.class.to_s}: #{e.message}"
end
#=> 〓

ですが、この方法では変換不能な箇所に一律置換するだけで、内容に応じて置換文字を変化させることはできません。たとえば、波ダッシュ "〜" のときは全角チルダ "~" に置換するが、それ以外で変換できないときは諦めて "〓" にするなど。



このような変換不能なバイト列に応じて置換するには、 Encoding::Converter#primitive_converter を使うと可能なようです。

このメソッドは、対象文字列のエンコードを変換できるところまで変換します。返り値で、最後まで問題なく変換できたか、変換不能な文字があったかなど判別することができます。また Encoding::Converter#primitive_errinfo で直近の変換結果の情報を参照することができ、変換エラーの原因となったバイト列の情報も含まれているため、それを用いて条件分岐を行うことができます。置換する場合は Encoding::Converter#insert_output を用いると置換することができます。

エラーがあったところまでしか変換されていないので、 redo を使って、再度、以降の変換を行います。



使い方は instance method Encoding::Converter#primitive_convert (Ruby 2.1.0) にも記載されていますが、ここで、サンプルとして UTF-8 から cp932 に変換を行い、波ダッシュで変換エラーが生じるときは全角チルダで置換してみます。

#!/usr/local/bin/ruby
# vim:set fileencoding=UTF-8:
# †

# UTF-8 の波ダッシュのバイト列(バイト列同士の比較のため ASCII-8bit 扱いで)
WAVE_DASH = "\u301C".force_encoding(Encoding::ASCII_8BIT)

# cp932 の全角チルダのバイト列
FULLWIDTH_TILDA = "\x81\x60".force_encoding(Encoding::CP932)


def utf8_to_cp932(src)
    ec = Encoding::Converter.new(Encoding::UTF_8, Encoding::CP932)
    dst = ""

    begin
        ret = ec.primitive_convert(src, dst)

        # 返り値で条件分岐を行い、エラーごとの処理を行う
        case ret
        when :invalid_byte_sequence
            ec.insert_output("")
            redo
        when :undefined_conversion
            # 変換エラーの原因が波ダッシュならば(比較は ASCII-8bit で)
            if ec.primitive_errinfo[3] == WAVE_DASH
                ec.insert_output(FULLWIDTH_TILDA)
            else
                ec.insert_output("")
            end
            redo
        end
        break
    end while nil

    dst
end

str = "東京~名古屋\u301C新大阪~広島\u301C博多"

str.encode(Encoding::CP932, {:undef => :replace, :replace => ""})
#=> 東京~名古屋〓新大阪~広島〓博多

utf8_to_cp932(str)
#=> 東京~名古屋~新大阪~広島~博多



pg_dump の EUC の文字化け - 見上げれば、空について試行錯誤してたときに知った方法。


波ダッシュ問題について

詳しくは調べていただくとして、簡単に。

通常 Windows で文字を入力するときに、波ダッシュと思って入力している "~" は厳密には全角チルダを意味する文字が使われています。一方で、波ダッシュと全角チルダは本来は別の文字なので、波ダッシュから全角チルダに変換されません(が、アプリケーション等によっては変換するようにしたものもあります)。今回は、 UTF-8 の波ダッシュを cp932 の波ダッシュとして変換しようとしたところ、未割当のエラーが発生した結果となります。