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

내부:Ruby의 "Slurping" 및 스트리밍 파일

이번 Ruby Magic 에디션에서는 Ruby의 스트리밍 파일, IO 클래스는 파일을 메모리에 완전히 로드하지 않고 읽기를 처리하고 읽기 바이트를 버퍼링하여 줄당 파일을 읽는 방법을 처리합니다. 바로 뛰어들자!

"Slurping" 및 스트리밍 파일

Ruby의 File.read 메소드는 파일을 읽고 전체 내용을 반환합니다.

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

작은 파일에서는 작동하지만 전체 파일을 메모리로 읽는 것은 더 큰 파일을 처리할 때 문제가 될 수 있습니다. 예를 들어, 14GB 로그 파일을 구문 분석할 때 전체 파일을 한 번에 읽는 것은 비용이 많이 드는 작업입니다. 파일의 내용이 메모리에 유지되므로 앱의 메모리 공간이 상당히 늘어납니다. 이것은 결국 메모리 스와핑과 앱의 프로세스를 종료하는 OS로 이어질 수 있습니다.

운 좋게도 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

그렇다면 먼저 전체 파일을 읽을 필요 없이 파일을 한 줄씩 읽는 것은 어떻게 작동합니까? 이를 이해하려면 파일 읽기와 관련된 일부 레이어를 벗겨야 합니다. Ruby의 IO를 자세히 살펴보겠습니다. 수업.

I/O 및 Ruby의 IO 수업

File.readFile.foreach 존재하는 경우 File에 대한 문서 클래스는 그것들을 나열하지 않습니다. 실제로 File에서 파일 읽기 또는 쓰기 방법을 찾을 수 없습니다. 상위 IO에서 상속되기 때문에 클래스 문서 수업.

입출력

I/O 장치 예를 들어 키보드, 디스플레이 및 하드 드라이브와 같이 컴퓨터와 데이터를 주고받는 장치입니다. 입력/출력을 수행합니다. , 또는 I/O , 데이터 스트림을 읽거나 생성합니다.

하드 드라이브에서 파일 읽기 및 쓰기는 가장 일반적인 I/O입니다. 다른 유형의 I/O에는 소켓 통신, 터미널에 대한 로깅 출력 및 키보드 입력이 포함됩니다.

IO Ruby의 클래스는 파일 읽기 및 쓰기와 같은 모든 입력 및 출력을 처리합니다. 파일을 읽는 것은 다른 I/O 스트림에서 읽는 것과 다르지 않기 때문에 File 클래스는 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 스트림 읽기

내부적으로 Ruby의 IO 클래스의 읽기 및 쓰기 능력은 커널 시스템 호출에 대한 추상화를 기반으로 합니다. 운영 체제의 커널은 I/O 장치에서 읽고 쓰는 작업을 처리합니다.

파일 열기

IO.sysopen 커널에 파일 테이블에 있는 파일에 대한 참조를 넣도록 요청하고 프로세스의 파일 디스크립터 테이블에 파일 디스크립터를 생성하여 파일을 엽니다.

파일 설명자와 파일 테이블

파일을 열면 I/O 리소스에 액세스하는 데 사용되는 정수인 파일 설명자가 반환됩니다.

각 프로세스에는 파일 설명자를 메모리에 유지하기 위한 자체 파일 설명자 테이블이 있으며 각 설명자는 시스템 전체의 파일 테이블 항목을 가리킵니다. .

I/O 리소스에서 읽거나 쓰기 위해 프로세스는 시스템 호출을 통해 파일 디스크립터를 커널에 전달합니다. 그런 다음 프로세스는 파일 테이블에 액세스할 수 없으므로 커널은 프로세스를 대신하여 파일에 액세스합니다.

파일을 열지 않습니다. 내용을 메모리에 유지하지만 파일 설명자 테이블이 가득 찰 수 있으므로 파일을 연 후에는 항상 닫는 것이 좋습니다. File.open을 래핑하는 메소드 File.read와 같이 이 작업을 자동으로 수행할 뿐만 아니라 차단하는 사용자도 수행합니다.

이 예에서는 IO.sysopen을 호출하여 한 단계 더 나아가겠습니다. 직접 방법. 파일 이름을 전달하면 메서드는 나중에 열린 파일을 참조하는 데 사용할 수 있는 파일 설명자를 만듭니다.

irb> IO.sysopen("log/production.log")
=> 9

IO를 생성하려면 Ruby가 읽고 쓸 수 있도록 파일 설명자를 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를 사용합니다. 파일 설명자 정수를 IO.new에 전달하여 이전에 생성한 인스턴스 . IO#sysread를 호출하여 파일에서 처음 64바이트를 읽고 반환합니다. 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로 이동한 다음 IO#sysread를 사용하여 64바이트를 읽습니다. . 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에 바이트를 추가합니다. @buffer 변수에는 개행이 포함됩니다. 그런 일이 발생하면 버퍼를 첫 번째 줄 바꿈으로 분할합니다. 첫 번째 부분은 line입니다. , 두 번째 부분은 새 버퍼입니다.

그런 다음 전달된 블록은 행과 나머지 @buffer와 함께 호출됩니다. 다음 루프에서 사용하기 위해 보관됩니다.

파일 내용을 버퍼링하여 파일을 논리적 청크로 분할하면서 I/O 호출 횟수를 줄입니다.

스트리밍 파일

요약하면 스트리밍 파일은 운영 체제의 커널에 파일을 열도록 요청한 다음 비트 단위로 바이트를 읽도록 요청하여 작동합니다. Ruby에서 한 줄에 파일을 읽을 때 파일에서 한 번에 512바이트의 데이터를 가져온 다음 "줄"로 분할합니다.

이것으로 Ruby의 I/O 및 스트리밍 파일에 대한 개요를 마칩니다. 이 기사에 대한 귀하의 생각이나 질문이 있는 경우 알고 싶습니다. 우리는 항상 조사하고 설명할 주제를 찾고 있습니다. 따라서 Ruby에 대해 읽고 싶은 마법 같은 것이 있다면 주저하지 말고 지금 @AppSignal로 알려주세요!