初めまして。12月からrooterの学生アルバイトとして働いておりますmiyayamaと申します。今回は、研修の中で学んだRubyのCSVモジュールの使い方について書きたいと思います。

Ruby標準添付ライブラリーであるcsvモジュールは、ruby上でCSVの入出力およびCSV形式のレコード作成まで面倒をみてくれます。しかし、全ての処理をcsvモジュールに頼るのが良いというと、そうでもありません。例えば、Ruby上の配列データをCSVファイルとして出力することを考えてみます。

CSVファイルの出力

require 'csv'

people = [["山田太郎", "男", "19"],["鈴木華子", "女", "25"],["田中一郎", "男", "32"]]

CSV.open("people.csv", "w", :force_quotes => true) do |file|
  people.each do |person|
    file << person
  end
end
  csvモジュールに含まれるCSV.openメソッド使って簡単に出力ができました。 しかしpeople.csvをExcelで開いてみると、 文字化けしてしまいました。

Rubyで出力したcsvファイルは、UTF-8でコードされています。Excelは何も指定のないファイルは全てShift-JISでコードされたファイルとして開く設定になっているので、UTF-8でコードされたファイルは文字化けします。 そこで、BOMというものをファイルの先頭に追加する必要があります。

BOM(Byte Order Mark)とは

Unicodeで書かれたファイルであることを明示するために、ファイルの先頭につける数バイトのデータのことです。UTF-8の場合は、”0xEF 0xBB 0xBF”を先頭に追加すると、BOMつきUTF-8としてファイルを出力でき、ExcelがUTF-8としてファイルを開いてくれます。

BOMつきCSVファイルの出力

では、BOMを追加してみましょう。
require 'csv'

people = [["山田太郎", "男", "19"],["鈴木華子", "女", "25"],["田中一郎", "男", "32"]]
bom ="\xEF\xBB\xBF" #bomを作成

CSV.open("people.csv", "w", :force_quotes => true) do |file|
  file << bom #bomを先頭に追加?
  people.each do |person|
    file << person 
  end
end
しかしこれはできません。CSV.openメソッドは、ファイルをブロックの中でCSVクラスのオブジェクトという形で渡しています。CSVクラスのオブジェクトには、文字列であるBOMを追加することができないのでエラーが発生するのです。 そこで、ファイルの書き込みはcsvモジュールに頼らず、Rubyに組み込まれているIOクラスのFile.openメソッドを使って行ってみます。一方、配列をcsvへ変換する時には、csvモジュールに組み込まれたto_csvメソッドを使います。
require 'csv'

people = [["山田太郎", "男", "19"],["鈴木華子", "女", "25"],["田中一郎", "男", "32"]]
bom ="\xEF\xBB\xBF" #bomを作成

File.open("people.csv", "w") do |file|
  file.print(bom) #bomを先頭に追加
  people.each do |person|
    file.puts(person.to_csv(:force_quotes => true))
  end
end
出力ファイルをExcelで開いてみます。 文字化けされず表示されました!

CSV.generateメソッドを使ったやり方

実はcsvモジュール内のCSV.generateメソッドを用いてもBOMつきcsvを作ることができます。CSV.generateは、引数に与えた文字列を追加したCSVオブジェクトをブロックに渡します。文字列としてのcsvが返ってくるので、これをFile.writeメソッドで書き込みます。
require 'csv'

people = [["山田太郎", "男", "19"],["鈴木華子", "女", "25"],["田中一郎", "男", "32"]]
bom ="\xEF\xBB\xBF" #bomを作成

csv_string = CSV.generate(bom, :force_quotes => true) do |csv| #bomをラップしてブロックに渡す
  people.each do |person|
    csv << person
  end
end

File.open("test.csv", "w") do |file|
  file.write(csv_string)
end
しかし、CSV.generateメソッドの使用には注意が必要です。
  • Ruby2.5系でCSV.generateでラップした文字列が追加されないバグがあるhttps://bugs.ruby-lang.org/issues/14253
  • CSV.generateはcsvの全行を含んだ文字列を作成するので、ビッグデータを扱うときはメモリの限界までしか展開できない
そのためCSV.generateではなくto_csvを使って1行づつ処理することが、安定性やパフォーマンスの面で良いと思われます。

ちょうど良いCSVモジュールの使い方

このようにcsvモジュールは便利ですが、BOMを追加したい時など、柔軟性に欠けるところがあります。 他にも、CSV.openはファイルをブロック内で配列として開くので、putsなどの標準出力により要素を追加することはできませんが、IO.openであればputsメソッドなどの標準出力が使えます。 そのため、
  • CSVモジュールはCSVのレコードを作る範囲でのみ使う
  • ファイルの入出力はRuby組み込みのIOクラスを使う
というように、レコード作成と入出力する処理を分担するのがちょうど良いCSVモジュールの使い方になるのかなと思います。

おまけ

CSV.openメソッドの公式リファレンスを見ると、BOMに関する記述があります。
CSV.open (Ruby 2.7.0 リファレンスマニュアル)


これは、読み取り時にBOMを取り除くオプションを指定できるということなのですが、書き込み時にBOMを追加するオプションはCSV.openにはありません。紛らわしいですが注意しましょう。
Pocket