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.read
và File.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.read
và IO.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.foreach
và IO#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!