24
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RubyでCSVを扱うために大切なことはみんなExcelから教わった

Last updated at Posted at 2019-06-17

f◯ck EXCEL

検証環境

以下の環境で検証しました。
mac
macOS Mojave
プロセッサ 2.7 GHz Intel Core i5
メモリ 16 GB
Excel for Mac バージョン 15.25.1

windows
Windows 10 Home
Intel Core i5-8250U 1.6GHz
メモリ 8GB
Excel for Office 365 MSO(16.0.11629.20210)

CSV出力

結論から言うと以下のポイントがあります。

  • 文字コードはBOM付きUTF-8、改行コードは\r\nCRLF)にする
    • BOM付きUTF-8などShift-JISやCP932(Shift-JIS拡張)に存在しない文字の出力のため
    • CRLFはWindowsのExcelで改行させるため
  • 項目をダブルクォートでくくる
  • 改行やクォートが崩れるので古いバージョン(2016年10月より前)のExcelでは編集しない

CSVを編集する環境

DBデータのCSVエクスポート機能が欲しいと言われた場合、文字コードや改行コードなど特に指定がなくとも、エンジニアでも無ければ一般的にはwindowsマシンのExcelで開くことを想定していると思います。
ちなみにDBはutf8mb4前提です。

文字コード

Excelは何も指定しなければShift-JISでファイルを開こうとするので、何も考えずにUTF-8のままCSV出力すると文字化けしてしまいます。Shift-JIS、もしくはShift-JISを拡張したCP932として出力することも考えられますが、UTF-8の文字列をShift-JIS等に変換する場合、そこそこの確率でその文字コードには存在しない文字に遭遇します。
代表的なもので(Shift-JIS)、(CP932)等がそれぞれ存在しない文字になります。ちなみに0x301C)と0xFF5E)は別の文字です。[1]

改行コード

windowsなのでCRLFで出力します。

コード

DBの店舗一覧をCSV出力してみます。
ActiveRecord::Baseを継承したStoreという店舗モデルを想定しています。

collection = Store.all
headers = collection.column_names

csv_str = CSV.generate(liberal_parsing: true,
                       converters: nil,
                       col_sep: ',',
                       quote_char: '"') do |csv|
  csv << headers

  collection.pluck(headers).each do |columns|
    csv << columns
  end
end

file_path = '/Users/kurashita/Desktop/output.csv'

File.open(file_path, 'w', encoding: Encoding::UTF_8) do |file|
  file.write "\xEF\xBB\xBF"
  # BOM付きUTF-8、改行コードCRLF
  # 置き換え出来ない場合は〓に変換
  file.write NKF.nkf("-w8 -Lw --fb-subchar=0x3013", csv_str)
end

読み込み

こちらも以下のポイントがあります。

  • 文字コードは事前に指定できるなら指定
  • 指定できない場合はNKF.guessで推測
    • 文字列読み込みは20MBで0.8秒、200MBで8秒程度
  • 改行コードは\nLF)に正規化
    • DB登録が前提のため。それ以外にも
  • File.openCSV.newを併用して読み込む
    • CSV.tableCSV.foreachは便利だが全データがメモリに展開される/エンコード等オプション指定の関係でCSV.newを使う
    • 文字コードが決まっていてファイルサイズが大きくなければCSV.table

コード

上記ポイントを抑えたコードが以下になります。

file_path = '/Users/kurashita/Desktop/input.csv'
# 200MBまで読み込む
file_encoding = NKF.guess File.read(file_path, 200_000_000)

csv_file_options = {
      universal_newline: true, # 改行コードの正規化
      undef: :replace,
      invalid: :replace,
      replace: ''
    }
    

File.open(file_path, "rt:BOM|#{file_encoding}", universal_newline: true,
                                                undef: :replace,
                                                invalid: :replace,
                                                replace: ''
) do |f|
  CSV.new(f, liberal_parsing: true,
             headers: true,
             converters: nil,
             header_converters: :symbol,
             skip_lines: /^(?:,\s*)+$/,
             col_sep: ',',
             quote_char: '"').each do |csv|
    # ...
  end
end

注意点

2016年10月より前のバージョンのExcelでの編集は避けることをおすすめします。
理由としては

  • デフォルトでタブ区切りとして保存する
  • カンマ区切りをUTF-8で保存が出来ない
  • CSVはデフォルトでカンマ区切りとして読み込みする

というものがあり、BOM付きUTF-8なファイルを編集して保存するとUTF-8でタブ区切りになるか、Shift-JISのカンマ区切りになるため、正しく項目を扱えなかったり、変換できない文字が_になったりします。

どうしても古いエクセルしかなくてそれで開きたいという場合は、[データ]タブ → [テキストファイル(テキストからデータを取り込み)]でファイルを読み込むことでウィザードに従ってUTF-8やタブ区切りなファイルを読み込むことも出来ますが、ダブルクォートが取れていて改行が正しく認識できなかったり、そもそも面倒な開き方だと思うのでやはり避けるのが無難です。

参考記事

https://qiita.com/swdyh/items/884a0cade2e5fcebfd8b
https://sites.google.com/site/fudist/Home/grep/damemoji

24
15
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?