Xử lý các file CSV lớn với RUBY
Bài đăng này đã không được cập nhật trong 3 năm
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
All rights reserved