web-dev-qa-db-ja.com

正規表現用のパーサーを書く

何年もプログラミングをしていても、正規表現を完全に把握したことがないのは恥ずかしいことです。一般に、問題が正規表現を必要とするとき、私は通常(多くの構文を参照した後)適切なものを見つけることができますが、それは私がますます頻繁に使用していると感じるテクニックです。

ですから、自分自身を教え、正規表現を適切に理解するために、私は何かを学ぼうとするときにいつもやることを決めました。つまり、十分に学んだと思うとすぐにおそらく放棄するという野心的なものを書くようにしてください。

このため、Pythonで正規表現パーサーを作成します。この場合、「十分に学ぶ」とは、Perlの拡張正規表現構文を完全に理解できるパーサーを実装することを意味します。ただし、最も効率的なパーサーである必要はなく、実際に使用する必要はありません。単に文字列内のパターンと正しく一致するか、または一致しなければならないだけです。

質問は、どこから始めればいいですか?正規表現が何らかの方法で有限状態オートマトンに関与するという事実を除いて、正規表現がどのように解析され解釈されるかについてはほとんど何も知りません。このやっかいな問題にアプローチする方法についての提案は大歓迎です。

編集: Pythonの正規表現パーサーを実装している間、私は過度に大騒ぎしていないことを明確にする必要があります例や記事がどのプログラミング言語で書かれているかについて。それがBrainfuckにない限り、おそらくそれを十分に理解して、しばらくの間それを価値があるものにするでしょう。

66
Chinmay Kanchi

正規表現エンジンの実装を記述することは、実際には非常に複雑なタスクです。

しかし、実際に実装するのに十分な詳細を理解できない場合でも、その方法に興味がある場合は、少なくともこの記事を参照することをお勧めします。

正規表現のマッチングは簡単かつ高速にできます(ただし、Java、Perl、PHP、Python、Rubyなどでは低速です)

人気のあるプログラミング言語の多くが正規表現を実装する方法を説明し、一部の正規表現では非常に遅くなる可能性があります。また、わずかに異なる高速な方法を説明します。この記事には、Cのソースコードを含む、提案された実装の動作方法の詳細が含まれています。正規表現を習い始めたばかりの場合は少し読みにくいかもしれませんが、この2つの違いを知っておく価値は十分にあると思いますアプローチ。

37
Mark Byers

Mark Byersにはすでに+1を与えましたが、私が覚えている限りでは、あるアルゴリズムが悪い理由と別のアルゴリズムが優れている理由を説明する以上に、正規表現のマッチングの仕組みについてはあまり言及していません。リンクに何かあるのでしょうか?

良いアプローチ-有限オートマトンの作成に焦点を当てます。最小化せずに決定的オートマトンに制限する場合、これはそれほど難しくありません。

(非常に簡単に)説明するのは、 Modern Compiler Design で行われたアプローチです。

次の正規表現があると想像してください...

a (b c)* d

文字は一致するリテラル文字を表します。 *は、通常の0回以上の繰り返し一致です。基本的な考え方は、点線の規則に基づいて状態を導出することです。状態ゼロは、まだ何も一致していない状態と見なします。そのため、ドットは先頭にあります...

0 : .a (b c)* d

唯一の可能な一致は「a」であるため、次の状態は...

1 : a.(b c)* d

2つの可能性があります-「b」に一致する(「b c」の繰り返しが少なくとも1つある場合)またはそうでない場合に「d」に一致する注-基本的にここでは有向グラフ検索(深さ優先または幅優先)を行っていますが、検索中に有向グラフを検出しています。幅優先の戦略を想定して、後で検討するためにケースの1つをキューに入れる必要がありますが、今後はその問題を無視します。とにかく、2つの新しい状態を発見しました...

2 : a (b.c)* d
3 : a (b c)* d.

状態3は終了状態です(複数ある場合があります)。状態2の場合、「c」のみを一致させることができますが、その後はドットの位置に注意する必要があります。 「a。(b c)* d」を取得します-これは状態1と同じなので、新しい状態は必要ありません。

IIRC、Modern Compiler Designのアプローチは、ドットの処理を簡素化するために、演算子にヒットしたときにルールを変換することです。状態1は...に変換されます.

1 : a.b c (b c)* d
    a.d

つまり、次のオプションは、最初の繰り返しに一致させるか、繰り返しをスキップすることです。これからの次の状態は、状態2および3と同等です。このアプローチの利点は、将来の一致のみを考慮するため、過去の一致(「。」の前のすべて)を破棄できることです。これは通常、より小さな状態モデルを提供します(ただし、必ずしも最小の状態モデルではありません)。

[〜#〜] edit [〜#〜]すでに一致した詳細を破棄する場合、状態の説明は文字列セットの表現になります。この時点から発生します。

抽象代数の観点では、これは一種の集合閉包です。代数は基本的に1つ(またはそれ以上)の演算子を持つ集合です。セットは状態の説明であり、演算子は遷移(文字の一致)です。閉じたセットとは、セット内のメンバーに演算子を適用すると、常にセット内にある別のメンバーが生成されるセットです。セットの閉包は、閉じられている最小の大きなセットです。したがって、基本的に、明白な開始状態から始めて、遷移演算子のセットに対して閉じられた状態の最小セット、つまり到達可能な状態の最小セットを構築しています。

ここで最小とは、クロージャプロセスを指します。通常、最小と呼ばれるより小さな同等のオートマトンが存在する場合があります。

この基本的な考え方を念頭に置いて、「2組の文字列を表す2つのステートマシンがある場合、結合を表す3番目のステートマシンを導出する方法」(または交差、またはセットの差...)と言うのはそれほど難しくありません。ドット表記規則の代わりに、状態表現は各入力オートマトンからの現在の状態(または現在の状態のセット)と、おそらく追加の詳細になります。

通常の文法が複雑になっている場合は、最小限に抑えることができます。ここでの基本的な考え方は比較的単純です。すべての状態を1つの等価クラスまたは「ブロック」にグループ化します。次に、特定の遷移タイプに関して、ブロックを分割する必要があるかどうか(状態は実際には同等ではない)を繰り返しテストします。特定のブロック内のすべての状態が同じ文字の一致を受け入れ、そうすることで同じ次のブロックに到達できる場合、それらは同等です。

Hopcroftsアルゴリズムは、この基本的なアイデアを処理する効率的な方法です。

最小化に関して特に興味深いのは、すべての決定性有限オートマトンが1つの最小形式を持っていることです。さらに、Hopcroftsアルゴリズムは、それがどのような大きなケースから始まったとしても、その最小形式の同じ表現を生成します。つまり、これはハッシュの導出または任意だが一貫性のある順序付けに使用できる「標準」表現です。つまり、コンテナへのキーとして最小限のオートマトンを使用できるということです。

上記はおそらく少し雑なWRT定義なので、自分で使用する前に自分で用語を調べてください。しかし、運が良ければ、基本的な考え方を簡単に紹介できます。

ところで- Dick Grunes site の残りの部分を見てみましょう-彼は解析技術に関する無料のPDF本を持っています。 Modern Compiler Designの第1版は非常に優れたIMOですが、おわかりのように、第2版が間近に迫っています。

21
Steve314

このペーパー は興味深いアプローチを採用しています。実装はHaskellで提供されますが、 Pythonで再実装 少なくとも1回は行われました。

7
dhaffey

Beautiful Code には、「A Regular Expression Matcher」と適切に呼ばれる興味深い(少し短い場合)章があります。その中で、彼はリテラル文字と.^$*記号。

6
Richard Fearn

正規表現エンジンを作成すると理解が向上することに同意しますが、ANTLRをご覧になりましたか?あらゆる種類の言語のパーサーを自動的に生成します。 Grammarの例 にリストされている言語文法の1つを使用して、ASTとそれが生成するパーサーを実行してみてください。コードですが、パーサーがどのように機能するかについて十分に理解できます。

1
A_Var