Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

게으른 열거자를 사용하여 Ruby에서 대용량 파일 작업하기

열거자는 Ruby를 강력하고 역동적인 언어로 만드는 핵심 요소입니다. 그리고 게으른 열거자는 매우 큰 컬렉션으로 효율적으로 작업할 수 있도록 하여 이를 한 단계 더 발전시킵니다.

파일 - 밝혀진 바에 따르면 - 라인이나 문자의 큰 모음입니다. 따라서 게으른 열거자는 매우 흥미롭고 강력한 일을 가능하게 합니다.

열거자란 무엇입니까?

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

게으른 열거자는 대규모 컬렉션을 위한 것입니다.

일반 열거자는 큰 컬렉션에 문제가 있습니다. 그 이유는 호출하는 각 메서드가 전체 컬렉션을 반복하기를 원하기 때문입니다. 다음 코드를 실행하여 직접 확인할 수 있습니다.

# 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]

모비딕 6천부

이러한 파일 트릭을 테스트하려면 큰 파일이 필요합니다. 너무 커서 "게으르지 않는 것"이 ​​분명합니다.

저는 프로젝트 구텐베르크에서 모비딕을 다운받아서 100장짜리 텍스트 파일을 만들었습니다. 그래도 충분히 크지는 않았습니다. 그래서 6,000까지 올렸습니다. 즉, 지금 당장은 Moby Dick의 6,000개 사본이 포함된 텍스트 파일을 가지고 있는 사람이 아마 전 세계에서 유일한 사람일 것입니다. 일종의 겸손입니다. 하지만 나는 빗나간다.

게으른 열거자를 사용하여 Ruby에서 대용량 파일 작업하기 모비딕을 다운로드하고 수천 번 복제하여 큰 파일로 재생할 수 있습니다. 구문은 bash가 아닙니다. 생선 껍질입니다. 저만 사용하는 것 같아요.

파일의 열거자를 얻는 방법

여기 당신이 그것을 사용하고 있다는 것을 몰랐더라도 당신이 아마 사용했을 멋진 Ruby 트릭이 있습니다. 컬렉션을 반복하는 Ruby의 거의 모든 메서드는 블록을 전달하지 않고 호출하면 Enumerator 객체를 반환합니다. 그게 무슨 뜻인가요?

이 예를 고려하십시오. 파일을 열고 각 줄을 사용하여 각 줄을 인쇄할 수 있습니다. 그러나 블록 없이 호출하면 열거자를 얻습니다. 관심 있는 방법은   each_line입니다. , each_chareach_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 }

이것은 정말 멋지지만 이러한 예에는 모두 하나의 큰 문제가 있습니다. 그들은 모두 반복하기 전에 전체 파일을 메모리에 로드합니다. 6,000개의 Moby Dick 사본이 포함된 파일의 경우 지연이 눈에 띕니다.

파일 줄 지연 로드

"고래"라는 단어의 처음 10개 인스턴스에 대해 큰 텍스트 파일을 스캔하는 경우 실제로 10번째 발생을 계속 볼 필요가 없습니다. 다행히도 Ruby의 열거자에게 이렇게 하도록 지시하는 것은 매우 쉽습니다. "게으른" 키워드를 사용하면 됩니다.

아래 예에서는 지연 로딩을 활용하여 매우 정교한 작업을 수행합니다.

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

파일 전용이 아닙니다.

소켓, 파이프, 직렬 포트 - IO 클래스를 사용하여 Ruby에서 표현됩니다. 즉, 모두 each_line , each_chareach_codepoint 행동 양식. 따라서 이 트릭을 모두 사용할 수 있습니다. 꽤 깔끔합니다!

마법이 아닙니다

안타깝게도 게으른 열거자는 수행하려는 작업에서 전체 파일을 읽을 필요가 없는 경우에만 속도를 높입니다. 책의 마지막 페이지에만 나오는 단어를 찾으려면 책 전체를 읽어야 찾을 수 있습니다. 그러나 이 경우 이 접근 방식은 비 열거자 접근 방식보다 느려서는 안 됩니다.