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

การใช้ตัวแจงนับขี้เกียจเพื่อทำงานกับไฟล์ขนาดใหญ่ใน Ruby

ตัวแจงนับเป็นหัวใจสำคัญของสิ่งที่ทำให้ Ruby เป็นภาษาที่ทรงพลังและมีพลัง และตัวแจงนับขี้เกียจก็ก้าวไปอีกขั้นโดยช่วยให้คุณทำงานกับคอลเล็กชันขนาดใหญ่มากได้อย่างมีประสิทธิภาพ

ไฟล์ - ปรากฎ - เป็นเพียงชุดของบรรทัดหรืออักขระจำนวนมาก ตัวแจงนับที่เกียจคร้านทำให้สามารถทำสิ่งที่น่าสนใจและทรงพลังกับพวกเขาได้

Enumerator คืออะไร

ทุกครั้งที่คุณใช้วิธีการเช่น each คุณสร้างตัวแจงนับ นี่คือเหตุผลที่คุณสามารถเชื่อมโยงวิธีการต่างๆ เช่น [1,2,3].map { ... }.reduce { ... } . คุณสามารถดูสิ่งที่ฉันหมายถึงในตัวอย่างด้านล่าง โทร each ส่งคืนตัวแจงนับ ซึ่งฉันสามารถใช้เพื่อดำเนินการวนซ้ำอื่น ๆ ได้

# 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

Lazy-enumerators-are-for-large-collections">Lazy-enumerators สำหรับคอลเลกชันขนาดใหญ่

ตัวแจงนับปกติมีปัญหากับคอลเลกชันขนาดใหญ่ เหตุผลก็คือแต่ละวิธีที่คุณเรียกใช้ต้องการทำซ้ำในคอลเล็กชันทั้งหมด คุณสามารถดูสิ่งนี้ได้ด้วยตัวเองโดยเรียกใช้รหัสต่อไปนี้:

# 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 เมธอดจะทำงานตลอดไป เพราะมันไม่สามารถวนซ้ำในคอลเล็กชันที่ไม่มีที่สิ้นสุดได้

แต่ด้วยการเพิ่มเล็กน้อยโค้ดจึงทำงานได้อย่างสมบูรณ์ ถ้าฉันเรียกแค่ว่า lazy วิธี Ruby ทำสิ่งที่ฉลาดและทำซ้ำได้มากเท่าที่จำเป็นสำหรับการคำนวณ ในกรณีนี้มีเพียง 10 แถว ซึ่งเล็กกว่าอินฟินิตี้อย่างมาก

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

Moby Dick จำนวนหกพันชุด

หากต้องการทดสอบกลเม็ดไฟล์เหล่านี้ เราจำเป็นต้องมีไฟล์ขนาดใหญ่ เรื่องใหญ่ที่ "ความล้มเหลวในการขี้เกียจ" ใด ๆ จะปรากฏชัด

ฉันดาวน์โหลด Moby Dick จาก Project Gutenberg แล้วสร้างไฟล์ข้อความที่มี 100 สำเนา ที่ไม่ใหญ่พอแม้ว่า ผมเลยเพิ่มเป็น 6,000. นั่นหมายความว่าตอนนี้ฉันอาจเป็นคนเดียวในโลกที่มีไฟล์ข้อความที่มี Moby Dick 6,000 ชุด เป็นการถ่อมตน แต่ฉันพูดเพ้อเจ้อ

การใช้ตัวแจงนับขี้เกียจเพื่อทำงานกับไฟล์ขนาดใหญ่ใน Ruby ฉันดาวน์โหลด moby dick และทำซ้ำหลายพันครั้งเพื่อให้ได้ไฟล์ขนาดใหญ่ที่จะเล่นด้วย ไวยากรณ์ไม่ได้ทุบตี มันคือเปลือกปลา ฉันคิดว่าเหลือฉันคนเดียวที่ใช้มัน

วิธีรับตัวแจงนับสำหรับไฟล์

นี่คือเคล็ดลับเด็ด ๆ ของ Ruby ที่คุณอาจเคยใช้ แม้ว่าคุณจะไม่รู้ว่าคุณกำลังใช้มันอยู่ก็ตาม เกือบทุกวิธีใน Ruby ที่วนซ้ำในคอลเลกชันจะส่งคืนวัตถุ Enumerator ให้กับคุณ หากคุณเรียกใช้โดยไม่ผ่านบล็อก หมายความว่าอย่างไร

ลองพิจารณาตัวอย่างนี้ ฉันสามารถเปิดไฟล์และใช้แต่ละบรรทัดเพื่อพิมพ์แต่ละบรรทัด แต่ถ้าฉันเรียกมันโดยไม่มีตัวกั้น ฉันจะได้ตัวแจงนับ วิธีการที่น่าสนใจคือ   each_line , each_char และ each_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

ตัวอย่างทั้งสองนี้เกือบจะเหมือนกัน แต่ตัวอย่างที่สองถือกุญแจเพื่อปลดล็อก พลังที่น่าอัศจรรย์ .

การใช้ตัวแจงนับของไฟล์

เมื่อคุณมีตัวแจงนับที่ "มี" ทุกบรรทัดในไฟล์แล้ว คุณสามารถแบ่งและแบ่งบรรทัดเหล่านั้นได้เหมือนกับที่คุณทำกับอาร์เรย์ทับทิม นี่เป็นเพียงตัวอย่างบางส่วนเท่านั้น

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 }

นี่เจ๋งจริงๆ แต่ตัวอย่างเหล่านี้ล้วนมีปัญหาใหญ่เพียงปัญหาเดียว พวกเขาทั้งหมดโหลดไฟล์ทั้งหมดลงในหน่วยความจำก่อนที่จะวนซ้ำ สำหรับไฟล์ที่มี Moby Dick 6,000 สำเนา จะสังเกตเห็นความล่าช้า

ขี้เกียจโหลดบรรทัดของไฟล์

หากเรากำลังสแกนไฟล์ข้อความขนาดใหญ่สำหรับ 10 อินสแตนซ์แรกของคำว่า "วาฬ" ก็ไม่จำเป็นต้องคอยดูแลการเกิดขึ้นครั้งที่ 10 ต่อไป โชคดีที่บอกผู้แจงนับของ Ruby ให้ทำเช่นนี้ได้ง่าย คุณเพียงแค่ใช้คำหลัก "ขี้เกียจ"

ในตัวอย่างด้านล่าง เราใช้ประโยชน์จากการโหลดแบบ Lazy Loading เพื่อทำสิ่งที่ค่อนข้างซับซ้อน

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

ไม่ได้มีไว้สำหรับไฟล์เท่านั้น

ซ็อกเก็ต ไปป์ พอร์ตอนุกรม - แสดงใน Ruby โดยใช้คลาส IO นั่นหมายความว่าพวกเขาทั้งหมดมี each_line , each_char และ each_codepoint วิธีการ ดังนั้นคุณสามารถใช้เคล็ดลับนี้กับพวกเขาทั้งหมดได้ สวยเนี๊ยบ!

ไม่ใช่เวทมนตร์

ขออภัย ตัวแจงนับขี้เกียจจะเร่งความเร็วขึ้นหากงานที่คุณพยายามทำให้สำเร็จไม่ต้องการให้อ่านไฟล์ทั้งหมด หากคุณกำลังค้นหาคำที่เกิดขึ้นเฉพาะในหน้าสุดท้ายของหนังสือ คุณต้องอ่านหนังสือทั้งเล่มจึงจะพบ แต่ในกรณีนี้ แนวทางนี้ไม่ควรช้ากว่าแนวทางที่ไม่ระบุจำนวน