翻译 使用 Ruby 处理大型 CSV 文件

xguox · 发布于 2016年10月26日 · 最后由 ForrestDouble 回复于 2017年04月03日 · 4777 次阅读
D51b3b
本帖已被设为精华帖!

早些个月看到的一篇, 翻译了以后好久其实也好像没啥机会正式用到, 今天刚好要处理一个 100+ MB 的 CSV 文件, 想起了这篇. 跑的时候顺手打开监视器看看内存使用什么的, 个别方法都跑到 6G + 内存了 Σ( ° △ °|||)︴

处理大文件是一项非常耗内存的操作, 有时候甚至会跑光服务器上的物理内存和虚拟内存. 下面来看看使用 Ruby 来处理大型 CSV 文件的几种方式, 同时测试一下这几种方式的内存消耗以及性能.

准备测试用的 CSV 数据文件.

在开始之前, 先准备一个拥有一百万行的 CSV 文件 data.csv(大约 75mb)用于测试.

# generate_csv.rb
require 'csv'
require_relative './helpers'

headers = ['id', 'name', 'email', 'city', 'street', 'country']

name    = "Pink Panther"
email   = "pink.panther@example.com"
city    = "Pink City"
street  = "Pink Road"
country = "Pink Country"

print_memory_usage do
  print_time_spent do
    CSV.open('data.csv', 'w', write_headers: true, headers: headers) do |csv|
      1_000_000.times do |i|
        csv << [i, name, email, city, street, country]
      end
    end
  end
end

内存及时间的消耗

上面这个脚本需要引用到 helpers.rb 脚本, helpers.rb 定义了两个 helper 方法来测量并打印出内存以及时间的消耗情况.

# helpers.rb
require 'benchmark'

def print_memory_usage
  memory_before = `ps -o rss= -p #{Process.pid}`.to_i
  yield
  memory_after = `ps -o rss= -p #{Process.pid}`.to_i

  puts "Memory: #{((memory_after - memory_before) / 1024.0).round(2)} MB"
end

def print_time_spent
  time = Benchmark.realtime do
    yield
  end

  puts "Time: #{time.round(2)}"
end

执行并生成 CSV 文件:

$ ruby generate_csv.rb
Time: 7.14
Memory: 4.79 MB

不同的机器输出结果也不尽相同, 不过, 幸运的是, 得益于 garbage collector (GC) 回收已使用过的内存, 这个 Ruby 进程耗掉的内存(4.79MB)并不是很夸张. 同时生成的数据文件如图为 74MB.

CSV.read

# parse1.rb
require_relative './helpers'
require 'csv'

print_memory_usage do
  print_time_spent do
    csv = CSV.read('data.csv', headers: true)
    sum = 0

    csv.each do |row|
      sum += row['id'].to_i
    end

    puts "Sum: #{sum}"
  end
end

执行结果:

$ ruby parse1.rb

Sum: 499999500000
Time: 21.3
Memory: 1277.07 MB

惊人的超过 1GB 内存消耗啊.

CSV.read 源码

 # File csv.rb, line 1750
def read
  rows = to_a
  if @use_headers
    Table.new(rows)
  else
    rows
  end
end

CSV.parse

# parse2.rb
require_relative './helpers'
require 'csv'

print_memory_usage do
  print_time_spent do
    content = File.read('data.csv')
    csv = CSV.parse(content, headers: true)
    sum = 0

    csv.each do |row|
      sum += row['id'].to_i
    end

    puts "Sum: #{sum}"
  end
end

执行结果:

$ ruby parse2.rb
Sum: 499999500000
Time: 21.88
Memory: 1362.89 MB

可以看到, 内存消耗的比刚刚第一个脚本还略多, 差距大概就是刚好 CSV 文件大小.

CSV.parse 源码

# File csv.rb, line 1293
def self.parse(*args, &block)
  csv = new(*args)
  if block.nil?  # slurp contents, if no block is given
    begin
      csv.read
    ensure
      csv.close
    end
  else           # or pass each row to a provided block
    csv.each(&block)
  end
end

CSV.new

# parse3.rb
require_relative './helpers'
require 'csv'

print_memory_usage do
  print_time_spent do
    content = File.read('data.csv')
    csv = CSV.new(content, headers: true)
    sum = 0

    while row = csv.shift
      sum += row['id'].to_i
    end

    puts "Sum: #{sum}"
  end
end

执行结果:

$ ruby parse3.rb
Sum: 499999500000
Time: 16.89
Memory: 76.72 MB

这次结果可以看到, 因为只是在内存中加载了整个文件内容, 所以内存的消耗大概就是文件的大小(74MB), 而处理时间也快了不少. 当我们只是想逐行逐行的操作而不是读取要一次过读取一整个文件时候, 这种方法非常奏效.

通过 IO 对象一行一行解析

虽然有了很大的进步, 但是, 使用 IO 文件对象还可以做得更好.

# parse4.rb
require_relative './helpers'
require 'csv'

print_memory_usage do
  print_time_spent do
    File.open('data.csv', 'r') do |file|
      csv = CSV.new(file, headers: true)
      sum = 0

      while row = csv.shift
        sum += row['id'].to_i
      end

      puts "Sum: #{sum}"
    end
  end
end

执行结果:

$ ruby parse4.rb
Sum: 499999500000
Time: 13.78
Memory: 2.64 MB

仅仅用了 2.64MB, 速度也稍稍又快了一些.

CSV.foreach

# parse5.rb
require_relative './helpers'
require 'csv'

print_memory_usage do
  print_time_spent do
    sum = 0

    CSV.foreach('data.csv', headers: true) do |row|
      sum += row['id'].to_i
    end

    puts "Sum: #{sum}"
  end
end

执行结果跟上一个差不多:

$ ruby parse5.rb
Sum: 499999500000
Time: 15.54
Memory: 2.62 MB

CSV.foreach 源码

# File csv.rb, line 1118
def self.foreach(path, options = Hash.new, &block)
  return to_enum(__method__, path, options) unless block
  open(path, options) do |csv|
    csv.each(&block)
  end
end
共收到 20 条回复
De6df3 huacnlee 将本帖设为了精华贴 10月27日 13:55
16154

awesome

57846d

这个好,👍 💪

11524

只是parse CSV还好。如果处理的内容比较重的话,还是牺牲内存追求时间了。

2575

大点的文件使用 IO 对象应该是最好的做法了,不用操心内存占用。具体场景中 csv 读取后一般就是数据库操作,比如插入数据。这时可以采取一些并发方式写入数据库,进一步优化整个过程,不过那是另一个问题了。

15420

早些个月用了 1G的csv文件做的测试。。。

3035

看完之后马上去检查自己写的rake 果然是 CSV.parse 😂

2203

#12楼 @IChou ++++++++++1 😂 😂 😂 😂 😂 😂 😂 😂

def self.parsed_csv
  csv_text = File.read(Rails.root.join('lib/data', 'initial_xxxxxxx_items.csv'))
  CSV.parse(csv_text, headers: true)
end
96

你好,请问为什么我的CSV.read反而比CSV.parse这种方式要少一个CSV文件左右呢?

28814

貌似直接写循环数组读取也不会有多慢啊,30万行,100M左右,我还要转格式生成大约3倍以上的output,用时也就10多分钟

25402

我觉得,处理大文件时基本原则有两个:

  1. 尽量不要一次性把所有内容都读取到内存中。按需读取,用完就释放。
  2. 尽量使用简单的数据结构。csv 文件的每一行解析为一个 hash 还是 array,在内存占用方面肯定差别很大。
D51b3b

#15楼 @qunlee 还是贴一下主要代码吧, 这么凭空说不准哪儿的问题

17671 easonlovewan Ruby 中读取大文件 中提及了此贴 11月09日 17:00
18898 flemon1986 Ruby 中读取大文件 中提及了此贴 11月09日 19:18
11224

正在处理一个 1G CSV 文件的导入。。正好看到此贴

28931

请问下,如何处理多个维度的csv表格,比如有多行多列 的那种表格

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册