rubyのIOとエンコーディングについて

IOのドキュメントより

IOオブジェクトは外部エンコーディングと内部エンコーディングを持つ。

外部エンコーディング
IOが表すファイルなどの文字エンコーディング
内部エンコーディング
IOから読み込まれた文字列、あるいはIOの書き込みメソッドへ渡す文字列の文字エンコーディング

Kernel.#openのドキュメントより

第二引数のモードには、文字列では以下のパターンを受け付けている。

  • "{mode}"
  • "{mode}:{ext_enc}"
  • "{mode}:{ext_enc}:{int_enc}"

modeはr/w/a/r+/w+/a+というオープンモードの指定。ext_encは外部エンコーディングの指定で、int_encは内部エンコーディングの指定。

File.open

Shift_JISで記述したCSVファイルをエンコーディングを指定してFile.openで読み込んだ場合。

$ cat encoding.rb
File.open('sjis.csv', 'r:Shift_JIS:UTF-8') do |input|
  input.each_line do |line|
    puts line.encoding
    puts line
  end
end
$ ruby -v
ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin11.0.1]
$ ruby encoding.rb
UTF-8
"あ","い","う","え","お"

期待通り、Shift_JISで読み込んだ文字列をUTF-8に変換して出力してくれる。

CSV.foreach

CSV.foreachの:encodingオプションも同じだと思い試したが、例外が発生してしまう。

$ cat csv_encoding.rb
require 'csv'

CSV.foreach('sjis.csv', :encoding => 'Shift_JIS:UTF-8') do |row|
  puts row
end
$ ruby csv_encoding.rb
/Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb:2027:in `=~': invalid byte sequence in UTF-8 (ArgumentError)
        from /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb:2027:in `init_separators'
        from /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb:1570:in `initialize'
        from /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb:1335:in `new'
        from /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb:1335:in `open'
        from /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb:1201:in `foreach'
        from csv_encoding.rb:3:in `<main>'

CSVライブラリのソースを追いかけて、読み込みの実装を確認。改めて、File.openの確認のために以下を試す。

io = File.open('sjis.csv', 'r:Shift_JIS:UTF-8')
data = io.read
puts data.encoding
io.rewind
data = io.read(1024)
puts data.encoding
io.close

バイト数を指定せずにreadを実行すると内部エンコーディングに変換されるが、バイト数を指定するとASCII-8BITとなる。

で?

どうしたらいいんだ?File#readの仕様を把握してないので、次はそこを探ろう。

追記

引数 length が指定された場合はバイナリ読み込みメソッド、そうでない場合はテキスト読み込みメソッドとして 動作します。

なるほど。

追記

CSVの読み込み処理。

class CSV
...
  def read_io(bytes)
    @io.read(bytes).force_encoding(raw_encoding)
  end
end
  1. バイナリ読み込みメソッドで読み込んでいる(変換されない)
  2. force_encodingで内部エンコーディングを強制している(変換してないのに)
  3. この状態のまま、=~でパターンマッチングを実行してinvalid byte sequence in UTF-8って言われる

追記

CSVモジュールのread_ioメソッドを書き換えて、エンコーディング指定だけじゃなく変換までやるようにしてみた。どうするのが筋が良いのかは考えてない。

--- /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb    2011-10-19 00:23:20.000000000 +0900
+++ /Users/koshigoe/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/csv.rb.orig       2011-10-19 00:23:18.000000000 +0900
@@ -2315,7 +2315,7 @@
   end

   def read_io(bytes)
-    @io.read(bytes).encode(raw_encoding, @io.external_encoding)
+    @io.read(bytes).force_encoding(raw_encoding)
   end
 end

外部エンコーディングだけ指定した場合、内部エンコーディングも指定した場合で期待通りの結果を得られた。

require 'csv'

CSV.foreach('sjis.csv', :encoding => 'Shift_JIS') do |row|
  row.each{|col| puts col.encode('UTF-8') }
end

CSV.foreach('sjis.csv', :encoding => 'Shift_JIS:UTF-8') do |row|
  puts row
end

次はRedmineで情報を探るべきだろうか。見逃される様なものじゃないと思うし、バグではないと思うんだけどよくわからんね。