RubyでCSV操作する

# 前提
- Ruby 3.1.2
- [ドキュメント](https://docs.ruby-lang.org/ja/3.1/class/CSV.html)
- 以下コードは`require 'csv'`している想定

# 読み込み
仕様としては、[RFC4180](https://datatracker.ietf.org/doc/pdf/rfc4180.pdf) に準拠している
※ 例えば、フィールドをクォートで囲むことで、カンマを含んだ文字列を扱える等が定義されている

また、バリデーションは必要に応じて別途行うこと
例. `raise MalformedCSVError if csv.first != CSV_HEADERS`


## ファイルから
### 一行ずつ
```ruby
CSV.open(file_path, 'r').each do |row|
  p row
end
# 結果
# [...]
# [...]
# ...
# => nil

CSV.foreach(file_path) do |row|
  p row
end
# 結果
# [...]
# [...]
# ...
# => nil

# headers オプションに偽でない値を指定した場合、CSV::Table を返す
CSV.read(file_path, headers: true).each do |row|
  p row
end
# 結果
# #<CSV::Row>
# #<CSV::Row>
# ...
# => #<CSV::Table>

# CSV.read(file_path, { headers: true, converters: :numeric, header_converters: :symbol }.merge(options)) と同じ
CSV.table(file_path).each do |row|
  p row
end
# 結果
# #<CSV::Row>
# #<CSV::Row>
# ...
# => #<CSV::Table>
```

### まとめて
```ruby
CSV.open(file_path, 'r').to_a
# 結果
# [[...], [...], ...]

CSV.read(file_path)
# 結果
# [[...], [...], ...]
```

### ※ BOM 付き CSV の場合
- [BOM](https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E9%A0%86%E3%83%9E%E3%83%BC%E3%82%AF) とは、符号化方式がUnicodeであることを判別するための印
    - 具体的には、テキストの先頭につける数バイトのデータのこと
- [Excelが絡むと起きるっぽい](https://tech.pepabo.com/2021/03/19/remove-the-bom-when-importing-csv/#:~:text=%E4%BB%8A%E5%9B%9E%E3%81%AE%E3%82%B1%E3%83%BC%E3%82%B9%E3%81%A7%E3%81%AFExcel%E3%81%AECSV%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BF%9D%E5%AD%98%E5%BD%A2%E5%BC%8F%E3%81%AB%E3%82%88%E3%81%A3%E3%81%A6BOM%E3%81%8C%E4%BB%98%E3%81%84%E3%81%A6%E3%81%84%E3%81%9F%E3%82%88%E3%81%86%E3%81%A7%E3%81%99%E3%80%82)
- [別途 mode 指定することで、BOM を自動的に取り除く](https://docs.ruby-lang.org/ja/3.1/class/CSV.html#:~:text=%E3%81%BE%E3%81%9F%20%22rb%3ABOM%7CUTF%2D8%22%20%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%20mode%20%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%A8%20BOM%20%E3%82%92%E8%87%AA%E5%8B%95%E7%9A%84%E3%81%AB%E5%8F%96%E3%82%8A%E9%99%A4%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82)

```ruby
CSV.open('test.csv', 'rb:BOM|UTF-8')
```

## 文字列から
```ruby
# 準備
csv_text = <<EOS
id,first name,last name,age
1,taro,tanaka,20
2,jiro,suzuki,18
EOS
```

### 一行ずつ
```ruby
CSV.parse(csv_text) do |row|
  p row
end
# 結果
# ["id", "first name", "last name", "age"]
# ["1", "taro", "tanaka", "20"]
# ["2", "jiro", "suzuki", "18"]
# => 62
```

### まとめて
```ruby
CSV.parse(csv_text)
# 結果
# => [["id", "first name", "last name", "age"], ["1", "taro", "tanaka", "20"], ["2", "jiro", "suzuki", "18"]]
```

## オプション
[一覧](https://docs.ruby-lang.org/ja/3.1/method/CSV/s/new.html#:~:text=%E3%81%8C%E5%87%BA%E6%9D%A5%E3%81%BE%E3%81%99%E3%80%82-,%5BPARAM%5D%20options%3A,-CSV%20%E3%82%92%E3%83%91%E3%83%BC%E3%82%B9)のうち、よく使うものを抜粋

- ヘッダー
    - 存在する場合は、`headers: true`
    - `~['カラム名']`で参照できるようになる
    - 変換する場合は、`header_converters: ~`
        - 変換例
            - 文字列 → シンボル: `:symbol`
            - 独自設定: `->(h) { HEADER_MAPS[h] }`
                - `HEADER_MAPS = { 'a' => :a, 'b' => :b }`
- 区切り文字
    - タブ区切りにしたい時は、`col_sep: "\t"`
- 文字コード
    - 変換する例: `encoding: 'CP932:UTF-8'`
- 不正CSV（RFC 4180 に違反）
    - 具体的には、フィールド内にエスケープされていない二重引用符を含むケース
    - パースする場合は、`liberal_parsing: true`
        - 例. ` CSV.parse('a,b"c,d', liberal_parsing: true)
=> [["a", "b\"c", "d"]]`
    - [解説記事](https://qiita.com/yohm/items/c3dde4acf0781fa442ea)




# 書き込み
※ 基本的にTSVで出力するようにしている
スプレッドシートに貼り付けて列分解する時に、列値にカンマ入っていると列がズレるため（列値にタブ入っていたら同じか。。スプレッドシート側の問題か）

## ファイルへ
```ruby
CSV.open("path/to/file.csv", "w") do |csv|
  csv << ["row", "of", "CSV", "data"]
  csv << ["another", "row"]
  # ...
end
```

### ※ BOM 付き CSV の場合
- 書き込み時にBOMを追加するオプションは`CSV.open`にはない
    - そもそも必要になることが少なそうではある
- なので、[明示的にBOMを付与してあげる必要がある](https://rooter.jp/data-format/how_to_use_csv_module/#:~:text=CSV.generate%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%82%84%E3%82%8A%E6%96%B9)


## 文字列へ
```ruby
csv_string = CSV.generate do |csv|
  csv << ["row", "of", "CSV", "data"]
  csv << ["another", "row"]
end
# 結果
# => "row,of,CSV,data\nanother,row\n"
```



## オプション
よく使うものを抜粋
※ `読み込み`と同じ場合は省略

- ヘッダー
    - 出力する場合は、`write_headers: true, headers: [...]`