ヘッダーが日本語の巨大CSVを取り込んでみる

CSVファイルの取り込みが必要とする場面はかなり頻繁に出ていると思います。しかし数百メガ以上ファイルサイズだとサーバーの処理に影響出かねなません。そして取り込まれるファイルのヘッダーが日本語だと、実装のハードルが格段上がると思います。

# TL;DR

Demo用のRuby fileは下記のURLで見れます。
https://gist.github.com/jerrywdlee/55ba403f02651afc67dbda8185329780

```rb
# まずは日本語ヘッダーを英語に転換するlambdaを作成
headers_jp = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
headers_en = %w[id name kana age blood state carrier]
headers_dict = headers_jp.zip(headers_en).to_h
converter = lambda { |h| headers_dict[h] }

# CSV.foreacで１行ずつ取り込む
CSV.foreach(path, headers: true, header_converters: converter) do |row|
  p row.headers if row['id'] == 1
  p row if row['id'] == 1
  # Do sth. with `row`
end
```

# 解説
## サンプルファイルの作成
下記のロジックでサンプルCSVを作成しています。

```rb
def generate(cnt = 1_000_000)
  headers = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
  exec_benchmark do
    CSV.open('dummy_data.csv', 'w', write_headers: true, headers: headers) do |csv|
      cnt.times do |i|
        age = rand(100)
        blood = %w[A B O AB][rand(4)]
        carrier = %w[ドコモ au ソフトバンク][rand(3)]
        csv << [i, '打見　花子', 'ダミ　ハナコ', age, blood, '東京都', carrier]
      end
    end
    file_size = `ls -lah dummy_data.csv | awk '{print $5}'`
    puts "File size: #{file_size}"
  end
end
```

__100万行で65Mのファイルとなります。__

```rb
irb(main):002:0> LargeUnicodeCsv.generate
File size: 65M
Time: 10.3s
Memory: 2.11MB
```

## `CSV.table`
まずおなじみの`CSV.table`メソッドを試してみます。

```rb
def csv_table(path = 'dummy_data.csv')
  exec_benchmark do
    table = CSV.table(path)
    p table.headers
    p table[0]
  end
end
```

結果見ると、まず、日本語のヘッダーは全部空白になっていました。
そしてin memory処理したせいか、1G程度のメモリを消耗しました。
処理時間も１分間超えていました。

```ruby
irb(main):003:0> LargeUnicodeCsv.csv_table
[:id, :"", :"", :"", :"", :"", :""]
#<CSV::Row id:0 :"打見　花子" :"ダミ　ハナコ" :27 :"B" :"東京都" :"ソフトバンク">
Time: 74.47s
Memory: 1021.91MB
=> nil
```

## `CSV.each`
こちらの[記事](https://qiita.com/tyamagu2/items/950aad7e67de1592e9d3)が紹介した、ヘッダー行が日本語のCSVの対処法です。

```ruby
def csv_each(path = 'dummy_data.csv')
  exec_benchmark do
    headers_jp = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
    headers_en = %w[id name kana age blood state carrier]
    headers_dict = headers_jp.zip(headers_en).to_h
    header_converter = lambda { |h| headers_dict[h] }

    csv = CSV.read(path, headers: :first_row, header_converters: header_converter)

    p csv.headers
    p csv[0]
  end
end
```

結果見ると日本語のヘッダーはちゃんと処理されました。
そして処理時間も劇的によくなりました。
しかしメモリ使用はまだ1G程度のままでした。

```rb
irb(main):002:0> LargeUnicodeCsv.csv_each
["id", "name", "kana", "age", "blood", "state", "carrier"]
#<CSV::Row "id":"0" "name":"打見　花子" "kana":"ダミ　ハナコ" "age":"27" "blood":"B" "state":"東京都" "carrier":"ソフトバンク">
Time: 9.61s
Memory: 1000.56MB
```

## `CSV.csv_foreach`
今回紹介したい`CSV.csv_foreach`メソッドです。文章によると、`File.open`のラッパーのようです。
`converters: :integer`や`encoding: 'Shift_JIS:UTF-8'`などパラメーターも付けられるので、汎用性はとても高いです。

```rb
def csv_foreach(path = 'dummy_data.csv')
  exec_benchmark do
    headers_jp = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
    headers_en = %w[id name kana age blood state carrier]
    headers_dict = headers_jp.zip(headers_en).to_h
    converter = lambda { |h| headers_dict[h] }

    CSV.foreach(path, headers: true, header_converters: converter) do |row|
      p row.headers if row['id'] == '1'
      p row if row['id'] == '1'
    end
  end
end
```

結果見ると日本語のヘッダーはちゃんと処理されました。
そして処理時間も低く抑えられていました。
肝心なメモリ使用量も５メガ以下に抑えられていました。

```rb
irb(main):002:0> LargeUnicodeCsv.csv_foreach
["id", "name", "kana", "age", "blood", "state", "carrier"]
#<CSV::Row "id":"1" "name":"打見　花子" "kana":"ダミ　ハナコ" "age":"52" "blood":"A" "state":"東京都" "carrier":"ソフトバンク">
Time: 6.34s
Memory: 4.6MB
```

# 参考
- [CSV のヘッダー行の日本語をシンボルに変換して要素にアクセスしたりとか](https://qiita.com/tyamagu2/items/950aad7e67de1592e9d3)
- [Processing large CSV files with Ruby](https://dalibornasevic.com/posts/68-processing-large-csv-files-with-ruby)
- [class CSV (Ruby 2.7.0 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/class/CSV.html)