PythonとGraphvizを使用した正規表現のアニメーション化

正規表現は評判が悪い。それらが言及されるときはいつでも、それは絶対にナンセンスのように見えるテキストの恐ろしい壁の画像を呼び出すようです。たとえば、これは電子メールアドレスを検証するための一般的に引用される正規表現です。

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

うわぁ。この記事の終わりまでにその表現を理解するふりをするつもりはありませんが、少なくとも、理解するのがそれほど難しくない単純なルールに基づいて構築されていることを示したいと思います。

あなたは疑問に思うかもしれません、なぜあなたはこれらのものがそもそもどのように機能するかについてさえ気にする必要がありますか?いくつかの理由があると思います。1つ目は、基本を理解すると、優れた正規表現の書き方を覚えやすくなるということです。

私は間違いなく、正規表現を作成した後、何ヶ月もそれを見る必要がなかった複数の状況にありました。やがて戻ってきたとき、すべてを忘れてしまい、一からやり直さなければなりませんでした。構文だけでなく正規表現の背後にある考え方を理解することで、この問題を回避できます。

また、これは私の肩にある独学のプログラマーチップかもしれませんが、理論計算機科学の世界を覗いてみるのはやりがいがあると思います。正規表現は、理論計算機科学から抜け出し、日常のプログラマーが使用するようになった数少ない概念の1つであるように思われます。正規表現エンジンを理解することは、有限状態オートマトンのようないくつかの気の利いた概念に取り組むための実用的な機会を提供します。

概念を理解するための絶対的な最善の方法は、それを視覚化することだと思います。PythonとGraphvizを使用して正規表現エンジンを構築しました。これは、正規表現がテキストの本文を検索しているときに実際に何が行われるかをアニメーション化するものです。独自の例を試してみたい場合は、プロジェクトはGitHubで公開されています。今後のデモとして、S+NAKESSSSNAKEというテキストを検索する正規表現のアニメーションを次に示します。

バックグラウンド

正規表現の概念の根底にある理論はたくさんありますが、正規表現エンジンを実装するために必要な最低限の要素を説明しようと思います。

まず、正規表現の具体的な定義が必要です。ウィキペディアでは、これを「テキスト内の検索パターンを指定する一連の文字」と定義しています。正規表現内のほとんどの文字は同じように扱われますが、メタ文字(*、+、?、|)と呼ぶ特別な文字がいくつかあります。これらには独自の機能があり、後で説明します。

エンジンの中核となるのは、決定性有限状態オートマトン(DFA)です。派手に聞こえますが、実際には、開始ノードと終了(または受け入れ)ノードを持つ有向グラフにすぎません。DFAは、いくつかの入力に基づいて状態を変更することによって動作します。すべての入力が読み取られた後、DFAの状態を評価します。受け入れ状態の場合は、を返しますTrue。それ以外の場合は、を返しますFalse

上記のDFAでは、開始状態から受け入れ状態に移行する唯一の方法は、シーケンス「BAT」を渡すことです。この例は単純に見えるかもしれませんが、任意の長さの入力や複雑な文字シーケンスに拡張できます。したがって、理想的には、正規表現をDFAに変換する方法を見つけたいと思います。

救助への理論!Kleeneの定理は、正規表現には、同じ文字列のセットを指定できるDFAが存在し、その逆も可能であると述べています。これは、前述の非常識な電子メール正規表現検証をDFAに変換できるアルゴリズムがあることを意味します。それがその形になると、コンピュータはそれを簡単に処理することができます。

そのアルゴリズムの構築を開始する前に、もう1つ注意点があります。正規表現をDFAに変換するには、計算コストが非常に高くなる可能性があります。

代わりに、それを非決定性有限状態オートマトン(NFA)に変えることができます。主な違いは、NFAが一度に複数の状態になる可能性があることと、追加の入力文字をスキャンせずに異なる状態に移行できることです。これは少し紛らわしいように聞こえるかもしれませんが、次の例で明らかになると思います。

正規表現→NFA

エンジンがサポートするメタキャラクターの概要は次のとおりです。

  • スター(* ):キャラクターに0回以上一致します。
  • プラス(+):キャラクターに1回以上一致します。
  • 質問(?):文字に0回または1回一致します。
  • ピリオド(. ):ワイルドカード演算子とも呼ばれ、任意の文字に一致します。
  • 括弧(()):部分式をカプセル化します。
  • 垂直バー(|):or演算子とも呼ばれ、部分式内の複数の要素に一致します。

[]以前に正規表現を使用したことがある場合は、角かっこ( )や中かっこ({})などのいくつかのメタ文字が欠落していることに気付くでしょう。ただし、エンジンには、これらの欠落している文字によって実行される操作を実装するためのすべての機能があります。

サポートされていない式[ABC]は、サポートされている式と同等(A|B|C)です。同様に、A{2, 3}と同等AAA?です。これらのメタ文字を追加することは完全に可能ですが、グラフィック表現が複雑になるため、それらを除外することにしました。

(A*B|AC)D例として正規表現を使用して、変換プロセスを示します。まず、正規表現を括弧で囲んで少し前処理する必要があります。次に、正規表現内の各文字のノードを作成します。また、受け入れ状態を表すために、最後の空白ノードを1つ含めます。この時点で、NFAは次のようになります。

次に、マッチトランジションエッジを黒で追加します。これらのエッジは、アルファベットの文字が含まれるノードに対応するエッジと考えることができます。これらのエッジは、テキストからスキャンした文字がノードの文字と一致する場合にのみ追跡されます。一致遷移エッジを追加するためのロジックは単純です。ノードにメタ文字が含まれていない場合は、そのノードから次のノードへの一致遷移を追加します。

最も難しい部分は、イプシロン遷移エッジを追加することです。これらのエッジは、メタ文字を含むノードに対応するエッジと考えることができます。これらのエッジはメタ文字ごとに異なり、括弧の配置によっても影響を受けます。たとえば、スターオペレーターが正規表現に含まれている場合は常に、3つの別個のイプシロン遷移エッジが必要です。1つはその後の状態、もう1つはその前の状態、もう1つは前の状態から星に戻ります。

すべてのイプシロン遷移エッジを追加すると、NFAは完了します。

NFAパターンマッチング

NFAが完全に構築されたので、テキストの本文でNFAを実行し、状態から状態への遷移を観察できます。NFAが最終的な受け入れ状態に到達した場合、一致します。テキストのスキャンを終了し、受け入れ状態に到達しない場合、一致するものは見つかりません。

NFAを実行するための基本的なパターンは次のとおりです。

  1. テキストの最初の文字をスキャンする前に、アクティブ状態と呼ばれるリストを作成し、それにNFAの最初のノードを追加します。
  2. アクティブ状態のすべてのノードからすべての到達可能な状態へのイプシロン遷移を取得します。到達可能なすべての状態を候補状態のリストに入れます。
  3. テキスト内の次の文字をスキャンします。
  4. アクティブ状態のリストをクリアします。候補状態のいずれかの状態がテキスト内の文字と一致する場合は、一致遷移を次の状態にして、アクティブ状態の新しいリストに追加します。
  5. 受け入れ状態に達するか、テキストの終わりに達するまで、手順2〜4を繰り返します。

この手順のPythonコードは次のとおりです。


def recognize(text, regex, match_transitions, epsilon_transitions):
  active_states = [0]
  
  # get candidate states before scanning first character
  candidate_states = digraph_dfs(epsilon_transitions, active_states[0])
  candidate_chars = [regex[state] for state in epsilon_states]
  
  for i, letter in enumerate(text):
    # get epsilon transition states that match letter of input text
    matched_states = []
    [matched_states.append(state) for state, char in zip(candidate_states, candidate_chars) if
     char == letter or char == "."]

    # take match transition from matched state to next state
    active_states = []
    [active_states.extend(match_transitions[node]) for node in matched_states]

    # get next epsilon transitions
    candidate_states = []
    [candidate_states.extend(digraph_dfs(epsilon_transitions, node)) for node in active_states]
    candidate_chars = [regex[state] for state in candidate_states]

    # check if nfa has reached the accepting state
    if len(regex) in candidate_states:
        return True
  
  # if we've processed all text and haven't reached accepting state, return False 
  return False

視覚的な例として、前に作成したNFAを本文で実行します。テキストAABDを検索して、一致するかどうかを確認します。最初のステップは、AABDの最初の文字がスキャンされる前に、考えられるすべてのイプシロン遷移を取得することです。

テキストの最初の文字をスキャンする前に、イプシロン遷移を通じて利用可能な状態を見つける

NFAは、最初のステップですでに6つの異なる候補状態にあります。次に、テキストの最初の文字までスキャンします。

テキストの最初の文字を読む

2つのノードには、Aノード4とノード8からの一致遷移があります。次のステップは、これらのノードから一致遷移を取得することです。

A→*およびA→Cからの一致遷移

ここから、プロセスはまったく同じ方法で繰り返されます。アクティブな状態から利用可能なすべてのイプシロン遷移を取得し、次の文字をスキャンして、次の一致遷移を取得します。プロセス全体を以下にアニメーション化します。

NFA検索プロセス全体、最終的には受け入れ状態になります

最後の考え

正規表現エンジンの内部操作をよりよく理解して、この記事から離れることができたと思います。さらに明確にするために、ロバート・セッジウィック教授によるこれらのビデオ講義を強くお勧めします。

完全に操作しないと理解するのは難しいと思うので、これを読んでいる人は、独自の正規表現アニメーションを作成するか、正規表現デバッガーをいじってみることをお勧めします。

視覚化を改善する方法について質問や提案があれば、ぜひ聞いてみてください。読んでくれてありがとう!

ソース:https ://betterprogramming.pub/animating-regular-expressions-with-python-and-graphviz-e0df447b827a

#graphviz #python 

What is GEEK

Buddha Community

PythonとGraphvizを使用した正規表現のアニメーション化

PythonとGraphvizを使用した正規表現のアニメーション化

正規表現は評判が悪い。それらが言及されるときはいつでも、それは絶対にナンセンスのように見えるテキストの恐ろしい壁の画像を呼び出すようです。たとえば、これは電子メールアドレスを検証するための一般的に引用される正規表現です。

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

うわぁ。この記事の終わりまでにその表現を理解するふりをするつもりはありませんが、少なくとも、理解するのがそれほど難しくない単純なルールに基づいて構築されていることを示したいと思います。

あなたは疑問に思うかもしれません、なぜあなたはこれらのものがそもそもどのように機能するかについてさえ気にする必要がありますか?いくつかの理由があると思います。1つ目は、基本を理解すると、優れた正規表現の書き方を覚えやすくなるということです。

私は間違いなく、正規表現を作成した後、何ヶ月もそれを見る必要がなかった複数の状況にありました。やがて戻ってきたとき、すべてを忘れてしまい、一からやり直さなければなりませんでした。構文だけでなく正規表現の背後にある考え方を理解することで、この問題を回避できます。

また、これは私の肩にある独学のプログラマーチップかもしれませんが、理論計算機科学の世界を覗いてみるのはやりがいがあると思います。正規表現は、理論計算機科学から抜け出し、日常のプログラマーが使用するようになった数少ない概念の1つであるように思われます。正規表現エンジンを理解することは、有限状態オートマトンのようないくつかの気の利いた概念に取り組むための実用的な機会を提供します。

概念を理解するための絶対的な最善の方法は、それを視覚化することだと思います。PythonとGraphvizを使用して正規表現エンジンを構築しました。これは、正規表現がテキストの本文を検索しているときに実際に何が行われるかをアニメーション化するものです。独自の例を試してみたい場合は、プロジェクトはGitHubで公開されています。今後のデモとして、S+NAKESSSSNAKEというテキストを検索する正規表現のアニメーションを次に示します。

バックグラウンド

正規表現の概念の根底にある理論はたくさんありますが、正規表現エンジンを実装するために必要な最低限の要素を説明しようと思います。

まず、正規表現の具体的な定義が必要です。ウィキペディアでは、これを「テキスト内の検索パターンを指定する一連の文字」と定義しています。正規表現内のほとんどの文字は同じように扱われますが、メタ文字(*、+、?、|)と呼ぶ特別な文字がいくつかあります。これらには独自の機能があり、後で説明します。

エンジンの中核となるのは、決定性有限状態オートマトン(DFA)です。派手に聞こえますが、実際には、開始ノードと終了(または受け入れ)ノードを持つ有向グラフにすぎません。DFAは、いくつかの入力に基づいて状態を変更することによって動作します。すべての入力が読み取られた後、DFAの状態を評価します。受け入れ状態の場合は、を返しますTrue。それ以外の場合は、を返しますFalse

上記のDFAでは、開始状態から受け入れ状態に移行する唯一の方法は、シーケンス「BAT」を渡すことです。この例は単純に見えるかもしれませんが、任意の長さの入力や複雑な文字シーケンスに拡張できます。したがって、理想的には、正規表現をDFAに変換する方法を見つけたいと思います。

救助への理論!Kleeneの定理は、正規表現には、同じ文字列のセットを指定できるDFAが存在し、その逆も可能であると述べています。これは、前述の非常識な電子メール正規表現検証をDFAに変換できるアルゴリズムがあることを意味します。それがその形になると、コンピュータはそれを簡単に処理することができます。

そのアルゴリズムの構築を開始する前に、もう1つ注意点があります。正規表現をDFAに変換するには、計算コストが非常に高くなる可能性があります。

代わりに、それを非決定性有限状態オートマトン(NFA)に変えることができます。主な違いは、NFAが一度に複数の状態になる可能性があることと、追加の入力文字をスキャンせずに異なる状態に移行できることです。これは少し紛らわしいように聞こえるかもしれませんが、次の例で明らかになると思います。

正規表現→NFA

エンジンがサポートするメタキャラクターの概要は次のとおりです。

  • スター(* ):キャラクターに0回以上一致します。
  • プラス(+):キャラクターに1回以上一致します。
  • 質問(?):文字に0回または1回一致します。
  • ピリオド(. ):ワイルドカード演算子とも呼ばれ、任意の文字に一致します。
  • 括弧(()):部分式をカプセル化します。
  • 垂直バー(|):or演算子とも呼ばれ、部分式内の複数の要素に一致します。

[]以前に正規表現を使用したことがある場合は、角かっこ( )や中かっこ({})などのいくつかのメタ文字が欠落していることに気付くでしょう。ただし、エンジンには、これらの欠落している文字によって実行される操作を実装するためのすべての機能があります。

サポートされていない式[ABC]は、サポートされている式と同等(A|B|C)です。同様に、A{2, 3}と同等AAA?です。これらのメタ文字を追加することは完全に可能ですが、グラフィック表現が複雑になるため、それらを除外することにしました。

(A*B|AC)D例として正規表現を使用して、変換プロセスを示します。まず、正規表現を括弧で囲んで少し前処理する必要があります。次に、正規表現内の各文字のノードを作成します。また、受け入れ状態を表すために、最後の空白ノードを1つ含めます。この時点で、NFAは次のようになります。

次に、マッチトランジションエッジを黒で追加します。これらのエッジは、アルファベットの文字が含まれるノードに対応するエッジと考えることができます。これらのエッジは、テキストからスキャンした文字がノードの文字と一致する場合にのみ追跡されます。一致遷移エッジを追加するためのロジックは単純です。ノードにメタ文字が含まれていない場合は、そのノードから次のノードへの一致遷移を追加します。

最も難しい部分は、イプシロン遷移エッジを追加することです。これらのエッジは、メタ文字を含むノードに対応するエッジと考えることができます。これらのエッジはメタ文字ごとに異なり、括弧の配置によっても影響を受けます。たとえば、スターオペレーターが正規表現に含まれている場合は常に、3つの別個のイプシロン遷移エッジが必要です。1つはその後の状態、もう1つはその前の状態、もう1つは前の状態から星に戻ります。

すべてのイプシロン遷移エッジを追加すると、NFAは完了します。

NFAパターンマッチング

NFAが完全に構築されたので、テキストの本文でNFAを実行し、状態から状態への遷移を観察できます。NFAが最終的な受け入れ状態に到達した場合、一致します。テキストのスキャンを終了し、受け入れ状態に到達しない場合、一致するものは見つかりません。

NFAを実行するための基本的なパターンは次のとおりです。

  1. テキストの最初の文字をスキャンする前に、アクティブ状態と呼ばれるリストを作成し、それにNFAの最初のノードを追加します。
  2. アクティブ状態のすべてのノードからすべての到達可能な状態へのイプシロン遷移を取得します。到達可能なすべての状態を候補状態のリストに入れます。
  3. テキスト内の次の文字をスキャンします。
  4. アクティブ状態のリストをクリアします。候補状態のいずれかの状態がテキスト内の文字と一致する場合は、一致遷移を次の状態にして、アクティブ状態の新しいリストに追加します。
  5. 受け入れ状態に達するか、テキストの終わりに達するまで、手順2〜4を繰り返します。

この手順のPythonコードは次のとおりです。


def recognize(text, regex, match_transitions, epsilon_transitions):
  active_states = [0]
  
  # get candidate states before scanning first character
  candidate_states = digraph_dfs(epsilon_transitions, active_states[0])
  candidate_chars = [regex[state] for state in epsilon_states]
  
  for i, letter in enumerate(text):
    # get epsilon transition states that match letter of input text
    matched_states = []
    [matched_states.append(state) for state, char in zip(candidate_states, candidate_chars) if
     char == letter or char == "."]

    # take match transition from matched state to next state
    active_states = []
    [active_states.extend(match_transitions[node]) for node in matched_states]

    # get next epsilon transitions
    candidate_states = []
    [candidate_states.extend(digraph_dfs(epsilon_transitions, node)) for node in active_states]
    candidate_chars = [regex[state] for state in candidate_states]

    # check if nfa has reached the accepting state
    if len(regex) in candidate_states:
        return True
  
  # if we've processed all text and haven't reached accepting state, return False 
  return False

視覚的な例として、前に作成したNFAを本文で実行します。テキストAABDを検索して、一致するかどうかを確認します。最初のステップは、AABDの最初の文字がスキャンされる前に、考えられるすべてのイプシロン遷移を取得することです。

テキストの最初の文字をスキャンする前に、イプシロン遷移を通じて利用可能な状態を見つける

NFAは、最初のステップですでに6つの異なる候補状態にあります。次に、テキストの最初の文字までスキャンします。

テキストの最初の文字を読む

2つのノードには、Aノード4とノード8からの一致遷移があります。次のステップは、これらのノードから一致遷移を取得することです。

A→*およびA→Cからの一致遷移

ここから、プロセスはまったく同じ方法で繰り返されます。アクティブな状態から利用可能なすべてのイプシロン遷移を取得し、次の文字をスキャンして、次の一致遷移を取得します。プロセス全体を以下にアニメーション化します。

NFA検索プロセス全体、最終的には受け入れ状態になります

最後の考え

正規表現エンジンの内部操作をよりよく理解して、この記事から離れることができたと思います。さらに明確にするために、ロバート・セッジウィック教授によるこれらのビデオ講義を強くお勧めします。

完全に操作しないと理解するのは難しいと思うので、これを読んでいる人は、独自の正規表現アニメーションを作成するか、正規表現デバッガーをいじってみることをお勧めします。

視覚化を改善する方法について質問や提案があれば、ぜひ聞いてみてください。読んでくれてありがとう!

ソース:https ://betterprogramming.pub/animating-regular-expressions-with-python-and-graphviz-e0df447b827a

#graphviz #python