Help us understand the problem. What is going on with this article?

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

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

TL;DR

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

# まずは日本語ヘッダーを英語に転換する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で1行ずつ取り込む
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を作成しています。

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のファイルとなります。

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

CSV.table

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

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程度のメモリを消耗しました。
処理時間も1分間超えていました。

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

CSV.each

こちらの記事が紹介した、ヘッダー行が日本語のCSVの対処法です。

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程度のままでした。

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: :integerencoding: 'Shift_JIS:UTF-8'などパラメーターも付けられるので、汎用性はとても高いです。

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

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

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

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away