Ruby での、文字コード変換ができない文字の置換について
Ruby 1.9 から文字列にエンコード情報が付与されるようになっており、 String#encode
などでエンコード変換を行うことができます。
その際に変換できないバイト列があると、 Encoding::InvalidByteSequenceError
や Encoding::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 の文字化け - 見上げれば、空について試行錯誤してたときに知った方法。