Computer >> Máy Tính >  >> Lập trình >> Ruby

Sử dụng bộ liệt kê lười biếng để làm việc với các tệp lớn trong Ruby

Các điều tra viên là trung tâm của điều khiến Ruby trở thành một ngôn ngữ năng động và mạnh mẽ như vậy. Và những người điều tra lười biếng sẽ tiến thêm một bước nữa bằng cách cho phép bạn làm việc hiệu quả với các bộ sưu tập cực kỳ lớn.

Tệp - hóa ra - chỉ là một bộ sưu tập lớn các dòng hoặc ký tự. Vì vậy, những người điều tra lười biếng có thể thực hiện một số điều rất thú vị và mạnh mẽ với họ.

Dù sao thì Điều tra viên là gì?

Mỗi khi bạn sử dụng một phương pháp như each , bạn tạo một điều tra viên. Đây là lý do mà bạn có thể xâu chuỗi các phương thức lại với nhau như [1,2,3].map { ... }.reduce { ... } . Bạn có thể thấy ý của tôi trong ví dụ dưới đây. Gọi each trả về một điều tra viên mà tôi có thể sử dụng để thực hiện các thao tác lặp đi lặp lại khác.

# I swiped this code from Ruby's documentation https://ruby-doc.org/core-2.2.0/Enumerator.html

enumerator = %w(one two three).each
puts enumerator.class # => Enumerator

enumerator.each_with_object("foo") do |item, obj|
  puts "#{obj}: #{item}"
end

# foo: one
# foo: two
# foo: three

Người liệt kê lười biếng dành cho các bộ sưu tập lớn

Điều tra viên bình thường gặp vấn đề với các bộ sưu tập lớn. Lý do là mỗi phương thức bạn gọi muốn lặp lại trên toàn bộ tập hợp. Bạn có thể tự mình thấy điều này bằng cách chạy mã sau:

# This code will "hang" and you'll have to ctrl-c to exit
(1..Float::INFINITY).reject { |i| i.odd? }.map { |i| i*i }.first(5)

reject phương thức chạy mãi mãi, vì nó không bao giờ có thể hoàn tất việc lặp qua một tập hợp vô hạn.

Nhưng với một bổ sung nhỏ, mã chạy hoàn hảo. Nếu tôi chỉ gọi lazy , Ruby thực hiện điều thông minh và chỉ thực hiện nhiều lần lặp lại nếu cần thiết cho việc tính toán. Đó chỉ là 10 hàng trong trường hợp này, nhỏ hơn đáng kể so với vô cực.

(1..Float::INFINITY).lazy.reject { |i| i.odd? }.map { |i| i*i }.first(5)
#=> [4, 16, 36, 64, 100]

Sáu nghìn bản Moby Dick

Để kiểm tra các thủ thuật tệp này, chúng tôi sẽ cần một tệp lớn. Một con số lớn đến mức hiển nhiên là "lười biếng".

Tôi đã tải xuống Moby Dick từ Project Gutenberg, sau đó tạo một tệp văn bản chứa 100 bản sao. Tuy nhiên, điều đó không đủ lớn. Vì vậy, tôi đã nâng nó lên khoảng 6.000. Điều đó có nghĩa là bây giờ tôi có lẽ là người duy nhất trên thế giới có một tệp văn bản chứa 6.000 bản sao của Moby Dick. Đó là một loại khiêm tốn. Nhưng tôi lạc đề.

Sử dụng bộ liệt kê lười biếng để làm việc với các tệp lớn trong Ruby Tôi đã tải xuống moby dick và sao chép nó vài nghìn lần để có một tệp lớn để chơi. Cú pháp không phải là sai lầm. Đó là vỏ cá. Tôi nghĩ tôi là người duy nhất còn lại sử dụng nó.

Cách lấy các điều tra viên cho một tệp

Đây là một thủ thuật Ruby thú vị mà bạn có thể đã sử dụng, ngay cả khi bạn không biết mình đang sử dụng nó. Hầu như bất kỳ phương thức nào trong Ruby lặp qua một tập hợp sẽ trả về một đối tượng Enumerator cho bạn nếu bạn gọi nó mà không chuyển vào một khối. Điều đó có nghĩa là gì?

Hãy xem xét ví dụ này. Tôi có thể mở một tệp, và sử dụng từng dòng để in ra từng dòng. Nhưng nếu tôi gọi nó mà không có khối, tôi sẽ có một điều tra viên. Các phương pháp được quan tâm là each_line , each_chareach_codepoint .

File.open("moby.txt") do |f|
  # Print out each line in the file
  f.each_line do |l|
    puts l
  end

  # Also prints out each line in the file. But it does it
  # by calling `each` on the Enumerator returned by `each_line`
  f.each_line.each do |l|
    puts l
  end
end

Hai ví dụ này trông gần như giống hệt nhau, nhưng ví dụ thứ hai giữ chìa khóa để mở khóa AMAZING POWERS .

Sử dụng liệt kê của tệp

Khi bạn có một bảng liệt kê "chứa" tất cả các dòng trong một tệp, bạn có thể cắt và chia nhỏ các dòng đó giống như bạn có thể làm với bất kỳ mảng ruby ​​nào. Đây chỉ là một vài ví dụ.

file.each_line.each_with_index.map { |line, i| "Line #{ i }: #{ line }" }[3, 10]
file.each_line.select { |line| line.size == 9 }.first(10)
file.each_line.reject { |line| line.match /whale/i }

Điều này thực sự tuyệt vời, nhưng tất cả các ví dụ này đều có một vấn đề lớn. Tất cả đều tải toàn bộ tệp vào bộ nhớ trước khi lặp lại. Đối với một tệp chứa 6.000 bản sao của Moby Dick, độ trễ là đáng chú ý.

Tải chậm các dòng của tệp

Nếu chúng ta đang quét một tệp văn bản lớn cho 10 trường hợp đầu tiên của từ "cá voi" thì thực sự không cần phải tiếp tục tìm kiếm lần xuất hiện thứ 10. May mắn thay, thật dễ dàng để nói với các điều tra viên của Ruby làm điều này. Bạn chỉ cần sử dụng từ khóa "lười biếng".

Trong các ví dụ dưới đây, chúng tôi tận dụng tính năng tải chậm để thực hiện một số việc khá phức tạp.

File.open("moby.txt") do |f|

  # Get the first 3 lines with the word "whale"
  f.each_line.lazy.select { |line| line.match(/whale/i) }.first(3)

  # Go back to the beginning of the file. 
  f.rewind

  # Prepend the line number to the first three lines
  f.each_line.lazy.each_with_index.map do |line, i| 
    "LINE #{ i }: #{ line }" 
  end.first(3)

  f.rewind

  # Get the first three lines containing "whale" along with their line numbers
  f.each_line.lazy.each_with_index.map { |line, i| "LINE #{ i }: #{ line }" }.select { |line| line.match(/whale/i) }.first(3)

end

Nó không chỉ dành cho các tệp

Các ổ cắm, đường ống, cổng nối tiếp - chúng được biểu diễn trong Ruby bằng cách sử dụng lớp IO. Điều đó có nghĩa là tất cả chúng đều có each_line , each_chareach_codepoint các phương pháp. Vì vậy, bạn có thể sử dụng thủ thuật này cho tất cả chúng. Khá gọn gàng!

Nó không phải là phép thuật

Thật không may, người liệt kê lười biếng chỉ tăng tốc mọi thứ nếu nhiệm vụ bạn đang cố gắng hoàn thành không yêu cầu đọc toàn bộ tệp. Nếu bạn đang tìm kiếm một từ chỉ xuất hiện ở trang cuối cùng của cuốn sách, bạn phải đọc toàn bộ cuốn sách để tìm từ đó. Nhưng trong trường hợp đó, cách tiếp cận này không được chậm hơn cách tiếp cận không điều tra viên.