まだちょっと悩んでるけど一応できたのでメモ。
若者のiconv離れ
とあるデータ加工作業があって、その中で Shift_JIS の範囲外の文字を置き換えていくという作業を行う必要があった。具体的には (株) とか「はしごだか」とかああいう文字ね。
で、これまでその辺はある程度お任せにしていて、必要になったら(実はここがミソ)自分が手で iconv をこんな風に
iconv -f sjis -t sjis filename > /dev/null
叩くことでダメな文字を指摘していた。iconv は nkf のようにいい具合に skip して処理しないので、Shift_JIS の範囲内で変換できない文字のところで以下のようにエラーを吐いて止まる。
iconv: filename:{line}:{col}: cannot convert
iconv なんてイマドキそんなに使わないのでは?と思いつつ古のワザをくり出していたのだけど、何せこの表示、見にくい。具体的には
普段テキストエディタで作業していない人には全然分かりやすくない
こんなことしなくても作業の練度が上がるとダメな文字が入ってくるパターンを学習して「だいたいこんな感じ」でいい具合に弾けるようになる。ところがこの学習の効いていない人に引き継ぐと、途端に難しくなってしまう。じゃあ iconv 使ってもらうかというとそれは厳しいなぁと思う次第。
Rubyで書けるのではと思ったら意外に難しかった
他にも加工を支援する必要があるのである程度のカタマリで rake task を用意していくことにした。[^1]そこで Ruby の Encoding でも不正な文字列が入っていると変換に失敗するので似たようなことはできるのではないかと思ってやり始めてみたら意外に奥が深かった。
結論から書くとこんな感じ。
- 変換エラーで止まる際はどこの文字かヒントがない(バイト列は表示される)ので、 iconv よりも分かりにくい
- Encoding#valid_encoding? は変換表に基づいて valid? の判断をしていないので、1文字ずつなめて valid でない文字の前もヒントととして表示してあげるといったことはできない
- encode(enc, undef: :replace, replace: '??') みたいにするとダメな文字だけを特定しやすい文字に置き換えることはできる
- ただし、全部同じ文字に置き換えられてしまうので、結局元のデータを修正する時は目視が必要
- しかも問答無用で Windows-31J → Shift_JIS 変換を行ってはならない
全部ハマった。最後のものはどういうことかというと、
open(file, 'rb:Windows-31J').encode('Shift_JIS', undef: :replace, replace: '??')
で目的が達成できるかと思ったら、〜 がすべて不正な文字扱いになってしまう。
正しい Shift_JIS として記録されているのに Windows-31J として読み込んでしまうと 〜 を Shift_JIS の範囲外の文字として扱ってしまう[^2]。これを encode('Shift_JIS') してしまうと不正な文字だらけになってしまって意味がない。
結局こうなった
begin | |
open(f, 'rb:Shift_JIS:UTF-8').read | |
rescue Encoding::UndefinedConversionError => e | |
fulltilde = "\x81\x60".force_encoding("Windows-31J") | |
tmptab = "\v".force_encoding("Windows-31J") | |
w = open(f, 'rb:Windows-31J').read.gsub(fulltilde, tmptab) | |
s = w.encode('Shift_JIS', undef: :replace, replace: '??'.colorize(:magenta)) | |
diff = s.encode('Windows-31J', undef: :replace, replace: '??'.colorize(:magenta)).lines - w.lines | |
if diff.size > 0 | |
puts "" | |
puts f | |
puts diff.map {|e| e.gsub(tmptab, fulltilde).encode('UTF-8')} | |
end | |
end |