Processing large CSV files with Ruby
Bài đăng này đã không được cập nhật trong 6 năm
Chắc hẳn là các lập trình viên chúng ta đã rất thân thuộc với các thao tác quen mắt như: CSV.read
, CSV.open
, CSV.foreach
... khi làm việc với các file csv trong RoR Project.
Nhưng chưa hẳn chúng ta đã để ý tới sự khác nhau và việc ảnh hưởng đến bộ nhớ cũng như performance khi sử dụng những thao tác đó.
Thật ra, nếu làm việc với các file csv dữ liệu bé thì giữa các thao tác nêu trên không có sự khác biệt quá lớn. Nhưng nếu làm việc với file csv dữ liệu lớn thì sao???
Trong khi xử lý các tệp lớn là một hoạt động chuyên sâu về bộ nhớ và có thể khiến máy chủ hết bộ nhớ RAM và chuyển sang đĩa.
Ở phạm vi bài viết này chúng ta cùng nhau đi tìm hiểu một vài phương pháp xử lý file csv dung lượng lớn với Ruby và đo mức tiêu thụ bộ nhớ, hiệu suất tốc độ.
Trước khi bắt đầu, ta cùng chuẩn bị một file csv data.csv
với dung lượng thật lớn dữ liệu cỡ 1 triệu dòng (~ 75 MB) như sau:
require "csv"
require_relative "./helpers"
headers = ["id", "name", "email", "city", "street", "country"]
name = "Pink Dream"
email = "pinkdream@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 sử dụng
Chúng ta sẽ cần helpers.rb
xác định hai phương thức trợ giúp để đo và in ra bộ nhớ được sử dụng và thời gian sử dụng như sau:
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
Và. kết quả để tạo tệp CSV là:
$ ruby generate_csv.rb
Time: 5.17
Memory: 1.08 MB
Đầu ra có thể khác nhau giữa các máy, nhưng vấn đề là khi xây dựng tệp CSV, quá trình Ruby không tăng đột biến trong việc sử dụng bộ nhớ vì bộ thu gom rác (GC) đã lấy lại bộ nhớ đã sử dụng. Sự gia tăng bộ nhớ của quá trình này là khoảng 1MB, và nó tạo ra một tập tin CSV với kích thước 75 MB.
Đọc CSV từ một tệp cùng một lúc (CSV.read)
Ta sẽ xây dựng một đối tượng CSV từ một tệp (data.csv) và lặp lại với tập lệnh sau:
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ả là:
$ ruby parse1.rb
Sum: 499999500000
Time: 19.84
Memory: 920.14 MB
Phân tích cú pháp CSV từ trong bộ nhớ chuỗi (CSV.parse)
Xây dựng một đối tượng CSV từ một nội dung trong bộ nhớ như sau:
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ả là:
$ ruby parse1.rb
Sum: 499999500000
Time: 19.84
Memory: 920.14 MB
Chúng ta để ý thấy, ở đây là bộ nhớ lớn tăng đột biến lên 920 MB (thatlavidieu). Điều này xảy ra bởi vì chúng ta vừa xây dựng toàn bộ đối tượng CSV trong bộ nhớ. Chính vì thế mà đã gây ra rất nhiều đối tượng String được tạo bởi thư viện CSV và bộ nhớ đã sử dụng cao hơn nhiều so với kích thước thực tế của tệp CSV.
Phân tích cú pháp dòng CSV theo dòng từ chuỗi trong bộ nhớ (CSV.new)
Bây giờ chúng ta sẽ tải nội dung tập tin trong một String và phân tích nó theo từng dòng để xem điều gì sẽ xảy ra
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ả là:
$ ruby parse3.rb
Sum: 499999500000
Time: 9.73
Memory: 74.64 MB
Từ kết quả chúng ta có thể thấy rằng bộ nhớ được sử dụng cỡ bằng kích thước tệp (75 MB) vì nội dung tệp được tải trong bộ nhớ và thời gian xử lý nhanh hơn gấp hai lần.
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 đọc nó từ một tệp và chỉ muốn lặp lại nó theo từng dòng.
Phân tích cú pháp tệp CSV theo dòng từ đối tượng IO
Liệu có cách nào xử lý tốt hơn so với cách trước? Câu trả lời là: Có, nếu chúng ta có nội dung CSV trong một tệp thì hãy sử dụng một đối tượng tập tin IO trực tiếp:
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
Kết quả là:
$ ruby parse4.rb
Sum: 499999500000
Time: 9.88
Memory: 0.58 MB
Sau khi chạy xong thì 1 MB bộ nhớ tăng lên tuy thời gian có vẻ chậm hơn rất nhiều so với cách làm trước vì có nhiều IO tham gia hơn.
Và thư viện CSV có cơ chế tích hợp cho điều này, đó chính là CSV.foreach
:
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
Kết quả tương tự:
$ ruby parse5.rb
Sum: 499999500000
Time: 9.84
Memory: 0.53 MB
Chúng ta có thể thấy rất rõ kết quả khi dùng CSV.foreach
, không chỉ thời gian xử lý nhanh mà việc sử dụng bộ nhớ cũng được giảm đáng kể
=> Khi các bạn cần cần xử lý các tệp CSV lớn có dung lượng từ 10GB trở lên thì cứ mạnh dạn mà sử dụng cách cuối cùng nhé. foreach
luộn đồng hành cùng bạn =))
Bài viết được dịch và tham khảo từ đây
Thanks for your reading!
All rights reserved