12/08/2018, 17:22

Xử lý file CSV dung lượng lớn với Ruby

Việc xử lý file lớn là một hoạt động cần bộ nhớ lớn và có thể khiến máy chủ hết RAM và đổi sang ổ đĩa. Chúng ta cùng xem một số cách xử lý tệp CSV với Ruby sau và cùng xem mức tiêu thụ bộ nhớ và tốc độ thực hiện. 1. Chuẩn bị file CSV mẫu Trước khi bắt đầu, chúng ta cần chuẩn bị một file CSV ...

Việc xử lý file lớn là một hoạt động cần bộ nhớ lớn và có thể khiến máy chủ hết RAM và đổi sang ổ đĩa. Chúng ta cùng xem một số cách xử lý tệp CSV với Ruby sau và cùng xem mức tiêu thụ bộ nhớ và tốc độ thực hiện.

1. Chuẩn bị file CSV mẫu

Trước khi bắt đầu, chúng ta cần chuẩn bị một file CSV data.csv với một triệu bản ghi (~80M) để test.

Ở đây, chúng ta sẽ tạo ra file đó trong user_csv.rb

               require 'csv'
               require_relative './helpers'

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

               name    = "DuongNT"
               email   = "nguyen.thuy.duong@framgia.com"
               city    = "Ha Noi City"
               street  = "Nguyen Trai Road"
               country = "Ha Dong 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

2. Bộ nhớ cần sử dụng và thời gian thực hiện

Để thực hiện được đoạn code trên, ta cần có file helpers.rb định nghĩa 2 helper methods để đo và tính toán bộ nhớ sử dụngvà thời gian thực hiện

            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ả khi chạy user_csv.rb như sau:

$ ruby user_csv.rb
Time: 7.66
Memory: 0.77 MB

Kết quả có thể khác nhau do cấu hình mỗi máy nhưng vấn đề là khi tạo file CSV, quá trình xử lý của Ruby không tăng đột biến việc sử dụng bộ nhớ bởi vì garbage collector (GC) đã lấy lại được dữ liệu cũ đã sử dụng. Sự gia tăng bộ nhớ dùng để process là khoảng 1MB, và nó tạo một file CSV khoảng 86MB. Để kiểm chứng, chúng ta hãy xem:

$:: ls -lah data.csv
-rw-rw-r-- 1 duong duong 86M Mar 22 07:47 data.csv

3. Đọc file CSV từ một file cùng lúc (CSV.read)

Hãy build một object CSV từ file data.csv và iterate với đoạn code 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 sum.rb
Sum: 499999500000
Time: 18.29
Memory: 917.71 MB

ĐIều cần lưu ý ở đây là bộ nhớ lên đến gần 920MB. Lý do là vì chúng ta build toàn bộ object CSV trong bộ nhớ, điều này dẫn đến rất nhiều đối tượng dạng String được tạo ra bởi thư viện CSV và việc sử dụng bộ nhớ quá nhiều so với kích thước thực tế của tập tin.

4. Parse CSV từ bộ nhớ String (CSV.parse)

Giờ thử xem một cách khác nhé: Build một đối tượng CSV từ nội dung trong bộ nhớ có sẵn và iterate với đoạn code 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ả như sau:

$:: ruby sum2.rb
Sum: 499999500000
Time: 18.14
Memory: 1050.32 MB

Bạn có thể thấy, việc bộ nhớ sử dung ở đây tăng là do sự gia tăng bộ nhớ từ ví dụ trước cộng với kích thước bộ nhớ của nội dung tập tin mà chúng ta đọc trong bộ nhớ(86MB)

5. Parse từng dòng của file CSV dạng String từ bộ nhớ (CSV.new)

Chúng ta load nội dung file vào một String và parse nó từng dòng một

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ả như sau:

$:: ruby sum3.rb
Sum: 499999500000
Time: 9.9
Memory: 85.75 MB

Thật đáng ngạc nhiên!

Kết quả trên cho ta thấy , bộ nhớ được sử dụng chỉ khoảng 86MB (xấp xỉ bằng dung lượng file). Điều này là vì nội dung file đưuọc load trong bộ nhớ và thời gian xử lý nhanh gấp 2 lần. Hướng tiếp cận này thực sự rất hữu ích khi mà chúng ta chỉ cần nội dung mà không nhất thiết phải đọc hết một tập tin mà chỉ cần đọc từng dòng một.

6. Parse từng dòng của file CSV từ IO Object

Với cách trên, chúng ta thấy khá là tốt khi mà bộ nhớ sử dụng chỉ bằng với dung lượng của file. Như vậy có thực sự tốt chưa? Có cách nào đó tốt hơn không?

Câu trả lời là: Có! Nếu như chúng ta đã có nội dung của file CSV. Hãy sử dụng một file chứa đường dẫn của các IO object.

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ả như sau:

 $:: ruby sum4.rb
Sum: 499999500000
Time: 11.07
Memory: 0.26 MB

Ở cuối đoạn code trên ta thấy bộ nhớ sử dụng ít hơn 1M nhưng thời gian thì khá chậm so với các cách trước đó bỏi vì có thêm sự tham gia của IO. Thư viện CSV đã xây dựng cơ chế cho nó bằng cách sử dụng CSV.foreach, theo cách như sau:

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

Chúng ta có thể thấy kết quả của cuối cùng chạy như sau:

$:: ruby sum5.rb
Sum: 499999500000
Time: 11.33
Memory: 0.0 MB

Hãy tưởng tượng bạn cần xử lý một file CSV dung lượng lớn khoảng 10GB hoặc hơn thì việc sử dụng cách này là một lựa chọn tuyệt vời!!!

0