NKF.guess はもう古い。国際化時代のコードセット(エンコーディング)推測法

柄にもなく釣りっぽいタイトルですが、本文中には特にそんな派手なものはありません。

はじめに

Rubyの多言語化に関してまず読むべき、るびま記事である「Ruby M17N の設計と実装」が2009年の記事ですから四捨五入すればもう、ひと昔前のことですが、それでもなかなか移行は進まないもののようなので(あと少々クセがある)、エンコーディングの推測についてまとめた記事を書いてみます。

エンコーディングの変換

エンコーディングの変換については、例の記事の「エンコーディングの変換」の節( http://magazine.rubyist.net/?0025-Ruby19_m17n#l76 )と、詳細は最新のマニュアルの String#encoding についてのページ( https://docs.ruby-lang.org/ja/latest/method/String/i/encode.html )から追ってください。

NKF.guess はもう古い?

確かに NKF.guess は便利です(便利でした)。しかし K がおおもとは「漢字」に由来していることが暗示するように、現代の状況への対応には限界があります。

そもそもエンコーディングの推測ということ自体が、実はかなり無理なシロモノで、過去において NKF.guess の主な仕事だったと思われる EUC-JP と シフトJIS の判別についても、「あまり長くない EUC-JP 文字列と、半角カナばかりのシフトJIS文字列」というような(少々人為的ですが)判別が絶対に不可能な例があるほどです。

また現代ではとりあえず、最初に「UTF-8のように見えるならばUTF-8だとみなす」という判定を入れたい、などと思うことがありますが、そういう調整などを入れることも、オートマジカルな「NKF.guess 任せ」では難しいでしょう。

現代的なエンコーディング推測

((この1文だけ余談)専ら通用しているエンコーディングという表現を使っていますが、EUC-JP などは正確には EUCエンコーディングで、EUC-JP などを指す場合は厳密には「コードセット」となります)

現代的なやりかたは、ファイルの内容を最初から最後まで全部確認することでしょう。

全て読み込むとスラッシングが起きる巨大ファイルであるとかいう場合には、何か別の方法を使う必要がありますが、そうでなければ、ファイルをまず全部 String として読み込みます。

その際、エンコーディングを指定しないことが明示的なメソッドである、IO.binread メソッドを使うのが良いでしょう。

そして、その String オブジェクト s のオクテット列が、目的のエンコーディング e による文字列として valid かどうかは次のように、String#valid_encoding? メソッドでチェックできます。文字列に「付いているエンコーディングの情報」だけを強制的に設定する force_encoding メソッドを使うのがミソです。

s = IO.binread 'file.txt'
e = Encoding::UTF_8  # 例として UTF-8 についてチェック

s.force_encoding e
if s.valid_encoding? then
  print "UTF-8\n"
else
  print "not UTF-8\n"
end

後はこれを、Stringオブジェクトとエンコーディングを渡すと判定してくれるメソッドとして分離して(force_encodingによる副作用の存在に注意)、上位層で例えば、

  1. まず UTF-8 のようであれば、UTF-8 だとみなす
  2. そうでなければ、シフトJISEUC-JP を試す
  3. シフトJISEUC-JP のどちらでもなければ、とにかく UTF-8 だとする
  4. シフトJISEUC-JP の、どちらか片方で valid なら、それを選ぶ
  5. シフトJISEUC-JP の、両方で valid なら、特製の判定関数を使う

とでもいったような手続きとして実装しておけば、仮に将来、なにか誤判定で困るようなパターンがあっても、柔軟に対応できるでしょう。