UTF-8 文字列中に UTF-8 として正しくないコードが入っていた場合に、その文字を「?」などに置き換えたいことがあります。
たとえば MySQL に登録するときは不正な文字を消しとかないと、その文字以降すべて消えてしまいます。
mysql> insert into t (c) values (0x414243FF58595A); Query OK, 1 row affected, 1 warning (0.06 sec) Warning (Code 1366): Incorrect string value: '\xFFXYZ' for column 'c' at row 1 mysql> select * from t; +------+ | c | +------+ | ABC | +------+ 1 row in set (0.00 sec)
ということで、Ruby では Iconv を使ってこんな感じで対処してます。
require 'iconv' def sanitize_utf8(str) ret = '' i = Iconv.open('UTF-8', 'UTF-8') begin ret << i.iconv(str) rescue Iconv::Failure => e ret << e.success << '?' str = e.failed[1..-1] retry end ret << i.iconv(nil) ret end sanitize_utf8("ほ\xffげ") #=> "ほ?げ"
Iconv が UTF-8 として不正な文字をエラーにしているのを利用しています。
ですが、次のようなバイト列ではうまくいかないことがわかりました。
sanitize_utf8("ほ\xf8\x90\x90\x90\x90げ") #=> "ほ\xf8\x90\x90\x90\x90げ"
この5バイトは実は UTF-8 としては正しいバイト列なのです。ですが今は UTF-8 は 4バイトまでしか使われないことになってますし、当然 MySQL の utf8mb4 charset のカラムにも入りません。
Iconv はこの文字を UTF-8 として正しい文字として扱っているので UTF-8 から UTF-8 への変換では対処できません。4バイト以下の UTF-8 のみが有効な文字である UTF-16 を一旦経由することでうまくいきました。
require 'iconv' def sanitize_utf8(str) ret = [] i = Iconv.open('UTF-16BE', 'UTF-8') unknown_char = "\x00?" begin ret << i.iconv(str) rescue Iconv::Failure => e ret << e.success << unknown_char str = e.failed[1..-1] retry end ret << i.iconv(nil) Iconv.iconv('UTF-8', 'UTF-16BE', *ret).join end sanitize_utf8("ほ\xf8\x90\x90\x90\x90げ") #=> "ほ?????げ"
他にもっといい方法を知ってる人は教えて下さい。