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

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\n`（**CRLF**）にする
  - **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`という店舗モデルを想定しています。

```ruby
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秒程度
- 改行コードは`\n`（**LF**）に正規化
  - DB登録が前提のため。それ以外にも
- `File.open`、`CSV.new`を併用して読み込む
  - `CSV.table`や`CSV.foreach`は便利だが全データがメモリに展開される／エンコード等オプション指定の関係で`CSV.new`を使う
  - 文字コードが決まっていてファイルサイズが大きくなければ`CSV.table`で

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

```ruby
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