Cách xử lý CSV hiệu quả trong Ruby

Author

Thâm / November 29, 2023

5 min read––– lượt xem

First things first

Hey, bạn Thâm đây

Như tiêu đề bài viết thì trong bài này mình sẽ thử nghiệm các cách để đọc file CSV, rồi chọn ra cách tối ưu nhất ở phương diện tiêu thụ RAM, cũng như tốc độ xử lý. Let’s go

Chuẩn bị file test

require 'csv'
require_relative './helpers'

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

name      = "Tham Davies"
user_name = "boygialaideptraikhongyeuai"
email     = "large-data@csv.com"
city      = "Pleiku"
street    = "Le Duan"
country   = "Viet Nam"

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, user_name, email, city, street, country]
      end
    end
  end
end

Helpers

Tiếp theo, chúng ta sẽ sử dụng Benchmark để đo thời gian và đo lường việc sử dụng bộ nhớ.

  • print_memory_usage sẽ tính toán và in ra lượng bộ nhớ sử dụng trước và sau khi thực hiện các thao tác.
  • print_time_spent sẽ tính toán và in ra thời gian thực hiện các thao tác.
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)} seconds"
end

Sau khi có 2 file trên thì các bạn chạy câu lệnh bên dưới để tiến hành tạo file test

$ ruby generate_csv.rb
Time: 3.61
Memory: 1.67 MB

$ ls -lah data.csv
-rw-r--r--@ 1 tham  tham    85M Nov 22 10:45 data.csv

Kết quả ở trên tuỳ thuộc vào máy, và điểm mấu chốt ở đây là Ruby sử dụng rất ít bộ nhớ (~1.7MB) để generate file CSV có kích thước là 85MB vì cơ chế dọn rác (GC) đã thu hồi lại bộ nhớ đã sử dụng.

Các cách đọc file CSV

1. CSV.read

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

Kết quả:

$ ruby parse1.rb
Sum: 499999500000
Time: 20.12
Memory: 894.5 MB

Qoáo, cách này tiêu tốn 900MB RAM, mất 20S để đọc file, hãy thử tưởng tượng file của bạn tầm 10GB thì sẽ như thế nào 😄

Một số bạn sẽ thắc mắc tại sao file chỉ 85MB mà khi đọc file lại chiếm x10 bộ nhớ RAM?

Lý do là vì chúng ta đã đọc toàn bộ file và lưu đối tượng CSV trong bộ nhớ, khi làm như vậy thư viện CSV tạo ra rất nhiều đối tượng String, nên bộ nhớ sử dụng cao hơn nhiều so với kích thước thực tế của file CSV.

2. CSV.parse

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

Kết quả:

$ ruby parse2.rb
Sum: 499999500000
Time: 14.61
Memory: 1066.35 MB

Cách này thậm chí còn tiêu tốn RAM hơn cách 1, lý do là mình load toàn bộ dữ liệu từ file lưu vào biến (85MB), sau đó mới bắt đầu thực hiện các thao tác như cách 1.

3. CSV.new

Bây giờ chúng ta thử cách load toàn bộ file

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

Kết quả:

$ ruby parse3.rb
Sum: 499999500000
Time: 6.7
Memory: 170.19 MB

Từ kết quả ta thấy lượng bộ nhớ sử dụng x2 kích thước của file (85 MB) vì nội dung file đã được nạp vào bộ nhớ. Thời gian xử lý nhanh hơn gấp đôi. Phương pháp này hữu ích khi chúng ta đã có nội dung mà không cần đọc từ file, chỉ cần duyệt từng dòng. Việc lưu trực tiếp nội dung file vào bộ nhớ cho phép tăng tốc độ xử lý bằng cách loại bỏ quá trình đọc/ghi liên tục từ ổ đĩa.

Cách này được xem là cách tối ưu rồi, nhưng còn cách nào tốt hơn không? Cùng xem ví dụ ở dưới nhé.

4. IO read

Ở đây chúng ta mở file, sau đó đọc từng dòng và parse sang CSV object

require_relative './helpers'
require 'csv'

# cách 1: bằng cơm
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

# cách 2: dùng built-in của Ruby
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

Kết quả:

$ ruby parse4.rb
Sum: 499999500000
Time: 9.57
Memory: 0.72 MB

Á chà chà, cách này tiêu tốn rất ít RAM, nhưng đổi lại là tốc độ xử lý chậm hơn Cách 3 vì nhiều thao tác IO hơn.

Lời kết

Trong bài viết này, chúng ta đã thử nghiệm các cách để đọc file CSV và tìm ra cách tối ưu nhất từ góc nhìn tiêu thụ RAM và tốc độ xử lý. Chúng ta đã thấy rằng cách sử dụng CSV.foreach là cách tiết kiệm RAM nhất, dù tốc độ xử lý có chậm hơn so với cách sử dụng CSV.new. Tuy nhiên, tùy thuộc vào yêu cầu và tình huống cụ thể, chúng ta có thể lựa chọn phương pháp phù hợp để đạt được hiệu suất tối ưu. Nếu chúng ta xử lý file CSV dung lượng lớn thì rõ ràng cách tiêu thụ ít RAM nhất là cách hiệu quả nhất bởi nó sẽ không gây tràn RAM gây crash hệ thống.

Đăng ký nhận thông báo qua email khi có bài viết mới

0 người đăng ký