Techioz Blog

読み取り時に誤ってエンコードされたバイト シーケンスを消去する

概要

ファイルを 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>