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

Nâng cao:“Slurping” và truyền trực tuyến tệp trong Ruby

Trong ấn bản này của Ruby Magic, chúng ta sẽ tìm hiểu về cách phát trực tuyến các tệp trong Ruby, cách IO lớp xử lý việc đọc các tệp mà không cần tải hoàn toàn chúng vào bộ nhớ và cách nó đọc các tệp trên mỗi dòng bằng cách đệm các byte đã đọc. Hãy đi sâu vào ngay!

Tệp “Slurping” và Truyền trực tuyến

File.read của Ruby phương thức đọc một tệp và trả về nội dung đầy đủ của nó.

irb> content = File.read("log/production.log")
=> "I, [2018-06-27T16:45:02.843719 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"/articles\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:02.846719 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\nI, [2018-06-27T16:45:02.848212 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Rendering articles/index.html.erb within layouts/application\nD, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Article Load (0.3ms)  SELECT \"articles\".* FROM \"articles\"\nI, [2018-06-27T16:45:02.850901 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Rendered articles/index.html.erb within layouts/application (1.7ms)\nI, [2018-06-27T16:45:02.851633 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\n"

Bên trong, thao tác này sẽ mở tệp, đọc nội dung của nó, đóng tệp và trả về nội dung dưới dạng một chuỗi đơn. Bằng cách "đọc nhanh" nội dung của tệp ngay lập tức, tệp sẽ được lưu trong bộ nhớ cho đến khi được bộ thu gom rác của Ruby dọn dẹp.

Ví dụ:giả sử chúng tôi muốn viết hoa tất cả các ký tự trong một tệp và ghi nó vào một tệp khác. Sử dụng File.read , chúng ta có thể lấy nội dung, gọi String#upcase trên chuỗi kết quả và chuyển chuỗi viết hoa thành File.write .

irb> upcased = File.read("log/production.log").upcase
=> "I, [2018-06-27T16:45:02.843719 #9098]  INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] STARTED GET \"/ARTICLES\" FOR 127.0.0.1 AT 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:02.846719 #9098]  INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] PROCESSING BY ARTICLESCONTROLLER#INDEX AS HTML\nI, [2018-06-27T16:45:02.848212 #9098]  INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22]   RENDERING ARTICLES/INDEX.HTML.ERB WITHIN LAYOUTS/APPLICATION\nD, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22]   ARTICLE LOAD (0.3MS)  SELECT \"ARTICLES\".* FROM \"ARTICLES\"\nI, [2018-06-27T16:45:02.850901 #9098]  INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22]   RENDERED ARTICLES/INDEX.HTML.ERB WITHIN LAYOUTS/APPLICATION (1.7MS)\nI, [2018-06-27T16:45:02.851633 #9098]  INFO -- : [86A5D18C-19DD-4CBF-9D7A-461C79E98C22] COMPLETED 200 OK IN 5MS (VIEWS: 3.4MS | ACTIVERECORD: 0.3MS)\n"
irb> File.write("log/upcased.log", upcased)
=> 896

Mặc dù điều đó hoạt động đối với các tệp nhỏ, nhưng việc đọc toàn bộ tệp vào bộ nhớ có thể gặp vấn đề khi xử lý các tệp lớn hơn. Ví dụ:khi phân tích cú pháp tệp nhật ký 14 gigabyte, đọc toàn bộ tệp cùng một lúc sẽ là một thao tác tốn kém. Nội dung của tệp được lưu trong bộ nhớ, vì vậy bộ nhớ của ứng dụng tăng lên đáng kể. Điều này cuối cùng có thể dẫn đến hoán đổi bộ nhớ và hệ điều hành giết chết quy trình của ứng dụng.

May mắn thay, Ruby cho phép đọc từng dòng tệp bằng File.foreach . Thay vì đọc toàn bộ nội dung của tệp cùng một lúc, nó sẽ thực thi một khối được truyền cho mỗi dòng.

Kết quả của nó là có thể liệt kê được, do đó, nó cho ra một khối cho mỗi dòng, hoặc trả về một đối tượng Enumerator nếu không có khối nào được chuyển qua. Điều này cho phép đọc các tệp lớn hơn mà không phải tải tất cả nội dung của chúng vào bộ nhớ cùng một lúc.

irb> File.foreach("log/production.log") { |line| p line }
"I, [2018-06-27T16:45:02.843719 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"/articles\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\n"
"I, [2018-06-27T16:45:02.846719 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\n"
"I, [2018-06-27T16:45:02.848212 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Rendering articles/index.html.erb within layouts/application\n"
"D, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Article Load (0.3ms)  SELECT \"articles\".* FROM \"articles\"\n"
"I, [2018-06-27T16:45:02.850901 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Rendered articles/index.html.erb within layouts/application (1.7ms)\n"
"I, [2018-06-27T16:45:02.851633 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\n"

Để viết hoa toàn bộ tệp, chúng tôi đọc từng dòng từ tệp đầu vào, viết hoa và nối tệp đó vào tệp đầu ra.

irb> File.open("upcased.log", "a") do |output|
irb*   File.foreach("production.log") { |line| output.write(line.upcase) }
irb> end
=> nil

Vì vậy, làm thế nào để đọc một tệp từng dòng mà không cần phải đọc toàn bộ tệp trước tiên? Để hiểu điều đó, chúng tôi sẽ phải bóc lại một số lớp xung quanh việc đọc tệp. Chúng ta hãy xem xét kỹ hơn IO của Ruby lớp học.

I / O và IO của Ruby Lớp học

Mặc dù File.readFile.foreach tồn tại, tài liệu cho File lớp không liệt kê chúng. Trên thực tế, bạn sẽ không tìm thấy bất kỳ phương pháp đọc hoặc ghi tệp nào trong File tài liệu lớp, vì chúng được kế thừa từ IO cha lớp học.

I / O

Một thiết bị I / O là một thiết bị truyền dữ liệu đến hoặc từ máy tính, ví dụ như bàn phím, màn hình và ổ cứng. Nó thực hiện Đầu vào / Đầu ra hoặc I / O , bằng cách đọc hoặc tạo ra các luồng dữ liệu.

Đọc và ghi tệp từ ổ cứng là I / O phổ biến nhất mà bạn sẽ gặp phải. Các loại I / O khác bao gồm giao tiếp ổ cắm, đầu ra ghi nhật ký vào thiết bị đầu cuối của bạn và đầu vào từ bàn phím của bạn.

IO lớp trong Ruby xử lý tất cả đầu vào và đầu ra như đọc và ghi vào tệp. Vì đọc tệp không khác với đọc từ bất kỳ luồng I / O nào khác, nên File lớp kế thừa trực tiếp các phương thức như IO.readIO.foreach .

irb> IO.foreach("log/production.log") { |line| p line }
"I, [2018-06-27T16:45:02.843719 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"/articles\" for 127.0.0.1 at 2018-06-27 16:45:02 +0200\n"
"I, [2018-06-27T16:45:02.846719 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Processing by ArticlesController#index as HTML\n"
"I, [2018-06-27T16:45:02.848212 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Rendering articles/index.html.erb within layouts/application\n"
"D, [2018-06-27T16:45:02.850020 #9098] DEBUG -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Article Load (0.3ms)  SELECT \"articles\".* FROM \"articles\"\n"
"I, [2018-06-27T16:45:02.850901 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22]   Rendered articles/index.html.erb within layouts/application (1.7ms)\n"
"I, [2018-06-27T16:45:02.851633 #9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Completed 200 OK in 5ms (Views: 3.4ms | ActiveRecord: 0.3ms)\n"

File.foreach tương đương với IO.foreach , vì vậy IO phiên bản lớp có thể được sử dụng để có được kết quả tương tự như chúng tôi đã làm trước đó.

Đọc luồng I / O thông qua nhân

Nội bộ, IO của Ruby khả năng đọc và ghi của lớp dựa trên sự trừu tượng xung quanh các lời gọi hệ thống hạt nhân. Hạt nhân của hệ điều hành đảm nhiệm việc đọc và ghi vào các thiết bị I / O.

Mở tệp

IO.sysopen mở tệp bằng cách yêu cầu hạt nhân đặt tham chiếu đến tệp trong bảng tệp và tạo bộ mô tả tệp trong bảng bộ mô tả tệp của tiến trình.

Trình mô tả Tệp và Bảng Tệp

Mở tệp sẽ trả về bộ mô tả tệp - một số nguyên được sử dụng để truy cập tài nguyên I / O.

Mỗi quy trình có bảng bộ mô tả tệp riêng để giữ các bộ mô tả tệp trong bộ nhớ và mỗi bộ mô tả trỏ đến một mục nhập trong bảng tệp trên toàn hệ thống .

Để đọc hoặc ghi vào một tài nguyên I / O, quá trình chuyển bộ mô tả tệp tới hạt nhân thông qua một lệnh gọi hệ thống. Sau đó, hạt nhân sẽ truy cập tệp thay mặt cho quá trình, vì các quá trình không có quyền truy cập vào bảng tệp.

Mở tệp sẽ không giữ nội dung của chúng trong bộ nhớ nhưng bảng mô tả tệp có thể bị lấp đầy, vì vậy, bạn nên luôn đóng tệp sau khi mở. Các phương thức bao bọc File.open như File.read làm điều này tự động, cũng như những cái lấy một khối.

Trong ví dụ này, chúng ta sẽ tiến thêm một bước nữa bằng cách gọi IO.sysopen phương pháp trực tiếp. Bằng cách chuyển tên tệp, phương thức này sẽ tạo một bộ mô tả tệp mà chúng ta có thể sử dụng để tham chiếu tệp đang mở sau này.

irb> IO.sysopen("log/production.log")
=> 9

Để tạo IO ví dụ để Ruby đọc và ghi vào, chúng tôi chuyển bộ mô tả tệp vào IO.new

irb> file_descriptor = IO.sysopen("log/production.log")
=> 9
irb> io = IO.new(file_descriptor)
=> #<IO:fd 9>

Để đóng luồng I / O và xóa tham chiếu đến tệp khỏi bảng tệp, chúng tôi gọi IO#close trên IO ví dụ.

irb> io.close
=> nil

Đọc byte và di chuyển con trỏ

IO#sysread đọc một số byte từ IO đối tượng.

irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"/articles\" "

Ví dụ này sử dụng IO ví dụ mà chúng tôi đã tạo trước đó bằng cách chuyển số nguyên của bộ mô tả tệp tới IO.new . Nó đọc và trả về 64 byte đầu tiên từ tệp bằng cách gọi IO#sysread với 64 là đối số của nó.

irb> io.sysread(64)
=> "for 127.0.0.1 at 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:"

Lần đầu tiên chúng tôi yêu cầu byte từ tệp, con trỏ đã được di chuyển tự động, vì vậy hãy gọi IO#sysread trên cùng một phiên bản sẽ tạo ra 64 byte tiếp theo của tệp.

Di chuyển con trỏ

IO.sysseek di chuyển con trỏ đến một vị trí trong tệp theo cách thủ công.

irb> io.sysseek(32)
=> 32
irb> io.sysread(64)
=> "9098]  INFO -- : [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started "
irb> io.sysseek(0)
=> 0
irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"/articles\" "

Trong ví dụ này, chúng tôi di chuyển đến vị trí 32, sau đó đọc 64 byte bằng cách sử dụng IO#sysread . Bằng cách gọi IO.sysseek một lần nữa với 0, chúng tôi quay trở lại đầu tệp, cho phép chúng tôi đọc lại 64 byte đầu tiên.

Đọc từng dòng một tệp

Bây giờ, chúng ta biết cách IO các phương thức tiện lợi của lớp mở các luồng IO, đọc các byte từ chúng và cách chúng di chuyển vị trí của con trỏ.

Các phương thức như IO.foreachIO#gets có thể yêu cầu từng dòng từng dòng thay vì theo số byte. Không có cách nào hiệu quả khi nhìn về phía trước để tìm dòng mới tiếp theo và lấy tất cả các byte cho đến vị trí đó, vì vậy Ruby cần phải cẩn thận trong việc chia nhỏ nội dung của tệp.

class MyIO
  def initialize(filename)
    fd = IO.sysopen(filename)
    @io = IO.new(fd)
  end
 
  def each(&block)
    line = ""
 
    while (c = @io.sysread(1)) != $/
      line << c
    end
 
    block.call(line)
    each(&block)
  rescue EOFError
    @io.close
  end
end

Trong triển khai ví dụ này, #each phương thức lấy byte từ tệp bằng IO#sysread từng cái một, cho đến khi byte là $/ , cho biết một dòng mới. Khi nó tìm thấy một dòng mới, nó sẽ ngừng lấy byte và gọi khối đã truyền bằng dòng đó.

Giải pháp này hoạt động nhưng không hiệu quả vì nó gọi IO.sysread cho mỗi byte trong tệp.

Nội dung tệp đệm

Ruby thông minh hơn về cách nó thực hiện điều này bằng cách giữ một bộ đệm bên trong nội dung của tệp. Thay vì đọc tệp từng byte một, nó cần 512 byte cùng một lúc và kiểm tra xem có bất kỳ dòng mới nào trong các byte trả về hay không. Nếu có, nó trả về phần trước dòng mới và giữ phần còn lại trong bộ nhớ làm bộ đệm. Nếu bộ đệm không bao gồm một dòng mới, nó sẽ tìm nạp thêm 512 byte cho đến khi tìm thấy một dòng.

class MyIO
  def initialize(filename)
    fd = IO.sysopen(filename)
    @io = IO.new(fd)
    @buffer = ""
  end
 
  def each(&block)
    @buffer << @io.sysread(512) until @buffer.include?($/)
 
    line, @buffer = @buffer.split($/, 2)
 
    block.call(line)
    each(&block)
  rescue EOFError
    @io.close
  end
end

Trong ví dụ này, #each phương thức thêm byte vào @buffer nội bộ biến theo phần 512 byte cho đến khi @buffer biến bao gồm một dòng mới. Khi điều đó xảy ra, nó sẽ chia bộ đệm theo dòng mới đầu tiên. Phần đầu tiên là dòng line và phần thứ hai là bộ đệm mới.

Khối đã qua sau đó được gọi với dòng và @buffer còn lại được giữ lại để sử dụng trong vòng lặp tiếp theo.

Bằng cách lưu vào bộ đệm nội dung của tệp, số lượng lệnh gọi I / O sẽ giảm trong khi phân chia tệp thành các phần hợp lý.

Tệp truyền trực tuyến

Tóm lại, các tệp phát trực tuyến hoạt động bằng cách yêu cầu hạt nhân của hệ điều hành mở một tệp, sau đó đọc từng byte từ tệp đó. Khi đọc tệp trên mỗi dòng trong Ruby, dữ liệu được lấy từ tệp 512 byte tại một thời điểm và được chia thành "dòng" sau đó.

Phần này kết thúc tổng quan của chúng tôi về I / O và các tệp phát trực tuyến trong Ruby. Chúng tôi muốn biết bạn nghĩ gì về bài viết này hoặc nếu bạn có bất kỳ câu hỏi nào. Chúng tôi luôn theo dõi các chủ đề để điều tra và giải thích, vì vậy nếu có điều gì kỳ diệu trong Ruby mà bạn muốn đọc, đừng ngần ngại cho chúng tôi ngay tại @AppSignal!