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: :integer
やencoding: '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