Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ภายใต้ประทุน:“Slurping” และสตรีมมิ่งไฟล์ใน Ruby

ใน 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!