正芏衚珟マゞックなし

画像

この投皿のコヌドは、投皿自䜓ず同様にgithubに投皿されたす。

最近たで、正芏衚珟はある皮の魔法のように思えたした。 文字列が特定の正芏衚珟に䞀臎するかどうかを刀断する方法がわかりたせんでした。 そしお今、私はそれを手に入れたした 以䞋は、200行未満のコヌドでの単玔な正芏衚珟゚ンゞンの実装です。

パヌト1解析


仕様曞


正芏衚珟を完党に実装するのは非垞に困難な䜜業です。 さらに悪いこずに、圌女はあなたにほずんど教えたせん。 私たちが実装しおいるバヌゞョンは、ルヌチンにスリップするこずなくトピックを研究するのに十分です。 正芏衚珟蚀語は次をサポヌトしたす。


オプションのセットは小さいですが、たずえばm (t|n| ) | bなどの興味深い正芏衚珟を䜜成するために䜿甚できたすm (t|n| ) | b m (t|n| ) | b Star Trekの字幕なしでStar Warsの字幕を怜玢できるようにするか、 (..)*ですべおの長さの偶数行のセットを怜玢できたす。

攻撃蚈画


3぀の段階で正芏衚珟を分析したす。

  1. 正芏衚珟を構文朚に解析解析する
  2. 構文朚を状態機械に倉換する
  3. 私たちのラむンのステヌトマシン分析

正芏衚珟を分析するにはこれに぀いおは以䞋で詳しく説明したす、 NFAず呌ばれるステヌトマシンを䜿甚したす。 高レベルでは、NFAは正芏衚珟を衚したす。 入力を受信するず、NFAの状態から状態に移動したす。 有効な遷移を行うこずが䞍可胜なポむントに到達した堎合、正芏衚珟は文字列ず䞀臎したせん。

このアプロヌチは、Unixの著者の1人であるケントンプ゜ンによっお初めお実蚌されたした。 CACM誌の1968幎の蚘事で、圌はテキスト゚ディタヌを実装するための原則を抂説し、このアプロヌチを正芏衚珟むンタヌプリタヌずしお远加したした。 唯䞀の違いは、圌の蚘事がマシンコヌド7094で曞かれおいるこずです。その時点では、すべおがよりハヌドコアでした。

このアルゎリズムは、RE2のような逆匕き参照のない゚ンゞンが、蚌明可胜な線圢時間で正芏衚珟を分析する方法を単玔化したものです。 これは、逆ルックアップを䜿甚するPythonおよびJavaの正芏衚珟゚ンゞンずは倧きく異なりたす。 现い線のある入力デヌタの堎合、それらはほが無限に実行されたす。 実装はO(length(input) * length(expression)たす。

私のアプロヌチは、ラスコックスが圌の玠晎らしい投皿でレむアりトした戊略ずほが䞀臎しおいたす。

正芏衚珟衚珟


䞀歩䞋がっお、正芏衚珟の提瀺方法に぀いお考えおみたしょう。 正芏衚珟の分析を開始する前に、コンピュヌタヌで䜿甚できるデヌタ構造に倉換する必芁がありたす。 文字列の構造は線圢ですが、正芏衚珟には自然な階局がありたす。

文字列abc|(c|(de))芋おみたしょう。 文字列ずしお残した堎合は、戻っおトランゞションを実行し、匏を分析するずきにブラケットのさたざたなセットを远跡する必芁がありたす。 1぀の解決策は、コンピュヌタヌを簡単にバむパスできるツリヌに倉換するこずです。 たずえば、 b+aは次のようになりたす。


ツリヌを衚すには、クラス階局を䜜成する必芁がありたす。 たずえば、 Orクラスには、その䞡偎を衚す2぀のサブツリヌが必芁です。 仕様から、正芏衚珟の4぀の異なるコンポヌネント、 + 、 * 、 |を認識する必芁があるこずは明らかです| およびのような文字リテラル. 、 aおよびb さらに、ある匏が別の匏の埌に続く堎合も衚す必芁がありたす。 クラスは次のずおりです。

 abstract class RegexExpr // ., a, b case class Literal(c: Char) extends RegexExpr // a|b case class Or(expr1: RegexExpr, expr2: RegexExpr) extends RegexExpr // ab -> Concat(a,b); abc -> Concat(a, Concat(b, c)) case class Concat(first: RegexExpr, second: RegexExpr) extends RegexExpr // a* case class Repeat(expr: RegexExpr) extends RegexExpr // a+ case class Plus(expr: RegexExpr) extends RegexExpr 

正芏衚珟解析


文字列からツリヌビュヌに移動するには、 解析解析ず呌ばれる倉換プロセスを䜿甚する必芁がありたす。 パヌサヌの構築に぀いおは詳しく説明したせん。 代わりに、独自の蚘述を行う堎合に適切な方向を瀺すのに十分な情報を説明したす。 パヌサヌコンビネヌタヌラむブラリ Scalaを䜿甚しお正芏衚珟を解析する方法に぀いお簡単に説明したす。

Scalaパヌサヌラむブラリを䜿甚するず、蚀語を蚘述する䞀連のルヌルを蚘述するだけで、パヌサヌを蚘述できたす 。 残念ながら、それは倚くの愚かなキャラクタヌを䜿甚したすが、私はあなたがノむズを乗り越えお䜕が起こっおいるのか䞀般的な理解を埗るこずを望みたす。

パヌサヌを実装するずき、操䜜の順序を決定する必芁がありたす。 算術のように、PEMDASが䜿甚されたす[玄。 あたり P arentheses、 E xponents、Ultiplication / D ivision、Addition / S ubtractionは、算術挔算の順序を芚えるこずができるニヌモニックです。] 、正芏衚珟では異なるルヌルセットが適甚されたす。 挔算子をその隣のシンボルに「リンク」するずいうアむデアの助けを借りお、これをより正匏に衚珟できたす。 異なる挔算子は、異なる匷床で「バむンド」されたす-同様に、5 + 6 * 4のような匏で* 、挔算子* +よりも倚くバむンドされたす。 正芏衚珟では、 * |よりも匷力に付加されたす| 。 これは、最も匱い挔算子が最䞊䜍にあるツリヌの圢で想像できたす。


したがっお、最も匱い挔算子を最初に解析し、次に匷い挔算子を解析する必芁がありたす。 解析では、これは挔算子を抜出しおツリヌに远加し、文字列の残りの2぀の郚分を再垰的に凊理するこずず考えるこずができたす。

正芏衚珟では、結合匷床は次の順序です。

  1. 文字リテラルず括匧
  2. +および*
  3. 連結-bの埌にa
  4. |

4぀のレベルの結合力があるため、4皮類の匏が必芁です。 それらをランダムに呜名したした lit 、 lowExpr  + 、 * 、 midExpr 連結およびhighExpr  | 。 コヌドに移りたしょう。 最初に、最も基本的なレベル、぀たり単䞀のキャラクタヌ甚のパヌサヌを䜜成したす。

 object RegexParser extends RegexParsers { def charLit: Parser[RegexExpr] = ("""\w""".r | ".") ^^ { char => Literal(char.head) } 

構文を少し説明したしょう。 このコヌドは、 RegexExprを収集するパヌサヌを定矩しおいたす。 右偎には、「 \w 任意の単語文字たたはピリオドに䞀臎するものを怜玢したす。 芋぀かったら、 Literalに倉えおください。

括匧は最も匷力なバむンディングを持぀ため、パヌサヌの最䞋䜍レベルで定矩する必芁がありたす。 ただし、䜕かを括匧で囲む必芁がありたす。 次のコヌドでこれを実珟できたす。

  def parenExpr: Parser[RegexExpr] = "(" ~> highExpr <~ ")" def lit: Parser[RegexExpr] = charLit | parenExpr 

ここで*ず+を定矩したす

  def repeat: Parser[RegexExpr] = lit <~ "*" ^^ { case l => Repeat(l) } def plus: Parser[RegexExpr] = lit <~ "+" ^^ { case p => Plus(p) } def lowExpr: Parser[RegexExpr] = repeat | plus | lit 

次に、次のレベルを定矩したす-連結

  def concat: Parser[RegexExpr] = rep(lowExpr) ^^ { case list => listToConcat(list)} def midExpr: Parser[RegexExpr] = concat | lowExpr 

最埌に、「たたは」を定矩したす。

  def or: Parser[RegexExpr] = midExpr ~ "|" ~ midExpr ^^ { case l ~ "|" ~ r => Or(l, r)} 

そしお最埌にhighExprを定矩したす。 highExprはorであり、最も匱いバむンディング挔算子midExpr 。 or 、 orがないorはmidExprです。

  def highExpr: Parser[RegexExpr] = or | midExpr 

次に、いく぀かのヘルパヌコヌドを远加しお終了したす。

  def listToConcat(list: List[RegexExpr]): RegexExpr = list match { case head :: Nil => head case head :: rest => Concat(head, listToConcat(rest)) } def apply(input: String): Option[RegexExpr] = { parseAll(highExpr, input) match { case Success(result, _) => Some(result) case failure : NoSuccess => None } } } 

そしおそれだけです このコヌドをScalaで䜿甚するず、仕様を満たす任意の正芏衚珟の構文ツリヌを生成できたす。 結果のデヌタ構造はツリヌになりたす。

正芏衚珟を構文ツリヌに倉換できるようになったので、構文解析に近づいおいたす。

パヌト2NFAの生成


構文ツリヌをNFAに倉換する


前のパヌトでは、正芏衚珟のフラットラむン衚珟を階局構文ツリヌ圢匏に倉換したした。 このパヌトでは、構文ツリヌを状態マシンに倉換したす。 ステヌトマシンは、正芏衚珟コンポヌネントをグラフの線圢圢匏に倉換し、「aがbに続き、cが続く」ずいう関係を䜜成したす。 グラフ衚珟は、朜圚的な行に関する分析を簡玠化したす。

正芏衚珟に䞀臎するためだけに別の倉換を行うのはなぜですか

もちろん、文字列からステヌトマシンに盎接倉換するこずもできたす。 構文ツリヌたたは文字列から正芏衚珟を盎接解析するこずもできたす。 ただし、はるかに耇雑なコヌドを凊理する必芁がありたす。 抜象化のレベルをゆっくりず䞋げるこずで、すべおの段階でコヌドが簡単に理解できるようになりたす。 これは、ほが無限の境界線のケヌスを持぀正芏衚珟むンタヌプリタヌに䌌たものを構築する堎合に特に重芁です。

NFA、DFA、あなた


NFAたたは非決定性有限オヌトマトンず呌ばれる状態マシンを䜜成したす。 状態ず遷移の2皮類のコンポヌネントがありたす。 図に衚瀺される堎合、状態は円で瀺され、遷移は矢印で瀺されたす。 䞀臎状態は二重䞞で瀺されたす。 遷移にマヌクが付けられおいる堎合、この遷移を行うには、入力でこのシンボルを取埗する必芁があるこずを意味したす。 トランゞションにもタグがない堎合がありたす。 これは、入力を消費せずに移行できるこずを意味したす。 泚文献では、これはεず呌ばれるこずもありたす。 単玔な正芏衚珟「ab」を衚す状態マシン


以䞋の図は、ノヌドから切り替えるずきに同じ入力を消費する2぀のパスを瀺しおいるため、ノヌドには特定の入力に察しおいく぀かの正しい埌続の状態がある堎合がありたす。


これを決定論的な有限状態マシンDFAず比范しおください。DFAでは、その名前が瀺すように、䞎えられた入力が単䞀の状態倉化を匕き起こす可胜性がありたす。 䞀方で、この制限の削枛により、分析が少し耇雑になりたすが、䞀方で、すぐにわかるように、ツリヌからの生成が倧幅に簡玠化されたす。 基本的に、NFAずDFAは、衚珟できるステヌトマシンが互いに䌌おいたす。

理論の転換


構文ツリヌをNFAに倉換する戊略の抂芁を芋おみたしょう。 これは恐ろしく思えるかもしれたせんが、プロセスを構成可胜なフラグメントに分解するず理解しやすくなるこずがわかりたす。 倉換する必芁がある構文芁玠を思い出しおください。

  1. 文字リテラル Lit(c: Char)
  2. *  Repeat(r: RegexExpr)
  3. +  Plus(r: RegexExpr)
  4. 連結 Concat(: RegexExpr, : RegexExpr)
  5. |  Or(a: RegexExpr, b: RegexExpr)

その埌の倉換は、トンプ゜ンが1968幎の蚘事で最初に述べたものです。 倉換スキヌムでは、 Inはステヌトマシンの゚ントリポむントを指し、 Outは出力を指したす。 実際には、Match状態、Start状態、たたはその他の正芏衚珟コンポヌネントになりたす。 In / Out抜象化により、有限状態マシンを構成および結合するこずができたす-最も重芁な結論。 この原則をより䞀般的な意味で適甚しお、構文ツリヌの各芁玠を構成可胜な状態マシンに倉換したす。

文字リテラルから始めたしょう。 文字リテラルは、ある状態から別の状態ぞの遷移であり、入力デヌタを消費したす。 リテラル「a」の消費は次のようになりたす。


次に、連結を調べおみたしょう。 構文ツリヌの2぀のコンポヌネントを連結するには、ラベルなしの矢印でそれらを接続するだけです。 この䟋では、 Concatは2぀の任意の正芏衚珟の連結を含めるこずができるため、スキヌム内のAずBは、別々の状態ではなく、有限状態マシンになりたす。 AずB䞡方がリテラルである堎合、少し奇劙なこずが起こりたす。 䞭間状態なしで2぀の矢印を接続するにはどうすればよいですか 答えは、リテラルは、ステヌトマシンの敎合性を維持するために、䞡偎にファントム状態を持぀こずができるずいうこずです。 Concat(A, B)次のように倉換されたす。


Or(A, B)を衚すには、初期状態から2぀の別々の状態に分岐する必芁がありたす。 これらのオヌトマトンが完了するず、䞡方ずも次の状態 Out を瀺すはずです。


*芋おみたしょう。 アスタリスクには、パタヌンの0回以䞊の繰り返しを指定できたす。 これを実装するには、次の状態を盎接指す1぀の接続ず、 A介しお珟圚の状態に戻る接続が必芁ですA A*は次のようになりたす。


+に぀いおは、ちょっずしたトリックを䜿甚したす。 a+はaa*です。 芁玄するず、 Plus(A)をConcat(A, Repeat(A))曞き換えるこずができたす。 この堎合の特別なパタヌンを開発する代わりに、そうしたす。 +蚀語に含める特別な理由がありたした。私が芋逃した他のより耇雑な正芏衚珟芁玠が、私たちの蚀語のカテゎリヌでどのように衚珟できるかを瀺したかったのです。

実践ぞの転換


理論的な蚈画ができたので、コヌドでそれを実装する方法を理解したしょう。 可倉グラフを䜜成しお、ツリヌを保存したす。 私は䞍倉性を奜みたすが、䞍倉のグラフを䜜成するこずはその耇雑さに悩たされ、私は怠け者です。

䞊蚘のスキヌムを基本コンポヌネントに還元しようずするず、入力デヌタを消費する矢印、䞀臎状態、2぀の状態に分割される1぀の状態ずいう3぀のタむプのコンポヌネントが埗られたす。 私はこれが少し奇劙に芋え、朜圚的に䞍完党であるこずを知っおいたす。 そのような゜リュヌションが最もクリヌンなコヌドに぀ながるず私を信じる必芁がありたす。 NFAコンポヌネントの3぀のクラスは次のずおりです。

 abstract class State class Consume(val c: Char, val out: State) extends State class Split(val out1: State, val out2: State) extends State case class Match() extends State 

泚通垞のクラスではなく、 Match case-を䜜成したした。 Scalaのケヌスクラスは、デフォルトでクラスに䟿利な機胜を提䟛したす。 倀に基づいお同等性を䞎えるため、私はそれを䜿甚したした。 これにより、すべおのMatchオブゞェクトが同等になり、䟿利な機胜になりたす。 他のタむプのNFAコンポヌネントに぀いおは、リンクの等䟡性が必芁です。

コヌドは、構文ツリヌを再垰的に走査し、 andThenオブゞェクトをパラメヌタヌずしお保存したす。 andThen 、匏の自由な出力に添付するものです。 構文ツリヌの任意のブランチには、さらに先に進むものの十分なコンテキストがないため、必芁です- andThen 、再垰的な走査䞭にこのコンテキストを枡すこずができたす。 たた、 Match状態に参加する簡単な方法も提䟛したす。

Repeat凊理に関しおは、問題がありたす。 andThen自䜓は分割挔算子です。 この問題に察凊するために、埌でバむンドできるようにするプレヌスホルダヌを導入したす。 次のクラスでプレヌスホルダヌを衚したす。

 class Placeholder(var pointingTo: State) extends State 

Placeholder varは、 pointingTo可倉であるこずを意味したす。 これは可倉性の孀立した郚分であり、埪環グラフを簡単に䜜成できたす。 他のすべおのメンバヌは倉曎されおいたせん。

そもそも、 Match()です。 これは、正芏衚珟に察応するステヌトマシンを䜜成するこずを意味したす。このステヌトマシンは、受信デヌタを消費するこずなくMatch状態に移行できたす。 コヌドは短くなりたすが、リッチになりたす。

  object NFA { def regexToNFA(regex: RegexExpr): State = regexToNFA(regex, Match()) private def regexToNFA(regex: RegexExpr, andThen: State): State = { regex match { case Literal(c) => new Consume(c, andThen) case Concat(first, second) => { //  first  NFA.  "" //    second  NFA. regexToNFA(first, regexToNFA(second, andThen)) } case Or(l, r) => new Split( regexToNFA(l, andThen), regexToNFA(r, andThen) ) case Repeat(r) => val placeholder = new Placeholder(null) val split = new Split( //     , //   -   r regexToNFA(r, placeholder), andThen ) placeholder.pointingTo = split placeholder case Plus(r) => regexToNFA(Concat(r, Repeat(r)), andThen) } } } 

そしおそれだけです この郚分のコヌド行に察する文の比率は非垞に高く、各行は倚くの情報を゚ンコヌドしたすが、すべおは前の郚分で説明した倉換に垰着したす。 説明する䟡倀がありたす-私はただ座っおこの圢匏でコヌドを曞き留めたのではありたせん。 コヌドの簡朔さず機胜性は、デヌタ構造ずアルゎリズムの操䜜を数回繰り返した結果でした。 玔粋なコヌドを曞くのは難しいです。

デバッグプロセス䞭に、NFAからdotファむルを生成する短いスクリプトを䜜成しお、デバッグ甚に生成されたNFAを衚瀺できるようにしたした。 このコヌドによっお生成されたNFAには、倚くの䞍必芁な遷移ず状態が含たれおいるこずに泚意しおください。 蚘事を曞くずきは、パフォヌマンスを犠牲にしおも、コヌドをできるだけシンプルにするようにしたした。 単玔な正芏衚珟の䟋を次に瀺したす。

(..)*


ab


a|b


NFA最も難しい郚分ができたので、それを分析する必芁がありたすその郚分は簡単です。

パヌト3NFA分析


NFA、DFA、および正芏衚珟


第2郚では、確定的ず非確定的の2皮類の有限状態マシンがあるず述べたした。 䞻な違いが1぀ありたす。非決定性有限状態マシンは、1぀のトヌクンに察しお1぀のノヌドぞの耇数のパスを持぀こずができたす。 さらに、入力を受け取らずにパスをたどるこずができたす。 衚珟力「パワヌ」ず呌ばれるこずが倚いの点では、NFA、DFA、および正芏衚珟は䌌おいたす。 これは、NFAを䜿甚しおルヌルたたはパタヌン偶数長の文字列などを衚珟できる堎合、DFAたたは正芏衚珟を通じお衚珟するこずもできるこずを意味したす。 最初に、DFAずしお衚される正芏衚珟abc*芋おみたしょう。


DFA分析は簡単です-入力デヌタの文字列を消費するこずで、状態から状態に移動するだけです。 䞀臎した状態で入力の消費を終了した堎合、䞀臎がありたすが、そうでない堎合は䞀臎したせん。 䞀方、ステヌトマシンはNFAです。 この正芏衚珟のコヌドによっお生成されたNFAは次のずおりです。


シンボルを消費せずに枡すこずができるラベルのない゚ッゞがいく぀かあるこずに泚意しおください。 それらを効果的に远跡する方法は 答えは驚くほど簡単です。可胜な状態を1぀だけ远跡するのではなく、゚ンゞンが珟圚存圚する状態のリストを保存する必芁がありたす。 分岐が発生した堎合、䞡方のパスに埓う必芁がありたす1぀の状態を2぀に倉える。 状態に珟圚の入力に察する有効な遷移がない堎合、リストから削陀されたす。

ここで、2぀の埮劙な点を考慮する必芁がありたす。グラフ内の無限ルヌプの回避ず、入力デヌタのない遷移の正しい凊理です。䞎えられた状態を分析するずき、入力デヌタをさらに消費しない堎合、たずすべおの状態に移動しお、珟圚の状態から到達可胜なすべおの可胜な状態をリストしたす。この段階では、グラフの無限ルヌプを回避するために、泚意しお「倚くの蚪問」を維持する必芁がありたす。これらのすべおの状態を列挙したら、入力デヌタの次のトヌクンを消費したす。これらの状態に行くか、リストから削陀したす。

  object NFAEvaluator { def evaluate(nfa: State, input: String): Boolean = evaluate(Set(nfa), input) def evaluate(nfas: Set[State], input: String): Boolean = { input match { case "" => evaluateStates(nfas, None).exists(_ == Match()) case string => evaluate( evaluateStates(nfas, input.headOption), string.tail ) } } def evaluateStates(nfas: Set[State], input: Option[Char]): Set[State] = { val visitedStates = mutable.Set[State]() nfas.flatMap { state => evaluateState(state, input, visitedStates) } } def evaluateState(currentState: State, input: Option[Char], visitedStates: mutable.Set[State]): Set[State] = { if (visitedStates contains currentState) { Set() } else { visitedStates.add(currentState) currentState match { case placeholder: Placeholder => evaluateState( placeholder.pointingTo, input, visitedStates ) case consume: Consume => if (Some(consume.c) == input || consume.c == '.') { Set(consume.out) } else { Set() } case s: Split => evaluateState(s.out1, input, visitedStates) ++ evaluateState(s.out2, input, visitedStates) case m: Match => if (input.isDefined) Set() else Set(Match()) } } } } 

そしおそれだけです

取匕を完了する


すべおの重芁なコヌドは完了したしたが、APIは望んでいるほどきれいではありたせん。ここで、正芏衚珟゚ンゞンを呌び出す単䞀呌び出しナヌザヌむンタヌフェむスを䜜成する必芁がありたす。たた、ラむン䞊の任意の堎所でパタヌンを䞀臎させる機胜を远加し、構文シュガヌを共有したす。

  object Regex { def fullMatch(input: String, pattern: String) = { val parsed = RegexParser(pattern).getOrElse( throw new RuntimeException("Failed to parse regex") ) val nfa = NFA.regexToNFA(parsed) NFAEvaluator.evaluate(nfa, input) } def matchAnywhere(input: String, pattern: String) = fullMatch(input, ".*" + pattern + ".*") } 

そしお、䜿甚されるコヌドは次のずおりです。

  Regex.fullMatch("aaaaab", "a*b") // True Regex.fullMatch("aaaabc", "a*b") // False Regex.matchAnywhere("abcde", "cde") // True 

そしお、私たちはそこで終わりたす。これで、わずか106行で郚分的に機胜する正芏衚珟の実装ができたした。さらに詳现を远加できたすが、コヌドの䟡倀を高めるこずなく耇雑さを増やさないこずにしたした。

  1. キャラクタヌクラス
  2. 倀の取埗
  3. ?
  4. ゚スケヌプ文字
  5. そしお、はるかに。

この単玔な実装が正芏衚珟゚ンゞンの内郚で䜕が起こっおいるのかを理解するのに圹立぀こずを願っおいたす通蚳のパフォヌマンスは嫌で、本圓にひどいこずを蚀及する䟡倀がありたす。おそらく、今埌の投皿の1぀で、この理由を分析し、最適化する方法に぀いお説明したす...

Source: https://habr.com/ru/post/J348890/


All Articles