JavaでWindowsなEUC-JPの文字化けを防ぐ

文字コードについて - ooharakの日記 の続き。結論が出ていないが、考察過程。

Javaでサポートされているエンコーディングと、x-eucJP-msとの差

実際にJavaで変換を掛けた場合に、文字化けが起こるかどうかというのは、理屈の上では、区点やISO 2022などをベースに机上だけで考えることができるはずである。
だけども、どこかでボタンを掛け違ったままで考えを進めていくと、後で間違いに気づいてからの手戻りが大変そうなので、まず実際にJavaで動かして試してみることにした。

実装には、こういった試行錯誤系にはうってつけのJRubyを使う。
比較対照の規範としては、今回x-eucJP-msを使う。理由は、今回使用するデータベースPostgreSQLが、EUC-JPを指定した場合には、実際にはx-eucJP-msで動くようになっているとされるからである。
x-eucJP-msには、「コード変換規則」, Unicode とユーザ定義文字・ベンダ定義文字に関する問題点と解決策 添付資料, TOG/JVC CDE/Motif 技術検討 WG.の「Microsoft Windows 3.51 式の変換」の.txtファイルをダウンロードし、c:/home/data/charset以下に配置して用いた。

以下のJRubyプログラムを実行すると、x-eucJP-msと実際のJavaとで変換結果の違うものが表示される。

プログラム
require 'java'


DIR = "c:/home/data/charset"
#ENCODING = 'x-eucJP-Open'
ENCODING = 'EUC_JP'

def java_string_to_hex(s)
  return s.getBytes('UTF-16BE').to_a.pack('C*').unpack('H*')[0].tr('a-z','A-Z')
end

def array_to_java_string(a)
  return java.lang.String.new(a.to_java(:byte), ENCODING)
end


n = 0
Dir.foreach(DIR) {|f|
  next unless f =~ /txt$/
  File.foreach(DIR+'/'+f){|line|
    line.scan(/^0x([0-9A-F]+)\t0x([0-9A-F]+)\n?$/){|euc_hex,expected_hex|
      input_bytes = euc_hex.to_a.pack('H*').unpack('C*')
      output_hex = java_string_to_hex(array_to_java_string(input_bytes))
      if expected_hex != output_hex
        puts "#{euc_hex} --> expected: #{expected_hex} actual:#{output_hex}"
        n = n + 1
      end
    }
  }
}
puts "total #{n}"
結果
ENCODING 相違のあった行の数
EUC_JP 2,077
x-euc-jp-linux 8,143
EUC_JP_Solaris 1,888

EUC_JP_Solarisは別名x-eucJP-Openであり、成り立ちから言ってx-eucJP-msにもっとも近い結果になるのは自然である。

ではつぎに、EUC_JP_Solarisに絞り、その中でx-eucJP-msと差異が出ている部分について検討する。
このプログラムが出力する、A --> expected: B actual: C という形は、規範とした.txtファイルで述べられている変換規則はA(x-eucJP-ms)→B (UCS-2)であるが、EUC_JP_Solarisで変換したもの(C)がBとは違う値になったことをあらわしている。

A1BD --> expected: 2015 actual:2014
A1C1 --> expected: FF5E actual:301C
A1C2 --> expected: 2225 actual:2016
A1DD --> expected: FF0D actual:2212
A1F1 --> expected: FFE0 actual:00A2
A1F2 --> expected: FFE1 actual:00A3
A2CC --> expected: FFE2 actual:00AC
8FA2C3 --> expected: FFE4 actual:00A6

これらのx-eucJP-msEUC_JP_Solaris(expectedとactual)の特徴は、XML日本語プロファイル 解説で言及されている、それぞれx-eucjp-open-19970715-msおよびx-eucjp-jisx0221-1995と一致する。特にx-euc-jp-open-19970715-msは、そもそも定義からして、当プログラムのx-eucJP-msの規範とした資料を参照しているので、一致して当然である。

なお秀丸で「¬」の文字コードを調べると、EUC:0xA2CC, Unicode:U+FFE2となり、この場合においてはx-eucJP-msの方に一致することがわかる。wikipedia:EUC-JPによれば秀丸IEではCP51932の変換規則が用いられているとあるため、この付近に関してはCP51932≒x-eucJP-msEUC_JP_Solarisのように見える。

8FF5A1 --> expected: E3AC actual:FFFD
[中略]
8FFEFE --> expected: E757 actual:FFFD

x-eucJP-msで変換しているものが、EUC_JP_Solarisでは変換失敗して置換文字(U+FFFD)になっている。
これは少しく考察が必要になる。はじめにEUCで8Fとは、ISO 2022におけるSS3(G3の内容をIn-Use tableに呼び出すシングルシフト)の意である。続くF5A1の部分を見るとGRに呼び出されているので、元のG3でのコードは0x8080とXORを取って0x7521となる。これを区/点に直せば85/1となる。

ここで問題になるのが、G3に何の文字集合が割り当てられているかである。
EUCの場合、G3にはJIS X 0212 補助漢字が定義されているとする資料があるが、
JIS X 0212には77区までしかないので規格外になる。また、JIS X 0208は正式には84区までしかない。

そこで前述のWikipediaの記述に頼ると、eucJP-msで0x8FF5A1は「2面85区〜94区」に「ユーザ定義文字(後半)」なるものをあてはめている、とあるので、Unicodeの規格を見ると、U+E3ACはhttp://www.unicode.org/charts/PDF/UE000.pdfPrivate Use Area(私用領域)となっているので、結構それなりに変換されていることがわかる。

F5A1 --> expected: E000 actual:FFFD
[中略]
F8FE --> expected: E177 actual:FFFD

こちらは同じく、ユーザ定義文字の前半ということになる。
以下、ユーザ定義文字のU+FFFD変換エラーが続くのかと思いきや、途中でEUC_JP_Solarisの側でのみ、次のような変換が出てきた。

F9A1 --> expected: E178 actual:7E8A

GR領域なのでJIS X 0208の0x7921に相当するはずであり、区/点に直せば89/1となるが、これも規格範囲外であるが、前述のWikipediaやその他の資料を見ると、NEC選定IBM拡張文字が確かにこの区に入れられている。してみるとこの点については、EUC_JP_SolarisはCP51932と同等の動きをしていることになる。「¬」文字(0xA2CC)と異なり、この付近に関してはx-eucJP-msEUC_JP_Solaris≒CP51932のように見える。

ここまでのまとめ
  • JIS X 0208, JIS X 0212の文字に関しては、x-eucJP-msとCP51932が(おそらくは)似ている。 それに対し、EUC_JP_Solarisはこれらと一致しない。そのため、EUC_JP_SolarisをベースにCP51932にあわせることが目的なら、変換が必要である。
  • NEC選定IBM拡張文字については、EUC_JP_SolarisはCP51932と一致する。それに対しx-eucJP-msはこれらと一致しない。Windows上における「EUCコード」で取り込んだこの領域のデータをEUC_JP_SolarisUCS-2に変換することはできるが、これをx-eucJP-msに変換できるかは注意が必要。