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

Ruby의 숨겨진 보석, StringScanner

Ruby는 재미있는 언어일 뿐만 아니라 우수한 표준 라이브러리도 함께 제공됩니다. 그 중 일부는 알려지지 않았고 거의 숨겨진 보석입니다. 오늘 게스트 작가인 Michael Kohl은 가장 좋아하는 Stringscanner를 강조합니다.

Ruby의 숨겨진 보석:StringScanner

OpenStruct 및 Set over CSV 구문 분석과 같은 데이터 구조에서 벤치마킹에 이르기까지 타사 gem을 설치하지 않고도 상당히 멀리 갈 수 있습니다. 그러나 Ruby의 표준 설치에는 매우 유용할 수 있는 잘 알려지지 않은 라이브러리가 있습니다. 그 중 하나는 StringScanner입니다. 문서 "문자열에 대한 어휘 검색 작업을 제공합니다"에 따르면 .

스캔 및 파싱

그렇다면 "어휘 스캐닝"은 정확히 무엇을 의미합니까? 기본적으로 특정 규칙에 따라 입력 문자열을 가져와서 의미 있는 정보 비트를 추출하는 프로세스를 설명합니다. 예를 들어, 이것은 2 + 1과 같은 표현식을 취하는 컴파일러의 첫 번째 단계에서 볼 수 있습니다. 다음 토큰 시퀀스로 변환합니다.

[{ number: "1" }, {operator: "+"}, { number: "1"}]

어휘 스캐너는 일반적으로 유한 상태 자동 장치로 구현되며 이를 생성할 수 있는 몇 가지 잘 알려진 도구가 있습니다(예:ANTLR 또는 Ragel).

그러나 때로는 구문 분석 요구 사항이 그렇게 정교하지 않고 정규식 기반 StringScanner와 같은 더 간단한 라이브러리가 있습니다. 이러한 상황에서 매우 유용할 수 있습니다. 이른바 스캔 포인터의 위치를 ​​기억함으로써 작동합니다. 이것은 문자열에 대한 인덱스에 지나지 않습니다. 그런 다음 스캔 프로세스는 스캔 포인터 바로 뒤의 코드를 제공된 표현식과 일치시키려고 시도합니다. 일치 작업과는 별도로 StringScanner 또한 스캔 포인터를 이동(문자열을 통해 앞뒤로 이동), 앞을 내다보기(아직 스캔 포인터를 수정하지 않고 다음 내용 보기) 및 현재 문자열의 위치(시작 또는 줄 끝/전체 문자열 등).

Rails 로그 구문 분석

이론은 충분합니다. StringScanner를 살펴보겠습니다. 행동에. 다음 예는 아래와 같은 Rails의 로그 항목을 사용합니다.

log_entry = <<EOS
Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
Processing by HomeController#index as HTML
  Rendered text template within layouts/application (0.0ms)
  Rendered layouts/_assets.html.erb (2.0ms)
  Rendered layouts/_top.html.erb (2.6ms)
  Rendered layouts/_about.html.erb (0.3ms)
  Rendered layouts/_google_analytics.html.erb (0.4ms)
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
EOS

다음 해시로 구문 분석:

{
  method: "GET",
  path: "/"
  ip: "127.0.0.1",
  timestamp: "2017-08-20 20:53:10 +0900",
  success: true,
  response_code: "200",
  duration: "79ms",
}

주의:StringScanner에 대한 좋은 예입니다. 실제 애플리케이션은 Lograge와 JSON 로그 포맷터를 사용하는 것이 더 나을 것입니다.

StringScanner를 사용하려면 먼저 요구해야 합니다.

require 'strscan'

그런 다음 로그 항목을 생성자에 인수로 전달하여 새 인스턴스를 초기화할 수 있습니다. 동시에 우리는 파싱 노력의 결과를 담기 위해 빈 해시를 정의할 것입니다.

scanner = StringScanner.new(log_entry)
log = {}

이제 스캐너의 pos 메서드를 사용하여 스캔 포인터의 현재 위치를 가져올 수 있습니다. 예상대로 결과는 0입니다. , 문자열의 첫 번째 문자:

scanner.pos #=> 0

프로세스를 더 쉽게 따라갈 수 있도록 이를 시각화해 보겠습니다.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

스캐너 상태에 대한 추가 검사를 위해 beginning_of_line?을 사용할 수 있습니다. 및 eos? 스캔 포인터가 현재 줄의 시작 부분에 있고 아직 입력을 완전히 사용하지 않았는지 확인하려면:

scanner.beginning_of_line? #=> true
scanner.eos? #=> false

추출하고자 하는 정보의 첫 번째 비트는 "Started"라는 단어 바로 뒤에 공백이 오는 HTTP 요청 메서드입니다. 스캐너의 적절하게 명명된 skip 메소드를 사용하여 스캔 포인터를 진행할 수 있습니다. 그러면 무시된 문자의 수(이 경우에는 8)가 반환됩니다. 또한 matching?을 사용할 수 있습니다. 모든 것이 예상대로 작동하는지 확인하기 위해:

scanner.skip(/Started /) #=> 8
scanner.matched? #=> true

스캔 포인터는 이제 요청 메서드 바로 앞에 있습니다.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

이제 scan_until을 사용하여 전체 정규식 일치를 반환하는 실제 값을 추출할 수 있습니다. 요청 메서드가 모두 대문자이므로 간단한 문자 클래스와 +를 사용할 수 있습니다. 하나 이상의 문자와 일치하는 연산자:

log[:method] = scanner.scan_until(/[A-Z]+/) #=> "GET"

이 작업 후에 스캔 포인터는 단어 "GET"의 마지막 "T"에 있습니다.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

따라서 요청된 경로를 추출하려면 공백 하나를 건너뛰고 큰따옴표로 묶인 모든 항목을 추출해야 합니다. 이를 달성하는 방법에는 여러 가지가 있으며 그 중 하나는 캡처 그룹(괄호 안에 포함된 정규식의 일부, 즉 (.+) ) 임의의 문자 중 하나 이상과 일치:

scanner.scan(/\s"(.+)"/) #=> " \"/\""

그러나 이 scan의 반환 값은 사용하지 않습니다. 작업을 직접 수행하지만 대신 캡처를 사용하여 첫 번째 캡처 그룹의 값을 가져옵니다.

log[:path] =  scanner.captures.first #=> "/"

경로를 성공적으로 추출했으며 이제 스캔 포인터가 닫는 큰따옴표에 있습니다.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

로그에서 IP 주소를 구문 분석하기 위해 다시 한 번 skip를 사용합니다. 공백으로 둘러싸인 문자열 "for"를 무시하고 scan_until을 사용합니다. 하나 이상의 공백이 아닌 문자(\s 공백과 [^\s]를 나타내는 문자 클래스입니다. 부정):

scanner.skip(/ for /) #=> 5
log[:ip] = scanner.scan_until(/[^\s]+/) #=> "127.0.0.1"

이제 스캔 포인터가 어디에 있는지 알 수 있습니까? 잠시 생각한 다음 답과 솔루션을 비교하세요.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
       ^
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)

타임스탬프를 구문 분석하는 것은 이제 매우 친숙하게 느껴질 것입니다. 먼저 신뢰할 수 있는 오래된 skip를 사용합니다. "에서 리터럴 문자열 "를 무시하려면 그런 다음 scan_until을 사용하십시오. $로 표시되는 현재 줄의 끝까지 읽습니다. 정규 표현식:

scanner.skip(/ at /) #=> 4
log[:timestamp] = scanner.scan_until(/$/) #=> "2017-08-20 20:53:10 +0900"

관심 있는 다음 정보는 마지막 줄의 HTTP 상태 코드이므로 skip_until을 사용하여 "Completed"라는 단어 뒤의 공백으로 이동합니다.

scanner.skip_until(/Completed /) #=> 296

이름에서 알 수 있듯이 이것은 scan_until과 유사하게 작동합니다. 그러나 일치하는 문자열을 반환하는 대신 건너뛴 문자 수를 반환합니다. 그러면 관심 있는 HTTP 상태 코드 바로 앞에 스캔 포인터가 놓입니다.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
         ^

이제 실제 HTTP 응답 코드를 스캔하기 전에 HTTP 응답 코드가 성공(이 예에서는 2xx 범위의 모든 코드) 또는 실패(다른 모든 범위)를 나타내는지 알 수 있다면 좋지 않을까요? 이를 위해 스캔 포인터를 실제로 이동하지 않고 엿보기를 사용하여 다음 문자를 봅니다.

log[:success] = scanner.peek(1) == "2" #=> true

이제 scan을 사용하여 정규식 /\d{3}/로 표시되는 다음 세 문자를 읽을 수 있습니다. :

log[:response_code] = scanner.scan(/\d{3}/) #=> "200"

다시 한 번 스캔 포인터는 이전에 일치한 정규식의 끝에 올 것입니다.

Started GET "/" for 127.0.0.1 at 2017-08-20 20:53:10 +0900
...
Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
         ^

로그 항목에서 추출하려는 마지막 정보는 skip로 얻을 수 있는 밀리초 단위의 실행 시간입니다. "에서 문자열 " OK에 대해 ping 그런 다음 리터럴 문자열 "ms"까지 포함하여 모든 것을 읽습니다. .

scanner.skip(/ OK in /) #=> 7
log[:duration] = scanner.scan_until(/ms/) #=> "79ms"

그리고 마지막 비트가 있으면 원하는 해시를 갖게 됩니다.

{
  method: "GET",
  path: "/"
  ip: "127.0.0.1",
  timestamp: "2017-08-20 20:53:10 +0900",
  success: true,
  response_code: "200",
  duration: "79ms",
}

요약

Ruby의 StringScanner 간단한 정규 표현식과 완전한 렉서 사이의 좋은 중간 지점을 차지합니다. 복잡한 스캔 및 구문 분석 요구 사항에 대한 최선의 선택이 아닙니다. 그러나 기본적인 정규식 지식이 있는 모든 사람이 입력 문자열에서 정보를 쉽게 추출할 수 있고 저는 과거에 프로덕션 코드에서 성공적으로 사용했습니다. 이 숨겨진 보석을 발견하기를 바랍니다.

추신:다음에 강조해야 할 숨겨진 보석을 알려주세요!