Techioz Blog

jruby の入力データのエンコーディング エラーをサニタイズする

概要

私の JRuby アプリケーションでは、次の 2 つのソースから入力を取得します。

外部データの一部は ISO_8859_1 としてエンコードされる(ことになっている)のですが、内部では UTF_8 として処理し、出力としても UTF_8 を生成します。

残念ながら、エンコード エラーが発生することがあります。データには、有効な ISO_8859_1 ではないバイトが含まれる場合があり、これは修正されません。仕様では、これらの不正な入力バイトを単純に破棄する必要があります。

ファイルの場合、次を使用してファイルを読んでいます

string = File.new(filename, {external_encoding: Encoding::ISO_8859_1, internal_encoding: Encoding::UTF_8, converters: UTF8_CONVERTER})

Converts 句は、不正な入力バイトがスキップされるように注意します。

もちろん、Java 側から受け取った文字列の場合は、次のようにして UTF_8 に変換できます。

string = iso_string.encode(Encoding::UTF_8)

しかし、ここで不正な文字を見つけるにはどうすればよいでしょうか? encode メソッドの Ruby ドキュメントを理解したところ、宛先エンコードの後に指定できるオプションには、converts キーが提供されません。

アップデート

問題を示す簡単な例を次に示します。

  1. 良いケース(エラーなし)
s = [49, 67].pack('C*')
put s
puts s.encoding
u = s.encode(Encoding::UTF_8)
puts u
puts u.encoding

これは印刷します

1C    
ASCII-8BIT
1C
UTF-8
  1. エラーの場合
x = [49, 138, 67].pack('C*')
x.encode(Encoding::UTF_8)

予想通り、UnknownConversionError: ““�”” が ASCII-8BIT から UTF-8 に発生します。

私が試したこと(文書化されていませんが):

t = x.encode(external_encoding: Encoding::ISO_8859_1, internal_encoding: Encoding::UTF_8, converters: UTF8_CONVERTER)

興味深いことに、これにより例外は取り除かれましたが、それでも変換は成功しませんでした。私がやったら

t.encoding

まだ ASCII-8BIT が表示されます。何も変換されていなかったようです。不正な文字が削除されることを望みます。つまり、この場合 t は空の文字列です。

解決策

そんなことはないので、これは不思議です。 ISO 8859-1 文字エンコーディングは、256 個の可能な 8 ビット バイトすべてをカバーしており、無効なものはありません。また、これらはすべて Unicode に変換することもできます。これは、最も低い 256 の Unicode コード ポイントが ISO 8859-1 の 256 文字に 1:1 で対応するため、当然のことです。

(バイト 0 ~ 31 および 127 ~ 159 にマッピングされた 65 個の印刷不可能な「制御文字」がありますが、これらはすべて Unicode にも含まれています。これらの制御文字には、タブレータ、ライン フィード、キャリッジ リターンなどのかなり一般的なものが含まれます。ただし、めったに使用されないものも多数あります。)

実際の問題は、Ruby がバイト文字列を ISO_8859_1 ではなくデフォルトの ASCII_8BIT エンコーディングとしてマークしていることのようです。これは、文字列に 256 個の 8 ビット バイトすべてを含めることができるようにする特別なエンコードですが、7 ビット ASCII 文字エンコードに対応する最初の 128 個についてのみ Unicode 文字値を定義します。 Ruby のドキュメントを引用するには:

とにかく、あなたの場合の解決策は、単純に String#force_encoding メソッド (何らかの理由で従来の感嘆符がないにもかかわらず、文字列をその場で変更します!) を使用して、バイト文字列のエンコーディングをあるべきものに変更することです。つまり、あなたの場合、Encoding::ISO_8859_1、次のように:

x = [49, 138, 67].pack('C*')
puts "x = #{x.inspect} has encoding #{x.encoding}"
x.force_encoding(Encoding::ISO_8859_1)
puts "x = #{x.inspect} now has encoding #{x.encoding}"
u = x.encode(Encoding::UTF_8)
puts "u = #{u.inspect} has encoding #{u.encoding}"

これは次のように出力します:

x = "1\x8AC" has encoding ASCII-8BIT
x = "1\x8AC" now has encoding ISO-8859-1
u = "1\u008AC" has encoding UTF-8

ご覧のとおり、ISO 8859-1 制御文字 138 (16 進数 0x8A、検査出力では � として表される) は、Unicode に相当する U+008A (08A) に正常に変換されました。

追伸また、入力データが実際には ISO 8859-1 エンコーディングではなく、Windows-1252 などの他の関連エンコーディングである可能性もあります。ISO 8859-1 との違いは、65 の非エンコーディングのうち 32 を置き換えるという点のみです。印刷可能な制御文字 (正確には、128 から 159 までのバイトで構成される C1 ブロック) と、さまざまな追加記号およびアクセント付き文字。

その場合は (データの一部を Windows-1252 としてデコードして結果が意味があるかどうかを確認することで、かなり簡単にテストできるはずです)、Encoding::ISO_8859_1 の代わりに Encoding::WINDOWS_1252 を使用する必要があります。例えば:

x = [49, 138, 67].pack('C*')
puts "x = #{x.inspect} has encoding #{x.encoding}"
x.force_encoding(Encoding::WINDOWS_1252)
puts "x = #{x.inspect} now has encoding #{x.encoding}"
u = x.encode(Encoding::UTF_8)
puts "u = #{u.inspect} has encoding #{u.encoding}"

印刷されます:

x = "1\x8AC" has encoding ASCII-8BIT
x = "1\x8AC" now has encoding Windows-1252
u = "1ŠC" has encoding UTF-8

� バイトがアクセント付き文字 Š にどのように変換されているかに注目してください。これは Windows-1252 エンコードで表されるものです。