ブログ記事 からの転載です。
Ruby 2.5 系で CSV.generate
を使用しようとしたら意図しない動作をして、調べてみたらバグだったのでそのまとめ。
しかし、これ、結構クリティカルなバグだと思うんですけど、全然話題になってないのが不思議(当時は話題になっていたのかもしれないけど。
CSV.generate
とは
以下のような感じで CSV 形式で文字列を構築する事が出来ます。
# Ruby 2.4 で実行
require "csv"
require "pp"
result = CSV.generate do |csv|
csv << [1, 2, 3]
csv << ["homu", "mami", "mado"]
end
pp result
# => "1,2,3\n" + "homu,mami,mado\n"
また CSV.generate
の第一引数で『その CSV の先頭文字列』を指定する事が出来ます。
require "csv"
require "pp"
csv = CSV.generate("prefix") do |csv|
csv << [1, 2, 3]
end
pp csv
# => "prefix1,2,3\n"
# 既存の csv に対して追記したり
csv = CSV.generate(csv) do |csv|
csv << ["homu", "mami", "mado"]
end
pp csv
# => "1,2,3\n" + "homu,mami,mado\n"
CSV.generate
のバグ
で、件のバグなんですが、先ほど説明した『CSV.generate
の第一引数で『その CSV の先頭文字列』を指定する』が Ruby 2.5 では動作しません。
# Ruby 2.5 で実行した場合
require "csv"
csv = CSV.generate("prefix") do |csv|
csv << ["homu", "mami", "mado"]
end
pp csv
# => "homu,mami,mado\n"
上記のようなコードではすぐに『何かおかしい』とわかるんですが、例えば次のように『CSV データに BOM を追加する』みたいな事をしたい場合はほぼ気づきません。
require "csv"
# BOM 付き CSV データを生成したいが追加されない…
bom = "\uFEFF"
csv = CSV.generate(bom) do |csv|
csv << ["homu", "mami", "mado"]
end
# 出力先によっては BOM がついているかどうかが視覚的にわからないのでバグを見つけるのがむずかしい…
pp csv
# => "homu,mami,mado\n"
わたしも上記のような事をやりたかったんですが、うまく動作しなくて調べてみたら既知のバグでした。
『Ruby BOM CSV』でググると CSV.generate
を使ったやり方を書いているブログとかが結構ヒットするんですが、このバグに気づいてない人もいるんじゃないかなあ…。
Ruby 2.6 では修正済み
このバグは Ruby 2.6 では修正済みとなっています。
Ruby 2.5 系での対処方法
幸いにも CSV.generate
は Ruby で実装されているので次のようなモンキーパッチで対処する事が出来ます。
require "csv"
# CSV.generate の実装を上書き
class CSV
def self.generate(str=nil, **options)
# add a default empty String, if none was given
if str
str = StringIO.new(str)
str.seek(0, IO::SEEK_END)
else
encoding = options[:encoding]
str = String.new
str.force_encoding(encoding) if encoding
end
csv = new(str, options) # wrap
yield csv # yield for appending
csv.string # return final String
end
end
# これで BOM が追加される
bom = "\uFEFF"
csv = CSV.generate(bom) do |csv|
csv << ["homu", "mami", "mado"]
end
pp csv
# => "homu,mami,mado\n"
まとめ
Ruby 2.6 はよ〜