トップ «前の日記(2016-02-29) 最新 次の日記(2016-03-24)» 編集

2016-03-07 [長年日記]

_ Shift_JISの範囲外の文字を検出、見える化したい

まだちょっと悩んでるけど一応できたのでメモ。

若者の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")

    s = open(f, "rb:Windows-31J").read.gsub(fulltilde, tmptab)

    diff = s.encode("Shift_JIS", undef: :replace, replace: "??".colorize(:magenta)).encode("Windows-31J").lines - s.lines

    if diff.size > 0
      puts ""
      puts f

      puts diff.map {|e| e.gsub(tmptab, fulltilde).encode("UTF-8")}
    end
  end

Shift_JIS から UTF-8 に変換できない時だけ Windows-31J として読み込んで、その後 Sift_JIS に変換を試みることで Shift_JIS に収まっていない文字を置き換える作戦。これでこの結果の画面は

  • ダメな文字を含む行だけが表示され、かつ
  • ダメだった文字はハイライトされている

ので、詳しくない人にも結構分かりやすい表示になるんじゃないか。

また、そのままだと 〜 が Windows-31J から UTF-8 へ変換される過程で Shift_JIS では扱えない文字となってしまい、軒並み undef になってチェックの意味を成さなくなるので、これは強制的に垂直タブに置き換えて問題なく動作するようにしている。

cf. String#encodeが変換できそうで変換できない文字 - ネットの海の片隅で

Tags: Ruby

*1 コマンドラインから作業するハードルがあるのは認める

*2 途中で UTF-8 に変換する際に対応関係が壊れるのか?