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

복잡한 정규식을 단순 파서로 바꾸기

고백 시간:나는 ​​정규식으로 작업하는 것을 특히 좋아하지 않습니다. 항상 사용하지만 /^foo.*$/보다 더 복잡한 것은 멈추고 생각할 것을 요구합니다. \A(?=\w{6,10}\z)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3}) 얼핏 보면 인터넷 검색을 하는 데 몇 분이 걸리고 불행해집니다. Ruby를 읽는 것과는 상당히 다릅니다.

<블록 인용>

궁금한 점이 있으면 위의 예는 정규식 예측에 대한 이 기사에서 가져왔습니다.

상황

Honeybadge에서 저는 현재 검색 UI를 개선하기 위해 노력하고 있습니다. 많은 검색 시스템과 마찬가지로 당사는 간단한 쿼리 언어를 사용합니다. 변경하기 전에 사용자 지정 날짜 범위를 검색하려면 다음과 같이 쿼리를 수동으로 입력해야 했습니다.

occurred:[2017-06-12T16:10:00Z TO 2017-06-12T17:10:00Z]

앗!

새로운 검색 UI에서 날짜 관련 쿼리를 입력하기 시작하고 유용한 날짜 선택기를 팝업할 때 감지하고 싶습니다. 물론 datepicker는 시작에 불과합니다. 결국 더 많은 종류의 검색어를 다루기 위해 문맥 인식 힌트를 확장할 것입니다. 다음은 몇 가지 예입니다.

assigned:jane@email.com context.user.id=100
resolved:false ignored:false occurred:[
params.article.title:"Starr's parser post"       foo:'ba

다음과 같은 방식으로 이러한 문자열을 토큰화해야 합니다.

  • '', "" 또는 []로 묶인 경우를 제외하고 공백은 토큰을 구분합니다.
  • 따옴표 없는 공백은 자체 토큰입니다.
  • tokens.join("")을 실행할 수 있습니다. 입력 문자열을 정확하게 재생성하려면

예:

tokenize(%[params.article.title:"Starr's parser post"       foo:'ba])
=> ["params.article.title:\"Starr's parser post\"", "       ", "foo:'ba"]

정규 표현식 사용

내 첫 번째 생각은 캡처 정규식을 사용하여 유효한 토큰의 모양을 정의한 다음 String#split를 사용하는 것이었습니다. 문자열을 토큰으로 분할합니다. 정말 멋진 트릭입니다.

# The parens in the regexp mean that the separator is added to the array
"foo  bar  baz".split(/(foo|bar|baz)/)
=> ["", "foo", "  ", "bar", "  ", "baz"]

이것은 이상한 빈 문자열에도 불구하고 처음에는 유망해 보였습니다. 그러나 실제 정규 표현식은 훨씬 더 복잡했습니다. 내 첫 번째 초안은 다음과 같았습니다.

/
  (                          # Capture group is so split will include matching and non-matching strings
    (?:                      # The first character of the key, which is
      (?!\s)[^:\s"'\[]{1}    # ..any valid "key" char not preceeded by whitespace
      |^[^:\s"'\[]{0,1}      # ..or any valid "key" char at beginning of line
    )
    [^:\s"'\[]*              # The rest of the "key" chars
    :                        # a colon
    (?:                      # The "value" chars, which are
      '[^']+'                # ..anything surrounded by single quotes
      | "[^"]+"              # ..or anything surrounded by double quotes
      | \[\S+\sTO\s\S+\]     # ..or anything like [x TO y]
      | [^\s"'\[]+           # ..or any string not containing whitespace or special chars
    )
  )
/xi 

이것으로 작업하면 침몰하는 느낌을 받았습니다. 극단적인 경우를 찾을 때마다 정규식을 수정해야 하므로 훨씬 더 복잡해졌습니다. 또한 Ruby뿐만 아니라 JavaScript에서도 작동해야 하므로 Negative lookbehind와 같은 특정 기능을 사용할 수 없었습니다.

...이 모든 것의 부조리함이 나를 사로잡은 것은 이 무렵이었다. 내가 사용한 정규식 접근 방식은 처음부터 간단한 파서를 작성하는 것보다 훨씬 더 복잡했습니다.

파서의 구조

나는 전문가는 아니지만 간단한 파서는 간단합니다. 그들이 하는 일은:

  • 문자열을 한 글자씩 살펴보기
  • 각 문자를 버퍼에 추가
  • 토큰 분리 조건이 발생하면 버퍼를 배열에 저장하고 비우십시오.

이것을 알면 공백으로 문자열을 분할하는 간단한 파서를 설정할 수 있습니다. "foo bar".split(/(\s+)/)와 거의 동일합니다. .

class Parser

  WHITESPACE = /\s/
  NON_WHITESPACE = /\S/

  def initialize
    @buffer = []
    @output = []
  end

  def parse(text) 
    text.each_char do |c|
      case c
      when WHITESPACE
        flush if previous.match(NON_WHITESPACE)
        @buffer << c
      else
        flush if previous.match(WHITESPACE)
        @buffer << c
      end
    end

    flush
    @output
  end

  protected

  def flush
    if @buffer.any?
      @output << @buffer.join("")
      @buffer = []
    end
  end

  def previous
    @buffer.last || ""
  end

end


puts Parser.new().parse("foo bar baz").inspect

# Outputs ["foo", " ", "bar", " ", "baz"]

이것은 내가 원하는 방향으로 나아가는 단계이지만 따옴표와 대괄호에 대한 지원이 없습니다. 다행히 몇 줄의 코드만 추가하면 됩니다.

  def parse(text) 

    surround = nil

    text.each_char do |c|
      case c
      when WHITESPACE
        flush if previous.match(NON_WHITESPACE) && !surround
        @buffer << c
      when '"', "'"
        @buffer << c
        if !surround
          surround = c
        elsif surround == c
          flush
          surround = nil
        end
      when "["
        @buffer << c
        surround = c if !surround
      when "]"
        @buffer << c
        if surround == "["
          flush
          surround = nil
        end
      else
        flush() if previous().match(WHITESPACE) && !surround
        @buffer << c
      end
    end

    flush
    @output
  end

이 코드는 정규식 기반 접근 방식보다 약간 길지만 훨씬 더 간단합니다.

이별 생각

내 사용 사례에 잘 맞는 정규식이 있을 것입니다. 역사가 어떤 길잡이라면 아마 나를 바보처럼 보일 정도로 간단할 것입니다. :)

그러나 나는 이 작은 파서를 작성할 기회를 정말 즐겼습니다. 정규식 접근 방식을 사용하는 틀에서 벗어나게되었습니다. 좋은 보너스로, 저는 복잡한 정규식을 기반으로 하는 코드보다 결과 코드에 대해 훨씬 더 확신합니다.