正規表現の微妙さ。 パート2:払い戻しとその数量

パート1:文字クラスの内外のメタキャラクター

この部分では、正規表現エンジンの仕組み、正規表現が非常に遅いと考える人、なぜ多くのエンジンの作成者がPOSIX標準に従っていないのかについてお話したいと思います。

多くのタスクと同様に、「正規表現の文字列で一致を見つける」という問題を解決するための多くのアプローチと原則があります。 この場合の基本原則は、2:正規表現によって制御される一致する検索と、文字列によって駆動される一致する検索です。 これらの各原則の意味を詳しく見てみましょう。

ストリング駆動型マッチング


このタイプの正規表現エンジンは、いわゆるDFA( 決定性有限状態マシン )を使用して実装されます。 主な利点は、(別の原則と比較して)一致を見つけるのに非常に速いことです。 当然、この原則には多くの欠点があります(そうでなければ、面白くないでしょう?)。

第一に、コンパイル時間(プログラムのソースコードをコンパイルするのではなく、正規表現をエンジンの内部構造にコンパイルすることを意味します)は非常に長い場合があります(別の原則と比較して)。 実際、コンパイル中に、特定のパターンを比較するためのプログラム全体が作成されます。 DFAは文字列によって「制御」されており、コンパイル時に文字列がわからないため、考えられるすべての文字列を考慮してアルゴリズムが作成されます。 複雑な式の場合、これは明らかにそれほど高速ではありません。

第二に、DKAは、正規表現で慣れている便利で必要な機能の多くをサポートしていません。 たとえば、保持ブラケットはサポートされていません。 もちろん、DKAで利用可能なセットでは、多かれ少なかれ実際の状況で行うことは不可能です。 したがって、DKAは純粋な形ではほとんどどこでも使用されていません。 しかし、いわゆる「ハイブリッド」エンジンでうまく使用されています。最も単純な場合、特定の正規表現に最も適したエンジンを選択します。

したがって、大量のテキストを検索する必要があると同時に、正規表現がそれほど複雑でない場合、DKAよりも優れたものを見つけることは困難です。

それでは、DKAはどのように機能しますか? このエンジンは、そのように機能するため、「ライン駆動」と呼ばれます。 ステートマシンは、一致を検出する行を読み取り、その行をパターンと比較します。 時間ごとに、アルゴリズムはパターンと文字列のどの部分が一致するかを「認識」します。 アルゴリズムが行全体を完全にスキャンすると、行の先頭に近い最長一致のみを選択し、それを回答として残します。

明確にするために、正規表現^abc\w+e$と2行abcdeabceがあると仮定します。 式が最初の行と一致することは明らかですが、2番目の行とは一致しません。

DKAメカニズムで一致の検索がどのように発生するかを考えてみましょう。 最初の行には次のシーケンスがあります。



2番目のケースで何が起こるか:


これらの2つの例で何がわかりますか? アルゴリズムは、一致するかどうかは関係ありません。 文字ごとに1行ずつスキャンし、パターンに基づいて一致を照合します。 作業の最後にいくつかの完全な一致がある場合、結果は、行の左端に最も近く、最も長いものになります。

実際、アルゴリズムが非常に高速に機能する理由が明らかになります。 場合を除いて、文字列の各文字は1回だけ表示されます。 また、パターンのコンパイルに時間がかかる理由も明らかになります。複雑なパターンの場合、部分一致を正しく実行するには、多くの条件と依存関係を考慮する必要があります。

現時点では、DKAエンジンはgrepユーティリティでエンジン選択メカニズムの一部として使用され、Tcl言語ではハイブリッドエンジンの一部として使用されます。おそらく他の場所で使用されますが、それについては知りません。

正規表現マッチング


このタイプの正規表現エンジンは、いわゆるNCA( 非決定的ステートマシン )を使用して実装されます。 主な利点は、(別の原則と比較して)多数の機能をサポートしていることです。 当然、この原理には多くの欠点もあります。

第一に、動作時間は非常に長くなる可能性があります(別の原理と比較して)。 入力文字列の長さの指数関数的漸近まで。 多くの一般的なエンジンは、これに対する保護さえ備えており、検索を中断します。

第二に、NKAは正規表現の記述方法に大きく依存します。 DFAが録音の形式でまったく同じである場合、時間と検索結果は同等の式でまったく同じになりますが、NKAの場合、これはまったくの問題です。 1つの式は指数漸近線を与えることができ、もう1つの式はほぼ線形です。 したがって、パターンを最適化するという問題は、完全な成長において生じます。 NKAは現在、ほとんどの正規表現エンジンで使用されています。

クラシックNKAの重要な機能は、最初の完全一致が見つかるとすぐに検索を中断することです。 つまり 場合によっては、検索結果はDKAとは大きく異なります。 これはアルゴリズムの機能です。

それでは、なぜエンジンの作者はPOSIXに従うのを急いでいないのでしょうか? 実際、DKA検索のセマンティクスは標準で修正されています。 つまり もちろん、文字通りDKAを使用すべきだとは言われていませんが、「最大長の左端の一致」について言われています。 しかし、NKAは最初のものだけを返し、最大の長さは返しません。 そして何をすべきか? POSIXに準拠するために、彼らは通常POSIX NKAと呼ばれるアルゴリズムの修正を思いつきました。 そして、それは単なるNCAよりもずっと長く機能します。 標準を満たすためには、正規表現で許可されるすべてのオプションを実行する必要があるためです。 さらに、なぜずっと長いのかを説明します。

コンパイル済み(DFAと同じ意味です)NKAは、パターンに基づいた命令であるシーケンシャルアルゴリズムです。 NCAの主要な機能は「リターン」です。 検索速度はその数に直接依存します。

戻り値は、例で最もよく説明されています。 DFAの例で使用したものと同じ2行と同じ正規表現を使用してみましょう。

1行目:


2番目のケースで何が起こるか:


理解を深めるために、数量詞+常にできるだけ多くの行文字をキャプチャすることを説明します。 したがって、最大と呼ばれます。 初心者が犯すよくある間違いはこれに関連しています。 パターン\(.+\)は、「ここはテキスト(大括弧)、ここはテキスト(大括弧)、ここはテキスト」という部分で一致し\(.+\)ブラケット)」。 というのは、処理段階で.+行全体が最後までキャプチャされ、シンボル「)」に到達するとすぐに、完全に一致するものがすぐに見つかります。 正規表現を少し変更してみましょう: \([^)]+\)そして今では、私たちを悩ます問題はもうありません。 しかし、別のものが登場しました(そして、それなしで何が起こるか)。 さて、「ここにテキストがあります(ブラケット(および別のブラケット内)、またテキスト)、そしてテキストがあります」が「(ブラケット(および別のブラケット内))」とキャプチャされますが、これももちろん正しくありません。キャプチャ段階では、2番目の閉じ括弧に到達せず、見つかった一致をすぐに1番目の括弧に返します。問題を解決することができます バランスチェック(つまり、チェック)。

実際、アルゴリズムが具体的にどのように機能するかを説明したとき、私は少しずるいです。 実際には、エンジンが最適化を適用する場合、このように動作します(パターンが^文字で始まる場合、行の先頭で一致するか、まったく一致しないことが明らかです)。 実際、前のステップで完全に一致するものが1つも見つからなかった場合、最適化を破棄しても、エンジンは行の次の各文字から始まるまったく同じ検索を実行します。 つまり 2行目では、連続して5回の検索が実行され(文字「a」の前から開始し、次に「b」、「c」、「e」、最後の文字「e」の後)、その後のみ一致がないと言えます。 DKAはすべてのケースを1パスで処理します。 しかし、偶然の一致がないNKAには5が必要です。

POSIX NKAエンジンの違いは、最初のケース(一致が見つかった場合)でも検索が停止されないことですが、一致がない場合のように、5つの検索すべてが実行されます。

リターンを実行する手法を検討したので、指数関数的検索時間がどこから来たかを推測できます。 場合によっては、アルゴリズムがトラップに陥り、膨大な数のリターンが発生します。 たとえば、式(\w+)*a考えます。 「bbb」行に適用すると、どうなりますか? 最初に、最初の数量詞+は文字列全体をキャプチャし、2番目の数量詞に制御を渡します。 行にこれ以上文字がないため、パターンの「a」文字に移動します。 行にない場合は、1文字ずつ戻り、新しいグループで空いた文字をインターセプトし、「a」をチェックし、再び一致するものがない、戻ります。 さらに、意味は明確でなければなりません。 このアルゴリズムは、2つの数量詞によるキャプチャーのために、考えられるすべての改行を反復します。 文字列「bbb」の場合、これらは「[bbb]」、「[bb] [b]」、「[b] [bb]」、「[b] [b] [b]」というオプションになります。 角括弧は、条件付きでキャプチャされたグループを示します。 この場合、パーティションの数は文字列の長さから指数関数的に増加します。

別の状況。 1メガバイトのテキストとビューパターンを想像してください.*a 検索するとどうなりますか? 各段階で、文字列のメガバイト全体がキャプチャされ、「a」文字が見つかるまでシーケンシャルリターンが実行されます。 そして、文字列にそのような文字がない場合はどうなりますか? その後、1000001検索を実行するだけのものになります。 毎回、現在の文字から最後までの行全体をキャプチャし、文字「a」を見つけるまでその行に戻ります(そうではありません)。 これを回避するには、この場合[^a]*aと書くだけで十分です。 しかし、これはこの場合です。 一般的な場合(括弧付きの例のように)、これでは十分ではありません。

リターンの最適化手法については、さらに多くのことを書いてください。 次の記事では、これに専念する予定です。

本「 Jeffrey Friedl、Mastering Regular Expressions 」に基づいています。

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


All Articles