Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

複雑な正規表現を単純なパーサーに置き換える

告白時間:正規表現を扱うのは特に好きではありません。私はいつもそれらを使用していますが、/^foo.*$/よりも複雑なものは何でも 立ち止まって考える必要があります。 \A(?=\w{6,10}\z)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3}) 一見すると、グーグルで数分かかり、不幸になります。これは、Rubyを読むこととはかなり異なります。

興味がある場合は、上記の例は、正規表現の先読みに関するこの記事から抜粋したものです。

状況

Honeybadgerでは、現在検索UIの改善に取り組んでいます。多くの検索システムと同様に、私たちのシステムは単純なクエリ言語を使用しています。変更する前に、カスタムの日付範囲を検索する場合は、次のようなクエリを手動で入力する必要がありました。

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

痛い!

新しい検索UIでは、日付関連のクエリの入力を開始したときにそれを検出し、役立つ日付ピッカーをポップアップ表示します。そしてもちろん、デートピッカーはほんの始まりに過ぎません。最終的には、コンテキスト依存のヒントを拡張して、より多くの種類の検索用語をカバーする予定です。次にいくつかの例を示します。

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でも機能する必要があるため、ネガティブルックビハインドなどの特定の機能を利用できませんでした。

...この頃、このすべての不条理が私を襲った。私が使用していた正規表現のアプローチは、単純なパーサーを最初から作成するよりもはるかに複雑でした。

パーサーの構造

私は専門家ではありませんが、単純なパーサーは単純です。彼らがするのは:

  • 文字列を1文字ずつステップスルーします
  • 各文字をバッファに追加します
  • トークン分離条件が発生した場合は、バッファを配列に保存して空にします。

これを知っていると、文字列を空白で分割する単純なパーサーを設定できます。これは、"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

このコードは、私の正規表現ベースのアプローチよりも少し長いだけですが、はるかに簡単です。

別れの考え

私のユースケースでうまく機能する正規表現がおそらくそこにあります。歴史がガイドなら、それはおそらく私をばか者のように見せかけるのに十分単純です。 :)

しかし、私はこの小さなパーサーを書く機会を本当に楽しんだ。それは私が正規表現のアプローチでいた轍から私を壊しました。素晴らしいボーナスとして、複雑な正規表現に基づくコードを使用するよりも、結果のコードに自信を持っています。


  1. C++での例を含む式ツリー

    式ツリーは、ツリーの各ノードが演算子またはオペランドで構成される特殊なタイプの二分木です。 リーフノード ツリーのオペランドを表します 。 非リーフノード ツリーの演算子を表します 。 例: 簡単に解決できる中置式を取得するには、順序トラバーサルを使用してツリーをトラバースする必要があります。

  2. Rubyでパーサーを構築する方法

    構文解析は、一連の文字列を理解し、それらを理解できるものに変換する技術です。正規表現を使用することもできますが、必ずしもその仕事に適しているとは限りません。 たとえば、HTMLを正規表現で解析することはおそらく良い考えではないことは一般的な知識です。 Rubyには、この作業を実行できるnokogiriがありますが、独自のパーサーを作成することで多くのことを学ぶことができます。始めましょう! Rubyでの解析 パーサーの中核はStringScannerです クラス。 このクラスは、文字列のコピーと位置ポインタを保持します。ポインタを使用すると、特定のトークンを検索するために文字列をトラバ