12/08/2018, 15:44

Xử lý các file CSV lớn với RUBY

Khi xử lý các file với dữ liệu lớn, hoạt động của server có thể chuyển xử lý từ RAM sang Disk. Bài viết này đưa ra một số cách để xử lý các tệp tin CSV với Ruby nhằm tối ưu mức tiêu thụ bộ nhớ và tốc độ thực thi. Prepare CSV data sample Trước khi bắt đầu, mình chuẩn bị một file CSV data.csv ...

Khi xử lý các file với dữ liệu lớn, hoạt động của server có thể chuyển xử lý từ RAM sang Disk. Bài viết này đưa ra một số cách để xử lý các tệp tin CSV với Ruby nhằm tối ưu mức tiêu thụ bộ nhớ và tốc độ thực thi.

Prepare CSV data sample

Trước khi bắt đầu, mình chuẩn bị một file CSV data.csv với 1000000 rows để test (tương đương với 75MB dữ liệu) .

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

Bộ nhớ được sử dụng và thời gian thực thi

Đoạn code trên sẽ require helpers.rb , helper này sẽ định nghĩa hai helper methods để đo thời gian và in ra bộ nhớ đã sử dụng.

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

Kết quả để tạo ra file CSV là:

$ ruby generate_csv.rb
Time: 5.17
Memory: 1.08 MB

Output có thể khác nhau tùy theo phần cứng, nhưng vấn đề là khi xây dựng tệp tin CSV, tiến trình Ruby xử lý không tăng đột biến trong sử dụng bộ nhớ vì trình thu gom rác (GC) đã lấy lại bộ nhớ đã sử dụng. Bộ nhớ của tiến trình tăng khoảng 1MB và nó tạo ra một tập tin lớn với kích thước 75MB.

$ ls -lah data.csv
-rw-rw-r-- 1 dalibor dalibor 75M Mar 29 00:34 data.csv

Đọc file CSV

Chúng ta sẽ đọc file CSV và lặp qua từng row để get dữ liệu:

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

Và đây là kết quả:

$ ruby parse1.rb
Sum: 499999500000
Time: 19.84
Memory: 920.14 MB

Một lưu ý quan trọng ở đây là bộ nhớ tăng đột biến lên 920MB. Đó là bởi vì chúng ta xây dựng toàn bộ đối tượng CSV trong bộ nhớ. Điều đó tạo ra rất nhiều đối tượng String được tạo ra bởi thư viện CSV và bộ nhớ được sử dụng cao hơn nhiều so với kích thước thực tế của file data.csv

Đọc file CSV từ bộ nhớ

Build một đối tượng CSV từ một nội dung trong bộ nhớ và lặp để đọc nó.

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

Và đây là kết quả:

$ ruby parse2.rb
Sum: 499999500000
Time: 21.71
Memory: 1003.69 MB

Từ ví dụ có thể thấy việc tăng bộ nhớ sử dụng từ: 920.14 MB lên 1003.69 MB là tăng kích thước file mà chúng ta đọc trong bộ nhớ.

Đọc từng row từ String trong bộ nhớ

Hãy cùng xem điều gì sẽ xảy ra khi chúng ta load nội dung trong một String và parse nó từng row

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

Và đây là kết quả:

$ ruby parse3.rb
Sum: 499999500000
Time: 9.73
Memory: 74.64 MB

Từ kết quả, chúng ta có thể thấy bộ nhớ được sử dụng là kích thước file (75 MB). Nội dung tệp được tải trong bộ nhớ và thời gian xử lý nhanh gấp đôi. Cách tiếp cận này rất hữu ích khi chúng ta có nội dung mà chúng ta không cần phải đọc nó từ một tệp tin và chúng ta chỉ muốn lặp qua nó theo từng dòng.

Đọc file CSV từ IO object

Liệu có cách nào tốt hơn ví dụ trên. Nếu chúng ta có nội dung CSV trong một file, hãy sử dụng trực tiếp đối tượng IO

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

Và đây là kết quả:

$ ruby parse4.rb
Sum: 499999500000
Time: 9.88
Memory: 0.58 MB

Nếu bạn cần phải xử lý file CSV lớn cỡ GB trở lên thì lựa chọn cuối cùng này có vẻ là điều hiển nhiên.

Nguồn: https://dalibornasevic.com/posts/68-processing-large-csv-files-with-ruby

0