ใน Ruby Magic ฉบับนี้ เราจะเรียนรู้เกี่ยวกับการสตรีมไฟล์ใน Ruby ว่า IO
เป็นอย่างไร คลาสจัดการการอ่านไฟล์โดยไม่ต้องโหลดลงในหน่วยความจำทั้งหมด และวิธีอ่านไฟล์ต่อบรรทัดด้วยการบัฟเฟอร์การอ่านไบต์ มาดำดิ่งกันเลย!
“Slurping” และไฟล์สตรีมมิ่ง
File.read
ของ Ruby วิธีอ่านไฟล์และส่งคืนเนื้อหาทั้งหมด
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"
ภายใน การดำเนินการนี้จะเปิดไฟล์ อ่านเนื้อหา ปิดไฟล์ และส่งคืนเนื้อหาเป็นสตริงเดียว โดย "slurping" เนื้อหาของไฟล์ในครั้งเดียว ไฟล์จะถูกเก็บไว้ในหน่วยความจำจนกว่าเครื่องมือรวบรวมขยะของ Ruby จะล้างข้อมูล
ตัวอย่างเช่น สมมติว่าเราต้องการตัวพิมพ์ใหญ่อักขระทั้งหมดในไฟล์และเขียนไปยังไฟล์อื่น การใช้ File.read
เราสามารถรับเนื้อหาได้ โทร String#upcase
บนสตริงผลลัพธ์ และส่งสตริงตัวพิมพ์ใหญ่ไปที่ 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
แม้ว่าจะใช้ได้กับไฟล์ขนาดเล็ก แต่การอ่านไฟล์ทั้งหมดลงในหน่วยความจำอาจมีปัญหาเมื่อต้องจัดการกับไฟล์ขนาดใหญ่ ตัวอย่างเช่น เมื่อแยกวิเคราะห์ล็อกไฟล์ขนาด 14 กิกะไบต์ การอ่านไฟล์ทั้งหมดในครั้งเดียวจะเป็นการดำเนินการที่มีราคาแพง เนื้อหาของไฟล์ถูกเก็บไว้ในหน่วยความจำ ดังนั้นหน่วยความจำของแอปจึงเพิ่มขึ้นอย่างมาก ซึ่งอาจนำไปสู่การสลับหน่วยความจำและระบบปฏิบัติการฆ่ากระบวนการของแอป
โชคดีที่ Ruby อนุญาตให้อ่านไฟล์ทีละบรรทัดโดยใช้ File.foreach
. แทนที่จะอ่านเนื้อหาทั้งหมดของไฟล์ในคราวเดียว ไฟล์จะดำเนินการบล็อกที่ส่งผ่านสำหรับแต่ละบรรทัด
ผลลัพธ์ของมันสามารถนับได้ ดังนั้นมันจึงให้ผลบล็อกสำหรับแต่ละบรรทัด หรือส่งคืนอ็อบเจ็กต์ Enumerator หากไม่มีการส่งผ่านบล็อก ซึ่งช่วยให้อ่านไฟล์ขนาดใหญ่ได้โดยไม่ต้องโหลดเนื้อหาทั้งหมดลงในหน่วยความจำในครั้งเดียว
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"
หากต้องการพิมพ์ใหญ่ทั้งไฟล์ เราอ่านจากไฟล์อินพุตทีละบรรทัด ตัวพิมพ์ใหญ่ และต่อท้ายไฟล์เอาต์พุต
irb> File.open("upcased.log", "a") do |output|
irb* File.foreach("production.log") { |line| output.write(line.upcase) }
irb> end
=> nil
ดังนั้นการอ่านไฟล์ทีละบรรทัดทำงานอย่างไรโดยไม่ต้องอ่านไฟล์ทั้งหมดก่อน เพื่อให้เข้าใจว่า เราจะต้องลอกเลเยอร์บางส่วนรอบๆ การอ่านไฟล์ออก มาดู IO
. ของ Ruby กันดีกว่า ชั้นเรียน
I/O และ IO
ของ Ruby และ Ruby คลาส
แม้ว่า File.read
และ File.foreach
มีอยู่ เอกสารประกอบสำหรับ File
ชั้นเรียนไม่แสดงรายการ อันที่จริง คุณจะไม่พบวิธีการอ่านหรือเขียนไฟล์ใดๆ ใน File
เอกสารประกอบของคลาส เพราะมันสืบทอดมาจากพาเรนต์ IO
ชั้นเรียน
I/O
อุปกรณ์ I/O เป็นอุปกรณ์ที่ถ่ายโอนข้อมูลไปยังหรือจากคอมพิวเตอร์ เช่น แป้นพิมพ์ จอภาพ และฮาร์ดไดรฟ์ มันดำเนินการ อินพุต/เอาต์พุต , หรือ I/O โดยการอ่านหรือสร้างกระแสข้อมูล
การอ่านและเขียนไฟล์จากฮาร์ดไดรฟ์เป็น I/O ทั่วไปที่คุณจะพบ I/O ประเภทอื่นๆ ได้แก่ การสื่อสารแบบซ็อกเก็ต การบันทึกเอาต์พุตไปยังเทอร์มินัล และอินพุตจากแป้นพิมพ์
IO
คลาสใน Ruby จัดการอินพุตและเอาต์พุตทั้งหมด เช่น การอ่านและการเขียนไปยังไฟล์ เนื่องจากการอ่านไฟล์ไม่แตกต่างจากการอ่านจากสตรีม I/O อื่นๆ File
class สืบทอดเมธอดโดยตรง เช่น IO.read
และ 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
เทียบเท่ากับ IO.foreach
ดังนั้น IO
เวอร์ชันคลาสสามารถใช้เพื่อให้ได้ผลลัพธ์แบบเดียวกับที่เราทำก่อนหน้านี้
การอ่านสตรีม I/O ผ่านเคอร์เนล
ภายใน IO
. ของ Ruby ความสามารถในการอ่านและเขียนของคลาสนั้นอิงตามนามธรรมเกี่ยวกับการเรียกระบบเคอร์เนล เคอร์เนลของระบบปฏิบัติการดูแลการอ่านและเขียนข้อมูลไปยังอุปกรณ์ I/O
การเปิดไฟล์
IO.sysopen
เปิดไฟล์โดยขอให้เคอร์เนลอ้างอิงถึงไฟล์ในตารางไฟล์ และสร้าง file descriptor ในตาราง file descriptor ของกระบวนการ
ตัวอธิบายไฟล์และตารางไฟล์
การเปิดไฟล์จะส่งคืน file descriptor — จำนวนเต็มที่ใช้ในการเข้าถึงทรัพยากร I/O
แต่ละกระบวนการมีตาราง file descriptor ของตัวเองเพื่อเก็บ file descriptor ไว้ในหน่วยความจำ และ descriptor แต่ละตัวจะชี้ไปที่รายการใน file table ทั่วทั้งระบบ .
เมื่อต้องการอ่านหรือเขียนทรัพยากร I/O กระบวนการจะส่ง file descriptor ไปยังเคอร์เนลผ่านการเรียกของระบบ จากนั้นเคอร์เนลจะเข้าถึงไฟล์ในนามของกระบวนการ เนื่องจากกระบวนการไม่สามารถเข้าถึงตารางไฟล์ได้
การเปิดไฟล์จะ ไม่ เก็บเนื้อหาไว้ในหน่วยความจำ แต่ตารางตัวอธิบายไฟล์สามารถเต็มได้ ดังนั้นจึงควรปิดไฟล์ทุกครั้งหลังจากเปิดไฟล์ วิธีการที่ห่อ File.open
เช่น File.read
ทำสิ่งนี้โดยอัตโนมัติเช่นเดียวกับที่บล็อก
ในตัวอย่างนี้ เราจะก้าวไปอีกขั้นโดยการเรียก IO.sysopen
วิธีการโดยตรง เมื่อส่งชื่อไฟล์ เมธอดจะสร้าง file descriptor ที่เราสามารถใช้อ้างอิงไฟล์ที่เปิดได้ในภายหลัง
irb> IO.sysopen("log/production.log")
=> 9
การสร้าง IO
เช่น ให้ Ruby อ่านและเขียนถึง เราส่ง file descriptor ไปที่ IO.new
irb> file_descriptor = IO.sysopen("log/production.log")
=> 9
irb> io = IO.new(file_descriptor)
=> #<IO:fd 9>
ในการปิดสตรีม I/O และลบการอ้างอิงไปยังไฟล์จากตารางไฟล์ เราเรียก IO#close
บน IO
ตัวอย่าง
irb> io.close
=> nil
การอ่านไบต์และการย้ายเคอร์เซอร์
IO#sysread
อ่านจำนวนไบต์จาก IO
วัตถุ
irb> io.sysread(64)
=> " [86a5d18c-19dd-4cbf-9d7a-461c79e98c22] Started GET \"/articles\" "
ตัวอย่างนี้ใช้ IO
ตัวอย่างที่เราสร้างไว้ก่อนหน้านี้โดยส่ง file descriptor integer ไปที่ IO.new
. มันอ่านและส่งคืน 64 ไบต์แรกจากไฟล์โดยเรียก IO#sysread
โดยมี 64 เป็นอาร์กิวเมนต์
irb> io.sysread(64)
=> "for 127.0.0.1 at 2018-06-27 16:45:02 +0200\nI, [2018-06-27T16:45:"
ครั้งแรกที่เราขอไบต์จากไฟล์ เคอร์เซอร์ถูกย้ายโดยอัตโนมัติ ดังนั้นจึงเรียก IO#sysread
ในอินสแตนซ์เดียวกันอีกครั้งจะสร้างไฟล์ 64 ไบต์ถัดไป
การย้ายเคอร์เซอร์
IO.sysseek
ย้ายเคอร์เซอร์ไปยังตำแหน่งในไฟล์ด้วยตนเอง
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\" "
ในตัวอย่างนี้ เราย้ายไปที่ตำแหน่ง 32 จากนั้นอ่าน 64 ไบต์โดยใช้ IO#sysread
. โดยเรียก IO.sysseek
อีกครั้งด้วย 0 เราจะย้อนกลับไปที่จุดเริ่มต้นของไฟล์ ทำให้เราอ่าน 64 ไบต์แรกได้อีกครั้ง
การอ่านไฟล์ทีละบรรทัด
ตอนนี้เรารู้แล้วว่า IO
วิธีอำนวยความสะดวกของคลาสเปิดสตรีม IO อ่านไบต์จากพวกเขาและวิธีที่พวกเขาย้ายตำแหน่งของเคอร์เซอร์
วิธีการเช่น IO.foreach
และ IO#gets
สามารถขอบรรทัดทีละบรรทัดแทนต่อจำนวนไบต์ ไม่มีทางที่มีประสิทธิภาพในการมองไปข้างหน้าเพื่อค้นหาบรรทัดใหม่ถัดไปและใช้ไบต์ทั้งหมดจนถึงตำแหน่งนั้น ดังนั้น Ruby จึงต้องดูแลการแยกเนื้อหาของไฟล์
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
ในการใช้งานตัวอย่างนี้ #each
วิธีรับไบต์จากไฟล์โดยใช้ IO#sysread
ทีละครั้งจนกว่าไบต์จะเป็น $/
แสดงว่าขึ้นบรรทัดใหม่ เมื่อพบบรรทัดใหม่ จะหยุดรับไบต์และเรียกบล็อกที่ส่งผ่านด้วยบรรทัดนั้น
โซลูชันนี้ใช้งานได้แต่ไม่มีประสิทธิภาพเนื่องจากเรียก IO.sysread
สำหรับทุกไบต์ในไฟล์
เนื้อหาไฟล์บัฟเฟอร์
Ruby ฉลาดกว่าด้วยการรักษาบัฟเฟอร์ภายในของเนื้อหาของไฟล์ แทนที่จะอ่านไฟล์ครั้งละหนึ่งไบต์ ไฟล์จะใช้เวลา 512 ไบต์ในครั้งเดียว และตรวจสอบว่ามีการขึ้นบรรทัดใหม่ในไบต์ที่ส่งคืนหรือไม่ หากมี จะส่งคืนส่วนก่อนขึ้นบรรทัดใหม่และเก็บส่วนที่เหลือไว้ในหน่วยความจำเป็นบัฟเฟอร์ หากบัฟเฟอร์ไม่มีการขึ้นบรรทัดใหม่ บัฟเฟอร์จะดึงข้อมูลเพิ่มอีก 512 ไบต์จนกว่าจะพบ
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
ในตัวอย่างนี้ #each
วิธีการเพิ่มไบต์ให้กับ @buffer
ภายใน ตัวแปรเป็นส่วนๆ 512 ไบต์จนกระทั่งเมื่อ @buffer
ตัวแปรรวมถึงการขึ้นบรรทัดใหม่ เมื่อสิ่งนั้นเกิดขึ้น มันจะแยกบัฟเฟอร์ด้วยการขึ้นบรรทัดใหม่แรก ส่วนแรกคือ line
และส่วนที่สองคือบัฟเฟอร์ใหม่
บล็อกที่ส่งผ่านจะถูกเรียกด้วยบรรทัดและส่วนที่เหลือ @buffer
เก็บไว้ใช้ในรอบต่อไป
โดยการบัฟเฟอร์เนื้อหาของไฟล์ จำนวนการเรียก I/O จะลดลงในขณะที่แบ่งไฟล์ออกเป็นส่วน ๆ ทางตรรกะ
การสตรีมไฟล์
สรุป ไฟล์สตรีมมิ่งทำงานโดยขอให้เคอร์เนลของระบบปฏิบัติการเปิดไฟล์ จากนั้นอ่านไบต์จากไฟล์ทีละบิต เมื่ออ่านไฟล์ต่อบรรทัดใน Ruby ข้อมูลจะถูกดึงจากไฟล์ครั้งละ 512 ไบต์และแยกออกเป็น "บรรทัด" หลังจากนั้น
สรุปภาพรวมของ I/O และไฟล์สตรีมมิ่งใน Ruby เราอยากทราบว่าคุณคิดอย่างไรกับบทความนี้ หรือหากคุณมีคำถามใดๆ เราคอยมองหาหัวข้อที่จะตรวจสอบและอธิบายอยู่เสมอ ดังนั้นหากคุณต้องการอ่านเรื่องมหัศจรรย์ใน Ruby อย่าลังเลที่จะแจ้งให้เราทราบที่ @AppSignal!