読み取り時に誤ってエンコードされたバイト シーケンスを消去する
概要
ファイルを Ruby 文字列に読み込んでおり、これらの文字列は後で (たとえば、CSV モジュールを使用して) さらに処理されます。ファイルの外部エンコーディングはパラメータであり、おそらく、処理されるファイルは指定されたエンコーディングである必要があります。
読み取り中に、ファイルを想定されている外部エンコードから UTF-8 に変換します。
場合によっては、指定されたエンコードとは異なる方法でエンコードされた誤ったファイルを取得することがあります。
もちろん、エンコーディングが間違っていれば、プログラムはゴミのみを読み取りますが、エンコーディングが間違っているだけでなく、想定されているエンコーディングでは不正なバイト シーケンスが含まれている場合、ファイルの処理時に例外が発生します。
仕様では、エンコードが正しくないために解読できないバイト シーケンスは、プログラムを強制終了させるのではなく、入力ファイルから単純に削除する必要があります。
これを実装するには、次のような文字列にファイルを読み込みます。
UTF8_CONVERTER = ->(field) { field.encode('utf-8', invalid: :replace, undef: :replace, replace: "") }
read_flags = {
external_encoding: ext_enc, # i.e. Encoding::ISO_8859_1
internal_encoding: Encoding::UTF_8,
converters: UTF8_CONVERTER
}
file_content = IO.read(file_path, read_flags)
IMO、これは file_content を UTF-8 でエンコードされた有効な文字列にする必要があります。後でプログラムがこの文字列を CSV 解析する必要があると判断した場合、次のように csv パーサーを呼び出します。
e_enc = file_content.encoding
i_enc = Encoding::UTF_8
...
csv_opt = { col_sep: ';', row_sep: :auto, external_encoding: e_enc, internal_encoding: i_enc}
CSV.foreach(file_content, csv_opt) { .... }
ここでもエンコーディングを重複して指定する理由は、CSV を処理するメソッドには汎用性があり、文字列のエンコーディングが異なる場合にも機能するはずであるためです。
ただし、これは機能しません。
UTF-8 (つまり、ext_enc が Encoding::UTF_8 と等しい) であるはずのファイルを処理しているが、実際には Windows-1252 などでエンコードされており、その中にいくつかのバイト シーケンスが含まれている場合、これは以下では不正になります。 UTF、CSV.foreach は例外 ArgumentError: UTF-8 の無効なバイト シーケンスを発生させます。
このことから、私の UTF8_CONVERTER は間違ったバイトを削除しなかったと結論付けられます。
誰か私がここで間違っていることを理解できますか?
アップデート
@Stefan はコメントの中で、IO.read ではコンバーター オプションを使用できないことを指摘し、変換オプションを直接渡すことを提案しました。これも機能しません (Ruby 1.9.3 と同等の JRuby 1.7.21 を使用する必要があります)。少なくとも、再現可能な小さな例を作成することはできます。
次の内容のファイルillegal.txtを作成します。
> xxd illegal.txt
00000000: 66fc 720a f.r.
これには、正当な UTF-8 ではないバイト シーケンス FC 72 が含まれていることがわかります。
今、私はファイルを読みました
fc=IO.read('illegal.txt', {:external_encoding=>#<Encoding:UTF-8>, :internal_encoding=>#<Encoding:UTF-8>, :invalid=>:replace, :undef=>:replace, :replace=>""}
これにより少なくとも FC が削除され、結果の文字列が “fr” になると予想していました。 “、あるいは単に”f “。しかし、私が
puts fc.bytes.to_a
まだ [102, 252, 114, 10] が印刷されています。
解決策
IO.read 経由でファイルを読み取るときは、無効または未定義のバイト シーケンスを置換するために、外部エンコーディングを ASCII として指定し、内部エンコーディングを UTF-8 として指定する必要があります (デモ用に置換文字列として ’_’ を使用しています)ここでの目的は空の文字列でも機能します)
data = IO.read('invalid.txt', encoding: 'ASCII:UTF-8',
undef: :replace,
invalid: :replace,
replace: '_')
data #=> "f_r\n"
data.bytes #=> [102, 95, 114, 10]
data.encoding #=> #<Encoding:UTF-8>
あるいは、ファイルをバイナリ読み取りすることもできます。
data = IO.binread('invalid.txt')
data #=> "f\xFCr\n"
data.bytes #=> [102, 252, 114, 10]
data.encoding #=> #<Encoding:ASCII-8BIT>
…そしてエンコードしてください!その後の文字列:
data.encode!('utf-8', undef: :replace, invalid: :replace, replace: '_')
data #=> "f_r\n"
data.bytes #=> [102, 95, 114, 10]
data.encoding #=> #<Encoding:UTF-8>