目次
4. 最初に無視しますか、それとも最初に一致しますか? 特定の状況の詳細な分析
(1) 一致する可能性が最も高い複数選択ブランチを最初に配置します。
全体として、正規表現の効率を向上させる鍵は、バックトラッキングの背後にあるプロセスを徹底的に理解し、バックトラッキングの可能性を回避するテクニックを習得することです。
1. 代表的な例
まず、バックトラッキングと効率の重要性を実際に示す例を見てみましょう。二重引用符で囲まれた文字列と一致させるには、"(\\.|[^\\"])*" を使用します。エスケープされた二重引用符は許可されます。この式に問題はありませんが、NFA エンジンを使用する場合は、それぞれに適用してください。複数選択構造の効率は非常に低くなります。文字列内の「通常の」(バックスラッシュや二重引用符以外) 文字ごとに、エンジンは '\\.' をテストし、失敗後にバックトラックし、最終的に '[^\\"]' と一致しました。マッチングを高速化するためにいくつかの変更を加えることができます。
1. 少し変更 - 最も効果的な脚を最初に踏みます
一般的な二重引用符で囲まれた文字列の場合、通常の文字の数はエスケープ文字よりも多くなります。簡単な変更は、2 つの複数選択分岐の順序を逆にし、'[^\\"]' を '\\.' に置くことです。このようにして、文字列内でエスケープ文字が見つかった場合にのみ、複数選択構造に従ってバックトラッキングが実行されます。RegexBuddy ツールでターゲット文字列「2\"x3\" likeness」に一致する 2 つの式のプロセス以下の図に示されています。
図 1: 複数選択分岐の順序の影響 (従来の NFA)
左側は「(\\.|[^\\"])*」のマッチング プロセスで、合計 32 回のテストと 14 回のバックトラッキングが実行されました。右側は「([^\\"」のマッチング プロセスです。 ]|\\.)*" マッチング プロセス中に、合計 22 回のテストと 4 回のバックトラッキングが実行されました。2 つのバックスラッシュにより 2 つのブランチ バックトラッキングが発生し、最後の二重引用符により 2 つのブランチ バックトラッキングが発生しました。1 回目はブランチ [^\\"] との不一致が原因で、2 回目はアスタリスクと一致しないことが原因でした。量指定子がバックトラックされます。現時点では、すべての複数選択分岐が一致せず、複数選択構造全体が一致できません。各行は 1 回テストされます (行 22 を除く)。これは、 * 量指定子がバックトラックされ、未検証)。
この修正については、次の 2 つの側面から考える必要があります。
- この恩恵を受けるのはどのエンジンでしょうか? 従来の NFA、POSIX NFA、あるいはその両方?
- この変更はどのような状況で最大のメリットをもたらしますか? テキストが一致する場合、一致できない場合、それとも常にですか?
まず、この変更は POSIX NFA には影響しません。最終的には正規表現のあらゆる可能性を試さなければならないため、複数選択分岐の順序はあまり重要ではありません。しかし、従来の NFA の場合、一致が見つかるとエンジンが停止するため、このような高速化された複数選択ブランチの並べ替えは有利です。
2点目は、試合が成功した場合のみスピードが加速するという点です。NFA は、すべての可能性を試した後にのみ失敗する可能性があります。実際に一致するものがない場合は、あらゆる可能性が試されますが、その場合、並べ替え順序は影響しません。
次の表は、いくつかのケースで実行されたテストとバックトラッキングの数を示しています。数値が低いほど優れています。
ターゲット文字列 |
従来の NFA |
POSIX NFA |
||||
"(\\.|[^\\"])*" |
"([^\\"]|\\.)*" |
どちらの表現も同じです |
||||
テスト |
バックトレース |
テスト |
バックトレース |
テスト |
バックトレース |
|
「2×3」らしさ |
32 |
14 |
22 |
4 |
48 |
30 |
"makudonarudo" |
28 |
14 |
16 |
2 |
40 |
26 |
「とても...あと99文字...長い」 |
218 |
109 |
111 |
2 |
325 |
216 |
「ここには「一致」はありません |
124 |
86 |
124 |
86 |
124 |
86 |
表1
2 つの式は POSIX NFA では同じように動作し、変更後は従来の NFA のパフォーマンスが向上します (バックトラッキングが減少します)。一致しない場合 (最後の行)、両方のエンジンがすべての可能性を試行する必要があるため、結果は同じになります。
2. 効率と精度
効率を向上させるために正規表現を変更するときに考慮すべき最も重要な問題は、その変更が一致の精度に影響を与えるかどうかです。上記のように複数選択分岐の順序を並べ替えても、順序が一致結果から独立している場合にのみ精度に影響を与えません。欠陥のある例を見てください。正規表現「(\\.|[^"])*」を使用して、文字列「You need a 2\"3\" photo」と一致させます。46 回のテストと 21 回のバックトラックの後、結果は文字列全体が一致しました。
効率を高めるために複数選択ブランチを交換し、先頭に [^"] を置くと、効率は確実に向上します。バックトラッキングは 4 つだけで、テストは合計 27 件のみです。しかし、結果は 2 つの一致する文字列になります: "You need a 2\ "問題の根本的な原因は、2 つのブランチで一致する文字が重複しており、どちらもバックスラッシュと一致する可能性があることです。
したがって、効率を重視する場合は、正確性を決して忘れないでください。複数選択の分岐の場合は、最初に各分岐が相互に排他的であることを確認し、複数選択の分岐の順序が一致結果に関係がなく、精度が保証されてからパフォーマンスを考慮することが最善です。
3. 次へ – 一致の優先順位の範囲を制限する
図 1 からわかるように、どの正規表現でも、アスタリスクは通常の文字ごとに反復され、複数選択構造と括弧に繰り返し入ったり出たりします。これにはコストがかかるため、この追加の処理は可能な限り避けるべきです。
[^\\"] が「通常の」文字 (引用符やバックスラッシュではない) に一致する場合を考慮すると、[^\\"]+ を使用すると、(...)* 文字の 1 回の反復でできるだけ多くの文字が読み取られます。エスケープ文字のない文字列の場合、文字列全体が一度に読み取られます。そうすれば、後戻りはほとんどなくなり、アスタリスクの反復回数が最小限に抑えられます。
図 2 は、この例を従来の NFA に適用した例を示しています。左側は「(\\.|[^\\"]+)*」のマッチング処理で、図1の「(\\.|[^\\"])*」と比較し、複数選択構造とアスタリスクの繰り返しが削減されます。右側は、「([^\\"]+|\\.)*」のマッチング プロセスです。この変更と並べ替え手法を組み合わせることで、さらに多くのメリットがもたらされることがわかります。
図 2: プラス記号を追加した結果 (従来の NFA)
新しいプラス記号により、複数選択構造におけるバックトラックの数とアスタリスクの反復数が大幅に減少します。アスタリスク量指定子は括弧内の部分式を操作し、各反復で括弧を入力および終了する必要があります。エンジンは括弧内の部分式と一致するテキストを記録する必要があるため、コストがかかります。
4.「指数関数的」マッチング
POSIX NFA の場合、プラス記号を追加する変更は、まだ発生していない災害にすぎません。表 1 の「very...99 more chars...long」と一致させるために「([^\\"]+|\\.)*」を使用すると、30 億回を超えるバックトラッキングが必要になります。その理由は、この式の要素が括弧の外側にあるアスタリスクだけでなくプラス記号でも修飾されており、どの量指定子がどの特殊文字を制御しているかを区別できないためです。この不確実性が核心です。
アスタリスクがない場合、[^\\"] がアスタリスクの制約対象となり、real ([^\\"])* が一致できる文字が制限されます。1 つの文字と一致し、次に次の文字と一致し、ターゲット テキスト内のすべての文字と一致します。ターゲット文字列内のすべての文字と一致しない可能性があります (バックトラッキングが発生する) が、一致する文字の数はせいぜいターゲット文字列の長さに比例します。ターゲット文字列が長ければ長いほど、ワークロードが大きくなる可能性があります。
ただし、正規表現 ([^\\"]+)* の場合、プラス記号とアスタリスクの両方が文字列を分割する (分割される) 可能性は指数関数的に増加します。対象の文字列がマクドナルドの場合、アスタリスクは 12 回繰り返されますか、 [^\\"]+ は各反復で 1 文字と一致しますか? それとも、アスタリスクが 3 回反復され、内部の [^\\"]+ がそれぞれ 5、3、4 文字と一致しますか? または、アスタリスクが 4 回反復され、内部の [^\\"]+ が 2、2 と一致しますか? , 5. 3文字?または、他の何か...
長さ 12 の文字列には 4096 通りの可能性があり、文字列内の各文字に対して 2 つの可能性があります。POSIX NFA は、結果を得る前にすべての可能性を試さなければなりません。これが、「超線形」とも呼ばれる「指数関数的」マッチングの起源です。名前が何であっても、バックトラッキングが大量に行われることに変わりはありません。長さ n の文字列の場合、バックトラッキングの数は 2^(n+1) で、独立したテストの数は 2^(n+1)+ になります。 2^n.
POSIX NFA と従来の NFA の主な違いは、従来の NFA は最初の完全一致の可能性で停止することです。完全に一致するものがない場合は、従来の NFA であっても、一致するものを見つける前にすべての可能性を試す必要があります。表 1 の「No \"match\」という短い文字列であっても、失敗を報告する前に 8192 通りの可能性を試す必要があります。
正規表現エンジンがこれらの膨大な数の可能性を試すのに忙しい間、プログラム全体が「ロックアップ」されているように見えます。次のような正規表現を使用してエンジンのタイプをテストできます。
- 一致できない場合でも、いずれかの式ですぐに結果が得られる場合は、DFA である可能性があります。
- 試合ができるときだけすぐに結果が出れば、それは伝統的なNFAです。
- 常に遅い場合は、POSIX NFA です。
「可能性がある」という言葉が最初の判断で使用されているのは、高度に最適化された NFA がこれらの指数関数的に終わりのない一致を検出して回避できる可能性があるためです。同様に、これらの式を改善または書き換えて、一致やエラーの報告を高速化するためのさまざまな方法を後で説明します。
特定の高度な最適化の影響を除外すると、正規表現の相対的なパフォーマンスに基づいてエンジンのタイプを決定できます。従来の NFA は最も広く使用されているエンジンであり、識別するのは簡単です。まず、優先度数量子の無視をサポートしている場合、基本的には従来の NFA であると判断できます。優先数量子の無視は DFA ではサポートされておらず、POSIX NFA では意味がありません。これを確認するには、正規表現 nfa|nfa not を使用して文字列 nfa not と一致させます。nfa のみが一致する場合、これは従来の NFA です。nfa 全体が一致しない場合、エンジンは POSIX NFA または DFA です。
MySQL の通常のエンジンは従来の NFA です。
mysql> set @r:='nfa|nfa not';
Query OK, 0 rows affected (0.00 sec)
mysql> set @s:='nfa not';
Query OK, 0 rows affected (0.00 sec)
mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+------+------+
| c | s |
+------+------+
| 1 | nfa |
+------+------+
1 row in set (0.00 sec)
場合によっては、DFA と POSIX NFA の違いが明らかです。DFA は、括弧と後方参照のキャプチャをサポートしていません。これは便利ですが、両方のエンジンを使用するハイブリッド システムもあり、キャプチャ ブラケットが使用されない場合は DFA が使用されます。
次の簡単なテストでは、多くの問題を説明できます。=XX====================== という形式の文字列と一致させるには、X(.+)+X を使用します。実行に時間がかかる場合は、NFA です。前のテストで従来の NFA ではないと判断された場合、それは間違いなく POSIX NFA です。実行時間が非常に短い場合は、高度な最適化をサポートする DFA または NFA です。スタック オーバーフロー (スタック オーバーフロー) が表示される場合、またはタイムアウトで終了する場合、それは NFA エンジンです。
mysql> set @r:='X(.+)+X';
Query OK, 0 rows affected (0.00 sec)
mysql> set @s:='=XX======================';
Query OK, 0 rows affected (0.00 sec)
mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
ERROR 3699 (HY000): Timeout exceeded in regular expression match.
2. 総合的な調査・検討
まず、正規表現「.*」を次のテキストに適用する例を見てみましょう。
「マクドナルド」という名前は、日本語では「マクドナルド」と言われます。
マッチングプロセスを図 3 に示します。
図 3: 「.*」の照合成功プロセス
1. 従来のNFAのマッチングプロセス
正規表現では、文字列の先頭から順に各文字が試行されますが、開始引用符は一致しないため、最初の二重引用符の位置に到達するまで後続の文字は一致しません。その後、式の他の部分が試行されますが、ギアリングは、この試行が失敗した場合に式全体を次の位置から試行できることを認識しています。
次に、.* は文字列の終わりまで一致しますが、その時点でドットが一致できなくなるため、アスタリスクによって反復が停止されます。.* は一致に成功するために文字を必要としないため、エンジンはこのプロセス中にバックトラック用に 46 の状態を記録します。.* が停止されると、エンジンは最後に保存された状態からのバックトラックを開始し、文字列の末尾にある閉じ二重引用符との一致を試みます。ここの二重引用符も一致しないため、試行は依然として失敗します。その後、エンジンはバックトラックして試行を続けますが、結果も一致しません。
エンジンは、保存された状態を逆の順序 (最後に保存された状態が最初) で試行します。数回の試行の後、場所「...arudo」に到達し、一致が成功し、この場所でグローバル一致が行われます。
「McDonald's」は「makudonarudo」と言われます。
これは従来の NFA のマッチング プロセスであり、残りの未使用状態は破棄され、マッチングは正常に報告されます。
2. POSIX NFA にはさらに多くの処理が必要です
POSIX NFA 一致は「これまでの最長一致」ですが、さらに長い一致が存在するかどうかを確認するには、保存されているすべての状態を試行する必要があります。この例では、最初に見つかった一致が最長ですが、正規表現エンジンはこれを確認する必要があります。
3. マッチングできない場合の対処方法
マッチングができない場合の状況も分析する必要があります。".*"! サンプル テキストと一致しません。ただし、図 4 に示すように、照合プロセス中に多くの作業が行われます。
図 4: 「.*」! マッチング失敗のプロセス
図 4 の一連の試行全体は、従来の NFA と POSIX NFA の両方が通過する必要があるものです。一致しない場合、従来の NFA は POSIX NFA と同じ回数試行する必要があります。開始 A から終了 I までのすべての試行に一致がないため、送信はドライブ プロセスを開始して新しい試行ラウンドを開始する必要があります。J、Q、V で始まる試行は一致する可能性があるように見えますが、結果は A で始まる試行と同じです。最終的に Y に到達すると、試行を続ける方法がないため、マッチング全体が失敗します。図 4 に示すように、この結果を得るには多くの作業が必要でした。
4. はっきりと見える
比較のためにピリオドを ^" に置き換えます。「[^"]*"! を使用すると、[^"]* の一致する内容に二重引用符を含めることができないため、一致とバックトラッキングが減少します。図 5 は、失敗した試行のプロセスを示しています。
図 5: "[^"]*"! 一致しません
図からわかるように、後戻りの数が大幅に減少しています。この結果が要件を満たしている場合、バックトラッキングが減少するという有益な副作用が得られます。
5. 多肢選択構造はコストがかかる
おそらく、複数選択構造が後戻りの主な理由です。u|v|w|x|y|z と [uvwxyz] を比較すると、次の文字列と一致します。
「マクドナルド」という名前は、日本語では「マクドナルド」と言います。
実装によって効率は異なる場合がありますが、一般に文字グループの効率は、対応する複数選択構造よりも高くなります。通常、文字グループは単純にテストされるため、[uvwxyz] の照合には 34 回の試行のみが必要です。
u|v|w|x|y|z を使用する場合、同じ結果が得られるまでに、各位置で 6 回バックトラックする必要があり、合計 204 回バックトラックする必要があります。もちろん、すべての複数選択構造を文字グループに置き換えることができるわけではありません。また、置き換えることができたとしても、それほど単純ではない可能性があります。ただし、場合によっては、必要な複数選択構造の一致に関連するバックトラッキングを大幅に削減できる手法が使用されます。
バックトラッキングを理解することは、おそらく NFA の効率を学ぶ上で最も重要な問題ですが、それだけではありません。定期的にエンジンを最適化することで、効率を大幅に向上させることができます。
3. 性能試験
1. テストのポイント
基本的なパフォーマンス テストは、プログラムの実行時間を記録することです。最初にシステム時間を取得し、プログラムを実行し、次にシステム時間を取得し、その 2 つの時間の差、つまりプログラムの実行時間を計算します。たとえば、^(a|b|c|d|e|f|g)+$ と ^[ag]+$ を比較します。簡単な Perl プログラムを次に示します。
use Time::HiRes 'time'; # 这样 time() 的返回值更加精确
$StartTime = time();
"abababdedfg" =~ m/^(a|b|c|d|e|f|g)+$/;
$EndTime = time();
printf("Alternation takes %.3f seconds. \n", $EndTime - $StartTime);
$StartTime = time();
"abababdedfg" =~ m/^[a-g]+$/;
$EndTime = time();
printf("Character class takes %.3f seconds. \n", $EndTime - $StartTime);
これは簡単ですが、パフォーマンス テストを行う際には留意すべき点がいくつかあります。
- 「本当に興味深い」処理時間のみが記録されます。「処理」時間をできるだけ正確に記録し、「非処理時間」の影響を可能な限り回避します。開始前に初期化などの準備が必要な場合はその後に計時を開始し、仕上げ作業が必要な場合は計時を停止してから行ってください。
- 「十分な」処理を行います。多くの場合、テストに必要な時間は非常に短く、コンピューターの時計は意味のある値を与えるほど正確ではありません。
この Perl プログラムを私のマシンで実行すると、結果は次のようになります:
代替には 0.000 秒かかり、
文字クラスには 0.000 秒かかります。
プログラムの実行時間が短すぎる場合は、プログラムを複数回実行して「十分な」作業を確保します。ここでの「十分」とは、システム クロックの精度によって決まります。ほとんどのシステムは 1/100 秒までの精度があり、プログラムにかかる時間が 0.5 秒だけであっても、意味のある結果を得ることができます。
- 「正確」な処理を行います。1,000 万回のクイック操作を実行するには、タイミングを担当するコード ブロックに 1,000 万個のカウンターを蓄積する必要があります。可能であれば、オーバーヘッドを追加せずに実際の処理部分の割合を増やすことが最善のアプローチです。Perl の例では、正規表現はかなり短いテキストに適用されます。正規表現がもっと長い文字列に適用された場合、各ループでより多くの「実際の」処理が行われることになります。
これらの要素を考慮すると、次の手順が導き出されます。
use Time::HiRes 'time'; # 这样 time() 的返回值更加精确
$TimesToDo = 1000; # 设定重复次数
$TestString = "abababdedfg" x 1000; # 生成长字符串
$Count = $TimesToDo;
$StartTime = time();
while ($Count-- > 0) {
$TestString =~ m/^(a|b|c|d|e|f|g)+$/;
}
$EndTime = time();
printf("Alternation takes %.3f seconds. \n", $EndTime - $StartTime);
$Count = $TimesToDo;
$StartTime = time();
while ($Count-- > 0) {
$TestString =~ m/^[a-g]+$/;
}
$EndTime = time();
printf("Character class takes %.3f seconds. \n", $EndTime - $StartTime);
$TestString と $Count は、計算前に初期化されます ($TestString は、Perl が提供する x 演算子を使用して初期化されます。これは、左側の文字列が右側で繰り返される回数を表します)。Perl 5.10.1 を実行している私のマシンでは、結果は次のようになります:
代替には 1.473 秒かかり、
文字クラスには 0.012 秒かかります。
したがって、この例では、文字グループは複数選択構造よりも約 123 倍高速です。バックグラウンド システム アクティビティの影響を軽減するために、このテストはできるだけ短い時間で複数回実行する必要があります。
2. 何が測定されているかを理解する
初期化子を次のように変更すると、さらに興味深い結果が得られます:
$TimesToDo = 1000000;
$TestString = "abababdedfg";
ここで、テスト文字列の長さは上記の 1/1000 にすぎず、テストを 1,000,000 回実行する必要があります。正規表現ごとにテストおよび照合される文字の総数は変わらないため、理論的には「ワークロード」は変わらないはずです。しかし結果は全く異なり、
オルタネーションには 1.863 秒かかり、
キャラクター クラスには 0.247 秒かかります。
どちらの時間も前回よりも長かったです。その理由は、新しい「非処理」オーバーヘッド、つまり $Count の検出と更新、および通常のエンジンの構築にかかる時間が、以前の 1000 倍になったことです。
文字グループのテストでは、追加のオーバーヘッドに約 0.24 秒かかりましたが、複数選択構造では 0.39 秒かかりました。複数選択構造のテスト時間の大きな変動の主な理由は、キャプチャ ブラケットにあり、各テストの前後に追加の処理が必要となり、1000 倍のコストがかかります。
3.MySQLテスト
以下は、MySQL 8.0.16 でのテスト ストアド プロシージャです。
delimiter //
create procedure sp_test_regexp(s varchar(100), r varchar(100), c int)
begin
set @s:=1;
set @s1:='';
while @s<=c do
set @s1:=concat(@s1,s);
set @s:=@s+1;
end while;
select now(3) into @startts;
select regexp_like(@s1, r, 'c') into @ret;
select now(3) into @endts;
select @ret,@startts,@endts,timestampdiff(microsecond,@startts,@endts)/1000 diff_ts;
end;
//
delimiter ;
文字列と正規表現を初期化します。
set @str:='abababdedfg';
set @reg1:='^(a|b|c|d|e|f|g)+$';
set @reg2:='^[a-g]+$';
最初のサイクルは 1000 回で、結果は次のとおりです。
mysql> call sp_test_regexp(@str, @reg1, 1000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:22:44.921 | 2023-07-10 15:22:44.922 | 1.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
mysql> call sp_test_regexp(@str, @reg2, 1000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:22:44.933 | 2023-07-10 15:22:44.933 | 0.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
複数選択分岐には 1 ミリ秒かかり、文字グループには 0 ミリ秒かかりました。コントラストはあまり明らかではないため、サイクル数を増やし続けます。2 番目のループは 10,000 回実行され、結果は次のようになります。
mysql> call sp_test_regexp(@str, @reg1, 10000);
ERROR 3699 (HY000): Timeout exceeded in regular expression match.
mysql> call sp_test_regexp(@str, @reg2, 10000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:25:25.918 | 2023-07-10 15:25:25.919 | 1.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (0.28 sec)
Query OK, 0 rows affected (0.28 sec)
複数選択分岐でエラーが報告され、文字グループに 1 ミリ秒かかりました。このエラーは、複数選択ブランチの照合中にシステム変数 regexp_time_limit の制限を超えたために報告されます。この変数は、マッチング エンジンが実行できるステップの最大許容数を制限するため、実行時間 (通常はミリ秒程度) に間接的に影響します。デフォルト値は 32 です。テストを続行するには、この変数の値を増やします。
3 番目のテスト:
mysql> set global regexp_time_limit=3200;
Query OK, 0 rows affected (0.00 sec)
mysql> call sp_test_regexp(@str, @reg1, 10000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:33:47.033 | 2023-07-10 15:33:47.046 | 13.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (0.29 sec)
Query OK, 0 rows affected (0.29 sec)
mysql> call sp_test_regexp(@str, @reg2, 10000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:33:47.318 | 2023-07-10 15:33:47.319 | 1.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (0.27 sec)
今回は、複数選択分岐に 13 ミリ秒かかり、文字グループに 1 ミリ秒かかり、13 倍の差がありました。テストを続行するには、サイクル数を増やしてください。
4 番目のテスト:
mysql> call sp_test_regexp(@str, @reg1, 100000);
ERROR 3698 (HY000): Overflow in the regular expression backtrack stack.
mysql> call sp_test_regexp(@str, @reg2, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:36:42.548 | 2023-07-10 15:36:42.559 | 11.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (27.13 sec)
複数選択分岐でエラーが報告され、文字グループに 11 ミリ秒かかりました。このエラーは、複数選択ブランチの照合中にシステム変数 regexp_stack_limit の制限を超えたために報告されます。この変数は、regexp_like() または同様の正規表現関数がマッチングを実行するときにバックトレース スタックで使用できる最大メモリ (バイト単位) に使用されます。デフォルト値は 8000000 です。テストを続行するには、この変数の値を増やします。
5 番目のテスト:
mysql> set global regexp_stack_limit=800000000;
Query OK, 0 rows affected (0.00 sec)
mysql> call sp_test_regexp(@str, @reg1, 100000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 1 | 2023-07-10 15:41:48.045 | 2023-07-10 15:41:48.190 | 145.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (27.24 sec)
Query OK, 0 rows affected (27.24 sec)
mysql> call sp_test_regexp(@str, @reg2, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-10 15:42:15.307 | 2023-07-10 15:42:15.317 | 10.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (27.12 sec)
今回は、複数選択分岐に 145 ミリ秒、文字グループに 10 ミリ秒かかり、14.5 倍の差がありました。
4. 共通の最適化施策
正規表現の実装を最適化するには通常 2 つの方法があります。
- 特定の操作を高速化します。\d+ などの特定のタイプの一致は非常に一般的であるため、エンジンには汎用の処理メカニズムよりも高速に実行される特別な処理スキームが組み込まれている場合があります。
- 冗長な操作を避けてください。正しい結果を生成するために一部の特別な操作が不要である、または一部の操作を以前よりも少ないテキストに適用できるとエンジンが判断した場合、これらの操作を無視することで時間を節約できます。たとえば、\A で始まる正規表現は文字列の先頭でのみ一致します。そこで一致しない場合、ギアは他の場所で無駄に試行しません。
1. 得たものは失うもの
最適化に必要な時間、節約される時間、そしてさらに重要なことに、最適化の可能性の間には、相互に制限的な関係があります。最適化は、最適化手段が実行可能かどうかを検出するために必要な時間が、節約されたマッチング時間よりも短い場合にのみ有益です。
例を見てみましょう。式 \b\B (単語の区切り文字であると同時に単語の区切り文字ではない位置) は一致できません。エンジンは、指定された式に \b\B が含まれていることを検出した場合、式全体を一致させることができないと認識し、一致操作は実行されません。失敗した一致はすぐに報告されます。一致したテキストが非常に長い場合、時間を大幅に節約できます。
ただし、通常のエンジンではそのような最適化は行われていません。\b\B を含む正規表現は一致する可能性が高いため、エンジンは事前に確認するために追加の作業を行う必要があります。そうすることで時間を大幅に節約できる場合もありますが、速度の向上によりはるかに高い代償が伴う場合もあります。\b\B を使用すると、正規表現の特定の部分が一致しないことを確認できます。たとえば、\b\B を...(this|this other)... に挿入すると、最初の複数選択分岐が確実に実行されます。失敗します。
mysql> select regexp_substr('this this other','this|this other');
+----------------------------------------------------+
| regexp_substr('this this other','this|this other') |
+----------------------------------------------------+
| this |
+----------------------------------------------------+
1 row in set (0.00 sec)
mysql> select regexp_substr('this this other','this\\b\\B|this other');
+----------------------------------------------------------+
| regexp_substr('this this other','this\\b\\B|this other') |
+----------------------------------------------------------+
| this other |
+----------------------------------------------------------+
1 row in set (0.00 sec)
2. 最適化はさまざまです
さまざまな最適化措置を実行するときは、「ランチは人それぞれ異なり」、エンジンが異なれば最適化の方法も異なる可能性があることを覚えておくことが重要です。正規表現に少し変更を加えると、ある実装では速度が大幅に向上し、別の実装では速度が大幅に低下する可能性があります。
3. 正規表現の適用原理
対象文字列に正規表現を適用する手順は、大きく以下の手順に分かれます。
(1) 正規表現コンパイル:正規表現の文法が正しいかどうかを確認し、正しければ内部形式にコンパイルします。
(2)変速開始:変速装置は、通常のエンジンを目標ストリングの開始位置に「位置決め」する。
(3) 要素の検出: エンジンは正規表現とテキストのテストを開始し、正規表現の各要素 (コンポーネント) を順番にテストします。
- Subject 内の S、u、b、j、e などの接続された要素は 1 回試行され、要素が一致しない場合にのみ停止します。
- 量指定子によって変更された要素の場合、制御は量指定子 (量指定子が引き続き一致するかどうかを確認する) と修飾された要素 (一致できるかどうかをテストする) の間で交互に行われます。
- キャプチャ ブラケットの内外で制御を切り替える際には、ある程度のオーバーヘッドが発生します。括弧で囲まれた式に一致するテキストは、$1 を通じて参照できるように保存する必要があります。ブラケットのペアはバックトラッキング ブランチに属する可能性があるため、ブラケットの状態はバックトラッキングに使用される状態の一部であるため、キャプチャ ブラケットに出入りするときに状態を変更する必要があります。
(4) 一致する結果の検索: 一致する結果が見つかった場合、従来の NFA は現在の状態で「ロック」し、成功した一致を報告します。POSIX NFA の場合、一致がこれまでで最も長い場合、この一致の可能性が記憶され、利用可能な保存された状態から続行されます。保存されたすべての状態がテストされた後、最長一致が返されます。
(5) 送信装置の駆動処理: 一致するものが見つからなかった場合、送信装置はエンジンを駆動し、テキスト内の次の文字から再試行を開始します (ステップ 3 に戻ります)。
(6) 完全一致失敗: ターゲット文字列のすべての文字 (最後の文字の後の位置を含む) から開始する試行が失敗した場合、完全一致失敗が報告されます。
次のセクションでは、高度な実装によってこの処理をどのように削減できるか、およびこれらのテクニックを適用する方法について説明します。
4. 以前の最適化措置を適用する
優れた正規エンジンの実装では、実際に使用される前に正規表現を最適化することができ、場合によっては、特定の正規表現がとにかく一致しないとすぐに判断できるため、この正規表現を適用する必要がまったくありません。
(1) コンパイルキャッシュ
正規表現を使用する前にまず構文チェックを行い、問題がなければ内部形式にコンパイルされます。コンパイルされた内部フォームはさまざまな文字列をチェックするために使用できますが、次のプログラムはどうでしょうか。
while (...) {
if ($line =~ m/^\s*$/) ...
if ($line =~ m/^Subject: (.*)/) ...
if ($line =~ m/^Date: (.*)/) ...
if ($line =~ m/^Reply-To: (\S+)/) ...
if ($line =~ m/^From: (\S+) \(([^()]*)\)/) ...
}
明らかに、ループを実行するたびにすべての正規表現を再コンパイルするのは時間の無駄です。逆に、最初のコンパイル後に内部フォームを保存またはキャッシュし、後続のループで再利用すると、速度は明らかに向上しますが、メモリをある程度消費します。具体的な方法はアプリケーションが提供する正規表現の処理方法に依存し、統合型、手続き型、オブジェクト指向の3種類があります。
MySQL の正規表現は統合コンパイル キャッシュに属しており、正規表現のマッチング機能は関数を通じて提供されます。ストアド プロシージャまたはカスタム関数で通常の関数を呼び出すと、その関数はプリコンパイルされます。Java などのプログラムを使用してデータベースにアクセスする場合は、プリコンパイルに MySQL JDBC を使用できます。
(2) 統合処理でのコンパイルキャッシュ
Perl と awk は統合された処理方法を使用しており、コンパイルとキャッシュが非常に簡単です。内部的には、各正規表現はコードの特定の部分に関連付けられています。初めて実行されると、コンパイル結果とコードの間に関連付けが確立されます。次回実行時には、その正規表現を参照するだけで済みます。これにより、キャッシュされた式を保存するためにメモリの一部が必要になりますが、時間を最大限に節約できます。
変数補間 (変数補間、つまり、MySQL の動的 SQL など、変数の値を正規表現の一部として使用する) は、キャッシュに問題を引き起こす可能性があります。たとえば、m/^Subject: \Q$DesiredSubject\E\s*$/ の場合、正規表現の内容は補間変数に依存するため、ループごとに変更される可能性があり、この変数の値も変更される可能性があります。毎回異なる場合、正規表現は毎回コンパイルする必要があり、まったく再利用できません。妥協的な最適化手段は、補間結果 (つまり、正規表現の特定の値) をチェックし、特定の値が変更された場合にのみ再コンパイルすることです。
(3) プログラム処理でのコンパイルキャッシュ
在集成式处理中,正则表达式的使用与其在程序中所处的位置相关,所以再次执行这段代码时,编译好的正则表达式就能够缓存和重复使用。但是,程序式处理中只有通用的“应用此表达式”的函数。也就是说,编译形式并不与程序的具体位置相连,下次调用此函数时,正则表达式必须重新编译。从理论上说是如此,但是在实际应用中,禁止尝试缓存的效率无疑很低。相反,优化通常是把最近使用的正则表达式模式(regex pattern)保存下来,关联到最终的编译形式。
调用“应用此表达式”函数后,作为参数的正则表达式模式会与保存的正则表达式相比较,如果存在于缓存中,就使用缓存的版本。如果没有,就直接编译这个正则表达式,将其存入缓存。如果缓存有容量限制,可能会替换一个旧的表达式。如果缓存用完了,就必须放弃(thrown out)一个编译形式,通常是最久未使用的那个。
GUN Emacs 的缓存能够保存最多 20 个正则表达式,Tcl 能保存 30 个。PHP 能保存四千多个。.NET Framework 在默认情况下能保存 15 个表达式,不过数量可以动态设置,也可以禁止此功能。
(4)面向对象式处理中的编译缓存
在面向对象式处理中,正则表达式何时编译完全由程序员决定。正则表达式的编译是用户通过 New Regex、re.compile 和 Pattern.compile(分别对应 .NE、Python 和 java.util.regex)之类的构造函数来进行的。编译在正则表达式实际应用之前完成,但是它们也可以更早完成,有时候可以在循环之前,或者是程序的初始化阶段,然后可以随意使用。
在面向对象式处理中,程序员通过对象析构函数抛弃(thrown out)编译好的正则表达式。及时抛弃不需要的编译形式能够节省内存。
(5)预查必须字符 / 子字符串优化
相比正则表达式的完整应用,在字符串中搜索某个或一串字符是更加“轻量级”的操作,所以某些系统会在编译阶段做些额外的分析,判断是否存在成功匹配必须的字符或者字符串。在实际应用正则表达式之前,在目标字符串中快速扫描,检查所需的字符或字符串,如果不存在,根本就不需要进行任何尝试。
例如,^Subject: (.*) 的 ‘Subject: ’是必须出现的。程序可以检查整个字符串,或者使用 Boyer-Moore 搜索算法(一种很快的检索算法,字符串越长效率越高)。即便进行逐个字符检查也可以提高效率。选择目标字符串中不太可能出现的字符(如‘Subject: ’中的‘t’之后的‘:’)能够进一步提高效率。
正则引擎必须能识别出,^Subject: (.*) 的一部分是固定的文本字符串。对于任意匹配来说,识别出 this|that|other 中的‘th’是必须的,需要更多的功夫,而且大多数正则引擎不会这样做。此问题的答案并不是黑白分明的,某个实现方式或许不能识别出‘th’是必须的,但能够识别出‘h’和‘t’都是必须的,所以至少可以检查一个字符。
不同的应用程序能够识别出的必须字符和字符串有很大差别。许多系统会受到多选结构的干扰,在这种系统中,使用 th(is|at) 的表现好于 this|that。
(6)长度判断优化
^Subject: (.*) 能匹配文本的长度是不固定的,但至少必须包含 9 个字符。所以,如果目标字符串的长度小于 9 则根本不必尝试。当然需要匹配的字符更长,优化的效果才更明显,例如 :\d{79}:(至少需要 81 个字符)。
5. 通过传动装置进行优化
即使正则引擎无法预知某个字符串能否匹配,也能够减少传动装置真正应用正则表达式的位置。
(1)字符串起始 / 行锚点优化
この最適化では、^ で始まる正規表現は、^ が一致する場合にのみ一致すると推論できるため、これらの位置にのみ適用する必要があります。この最適化を使用する実装では、^(this|that) が正常に一致する場合、^ も一致する必要があることを認識する必要があります。ただし、多くの実装では ^this|^that を認識できません。この場合、^(this|that) または ^(?:this|that) を使用すると、マッチング速度が向上します。
同じ最適化措置は \A にも有効であり、一致が複数回発生する場合は \G にも有効です。
(2) 暗黙的なアンカーポイントの最適化
この最適化を使用できるエンジンは、正規表現が .* または .+ で始まり、グローバル代替がない場合、正規表現の先頭に目に見えない ^ があると想定できることを認識しています。このようにして、前のセクションの「文字列開始/行アンカーの最適化」を適用できるため、時間を大幅に節約できます。
より賢いシステムは、先頭の .* または .+ が括弧内にある場合でも同じ最適化を実行できることを認識していますが、括弧を捕捉する場合には注意が必要です。たとえば、(.+)X\1 は 'X' の両側が同じ文字列と一致することを期待しますが、^ を追加しても '1234X2345' には一致しません。
mysql> select regexp_substr('1234X2345','(.+)X\\1');
+---------------------------------------+
| regexp_substr('1234X2345','(.+)X\\1') |
+---------------------------------------+
| 234X234 |
+---------------------------------------+
1 row in set (0.00 sec)
(3) 文字列終端/行アンカーの最適化
この最適化では、末尾に $ またはその他の終了アンカー ポイント (\Z、\z など) を持つ正規表現が検出されると、文字列の末尾からの文字数から開始して一致を試みることができます。たとえば、正規表現 regex(es)?$ は文字列の末尾から 8 番目の文字のみに一致するため、ギアはその位置にジャンプし、ターゲット文字列内の多くの可能性のある文字をスキップできます。
ここで 7 文字ではなく 8 文字と言ったのは、多くのジャンルでは $ が文字列の末尾の改行文字の前の位置に一致するためです。
(4) 開始文字/文字グループ/部分文字列認識の最適化
これは、「プリフェッチが必要な文字/部分文字列の最適化」のより一般的なバージョンで、同じ情報を使用し (正規表現の一致は特定の文字またはリテラル部分文字列で始まる必要があります)、ギアが高速に部分文字列チェックを実行できるようにします。文字列内の適切な位置に正規表現を適用できます。たとえば、this|that|other は [ot] 位置からのみ一致できるため、Gearing は文字列内のすべての文字を事前チェックし、一致が可能な場合にのみ適用するため、時間を大幅に節約できます。事前にチェックできる部分文字列が長いほど、「誤った開始」が少なくなります。
(5) 埋め込み文字列検査の最適化
これは最初の文字列認識の最適化に似ていますが、より高度で、一致の固定位置に表示されるリテラル文字列を対象としています。正規表現が \b(perl|java)\.regex\.info\b の場合、一致するものには '.regex.info' が存在する必要があるため、スマート ギアリングでは高速の Boyer-Moore 文字列取得を使用できます。 「.regex.info」を検索し、次に 4 文字前方にカウントして、実際に正規表現の適用を開始します。
一般に、この最適化は、埋め込まれたリテラル文字列が式の先頭から固定距離にある場合にのみ機能します。したがって、\b(vb|java)\.regex\.info\b には使用できません。この式にはリテラル文字列が含まれていますが、この文字列と一致したテキストの開始位置との間の距離は定義されていません (2 文字または 4 文字) )。(\w+) は任意の数の文字に一致する可能性があるため、この最適化は \b(\w+)\.regex\.info\b にも使用できません。
(6) 長さ識別送信の最適化
この最適化は「長さ判定の最適化」に直接関係しており、現在位置がマッチング成功に必要な最小長未満の場合、送信によりマッチング試行が停止されます。
6. 正規表現自体の最適化
(1) 文字列接続の最適化
おそらく最も基本的な最適化は、エンジンが abc を 3 つの要素「a、次に b、そして c」ではなく「1 つの要素」として扱うことができることです。これが可能であれば、3 回の反復を必要とするのではなく、セクション全体を反復を照合するための単位として使用できるようになります。
(2) 量指定子の最適化を簡素化する
リテラル文字列や文字グループなどの共通要素を制約するプラス記号やアスタリスクなどの量指定子は、通常、一般的な NFA エンジンの段階的なオーバーヘッドのほとんどを回避するために最適化する必要があります。通常のエンジン内のメイン ループは汎用的であり、エンジンによってサポートされるすべての構造を処理できなければなりません。プログラミングでは、「ユニバーサル」とは速度が遅いことを意味するため、この種の最適化では .handler などの単純な数量子が処理されます。このように、汎用エンジンはこれらの構造を短絡させます。
たとえば、.* と (?:.)* は論理的には同等ですが、この最適化が適用されたシステムでは、実際には .* の方が高速です。java.util.regex では約 10%、Ruby と .NET では約 2.5 倍、Python では約 50 倍、PHP/PCRE では約 150 倍のパフォーマンス向上が見られます。 Perl の実装 次のセクションで説明する最適化を使用すると、.* と (?:.)* は同等に高速になります。
MySQL では、パフォーマンスが約 4 倍向上しました。
mysql> set @str:='abababdedfg';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg1:='.*';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg2:='(?:.)*';
Query OK, 0 rows affected (0.00 sec)
mysql> call sp_test_regexp(@str, @reg1, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 15:10:00.241 | 2023-07-11 15:10:00.252 | 11.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (27.15 sec)
Query OK, 0 rows affected (27.15 sec)
mysql> call sp_test_regexp(@str, @reg2, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 15:10:34.690 | 2023-07-11 15:10:34.731 | 41.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (27.74 sec)
(3) 不要な括弧を削除する
実装が (?:.)* と .* を完全に同等であるとみなした場合、前者を後者に置き換えます。
(4) 不要な文字群を削除する
単一の文字のみを含む文字グループは、まったく不要な文字グループとして扱われるため、多少冗長になります。したがって、賢明な実装では、内部的に [.] を \. に変換します。
(5) 優先度数量子の後の文字の最適化を無視します。
"(.*?)" の *? などの優先度の数量子を無視します。処理中に、エンジンは通常、オブジェクト (ドット) と " の後の文字の間で切り替える必要があります。さまざまな理由から、通常、優先度の数量子を無視する方が一致するよりも優れています。 -first 量指定子は、特に上記の「単純化された量指定子の最適化」の match-first 修飾構造の場合に遅くなります。もう 1 つの理由は、無視優先量指定子がキャプチャ括弧内にある場合、制御は括弧内にある必要があることです。内側と外側を切り替えると、追加のオーバーヘッドが発生します。
したがって、この最適化の原理は、リテラル文字が無視された優先数量子に続く場合、エンジンがそのリテラル文字に触れない限り、無視される優先数量子を通常の一致する優先数量子として処理できるということです。したがって、この最適化を含む実装は、この場合、特別な無視優先度数量子に切り替え、ターゲット テキスト内のリテラル文字列を迅速に検出し、このリテラル文字が検出されるまで通常の「無視」状態をスキップします。
mysql> set @str:='"aba"bab"dedfg';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg1:='"(.*)"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg2:='"(.*?)"';
Query OK, 0 rows affected (0.00 sec)
mysql> call sp_test_regexp(@str, @reg1, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 15:47:52.673 | 2023-07-11 15:47:52.687 | 14.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (34.25 sec)
Query OK, 0 rows affected (34.25 sec)
mysql> call sp_test_regexp(@str, @reg2, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 15:48:28.258 | 2023-07-11 15:48:28.270 | 12.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (35.58 sec)
この最適化には他にもさまざまな形式があります。たとえば、['"](.*?)["'] 内の ['"] をチェックするなど、特定の文字ではなく文字グループを事前チェックするなどです。先ほど紹介したものと同様 冒頭の文字認識が最適化されています。
(6) 「過剰な」後戻り検出
(.+)* などの量指定子を構造体と組み合わせると、指数関数的なバックトラッキングが発生する可能性があります。この状況を回避する簡単な方法は、バックトラッキングの数を制限し、制限を超えた場合にマッチングを停止することです。これはいくつかの実際的な状況では役立ちますが、正規表現を適用できるテキストに人為的な制限も設定します。
たとえば、制限が 10,000 トレースバックの場合、一致する各文字が 1 つのトレースバックに対応するため、.*? は 10,000 を超える文字列と一致することはできません。この状況は、特に Web ページを扱う場合には珍しいことではないため、この制限は非常に問題です。
さまざまな理由から、一部の実装ではバックトレース スタックのサイズが制限されています (たとえば、Python の上限は 10000)。それが同時に保存できる状態の上限です。バックトラッキングの上限と同様に、これも正規表現が処理できるテキストの長さを制限します。
「MySQL テスト」セクションでは、関連する 2 つの MySQL 構成パラメータのデフォルト値、効果、および変更について説明しました。
(7) 指数関数的 (超線形) マッチングを避ける
指数関数的なマッチングを回避するより良い方法は、マッチングが超線形状態に入ろうとするタイミングを検出することです。これにより、各量指定子の対応する部分式が一致しようとする場所を記録するために追加の作業を行うことができ、繰り返しの試行を回避できます。
実際、超線形マッチングは発生時に簡単に検出できます。単一の量指定子の反復 (ループ) の数は、ターゲット文字列の文字数を超えてはなりません。そうでない場合は、指数関数的な一致が確実に発生します。この手がかりに基づいて一致を終了できないことが判明した場合、重複する一致を検出して削除することはより複雑な問題になりますが、複数選択の分岐一致が非常に多いため、そうする価値がある可能性があります。
スーパーリニア一致を検出し、一致失敗を迅速に報告することの副作用の 1 つは、本当に非効率な正規表現が非効率であると認識されないことです。この最適化を使用して指数関数的な一致が回避されたとしても、必要な時間は実際に必要な時間よりもはるかに長くなりますが、ユーザーが簡単に気づくほど遅くはありません。
もちろん、全体としては利点が欠点を上回る可能性があります。多くの人は正規表現の効率性など気にせず、正規表現に対して恐怖心を抱いており、タスクの完了方法など気にせずただタスクを完了したいだけです。
(8) 所有優先度数量詞を使用して状態を削減する
通常の量指定子によって制約されたオブジェクトが一致すると、いくつかの「ここに一致しません」状態が保持され、量指定子の反復ごとに 1 つの状態が作成されます。所有量指定子はこれらの状態を保持しません。具体的な方法は 2 つあり、1 つはすべての量指定子が完了した後にすべてのスタンバイ ステートを破棄する方法で、より効率的な方法は、各反復で前のラウンドのスタンバイ ステートを破棄する方法です。量指定子が照合を継続できない場合でもエンジンが実行を継続できるように、照合時に状態を保存することが常に必要です。
反復中にその場で状態を破棄すると、必要なメモリが少なくなるため、より効率的になります。.* を適用すると、各文字を照合するときに状態が作成されますが、文字が長い場合は大量のメモリを消費する可能性があります。
(9) 量子等価変換
\d\d\d\d と \d{4} の間に効率の違いはありますか? NFA の場合、答えはほぼ「はい」ですが、結果はツールによって異なります。量指定子が最適化されている場合、量指定子のない正規表現がさらに最適化されない限り、 \d{4} は高速になります。\d{4} は MySQL では約 25% 高速です。
mysql> set @str:='1234';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg1:='\\d\\d\\d\\d';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg2:='\\d{4}';
Query OK, 0 rows affected (0.00 sec)
mysql> call sp_test_regexp(@str, @reg1, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 17:26:35.087 | 2023-07-11 17:26:35.091 | 4.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (7.09 sec)
Query OK, 0 rows affected (7.09 sec)
mysql> call sp_test_regexp(@str, @reg2, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 17:26:42.168 | 2023-07-11 17:26:42.171 | 3.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (7.07 sec)
==== と ={4} を比較すると、この時に繰り返されているのはあるリテラル文字であり、==== エンジンを直接使った方がリテラル文字列として認識しやすいです。その場合、サポートされている効率的な「開始文字/文字グループ/部分文字列認識の最適化」が役に立ちます。これは Python と Java の場合にまさに当てはまり、==== は ={4} より 100 倍高速です。
Perl、Ruby、および .NET はより最適化されており、==== と ={4} を区別しないため、どちらも同等に高速になります。MySQL でも両方の速度は同じです。
mysql> set @str:='====';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg1:='====';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg2:='={4}';
Query OK, 0 rows affected (0.00 sec)
mysql> call sp_test_regexp(@str, @reg1, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 17:33:40.960 | 2023-07-11 17:33:40.963 | 3.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (7.08 sec)
Query OK, 0 rows affected (7.08 sec)
mysql> call sp_test_regexp(@str, @reg2, 100000);
+------+-------------------------+-------------------------+---------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+---------+
| 1 | 2023-07-11 17:33:48.054 | 2023-07-11 17:33:48.057 | 3.0000 |
+------+-------------------------+-------------------------+---------+
1 row in set (7.10 sec)
(10) 身分証明書が必要
もう 1 つの簡単な最適化は、テキストが必ずしもキャプチャされるわけではない括弧のキャプチャの使用など、マッチング結果にとって価値がないとみなされる作業をエンジンが事前にキャンセルすることです。認識機能はプログラミング言語に大きく依存しますが、マッチング中に特定のオプションを指定して、コストのかかる特定の機能を無効にできる場合は、この最適化を簡単に実装できます。
Tcl はこの最適化が可能であり、そのキャプチャ ブラケットは、ユーザーが明示的に要求しない限り、実際にはテキストをキャプチャしません。.NET の正規表現には、プログラマがキャプチャ ブラケットをキャプチャする必要があるかどうかを指定できるオプションが用意されています。
5、表現速度を上げるコツ
前に紹介したのは、従来の NFA エンジンで使用されるさまざまな最適化です。従来の NFA の動作原理も理解し、この知識を組み合わせると、次の 3 つの側面から恩恵を受けることができます。
- 最適化に適した正規表現を記述する
既知の最適化手段に適応する式を作成します。たとえば、xx* は、ターゲット文字列に出現する必要がある文字をチェックしたり、文字認識を開始したりするなど、x+ よりも多くの最適化手段を適用できます。
- シミュレーションの最適化
最適化措置を手動でシミュレートすることで、多くの時間を節約できます。たとえば、this|that の前に (?=t) を追加すると、システムが一致結果が t で始まる必要があると予測できない場合でも、最初の文字認識をシミュレートできます。
- ドミナントエンジンマッチング
従来の NFA エンジンがどのように動作するかについての知識を利用して、より高速に一致するようにエンジンを指示できます。this|that を例に挙げると、各複数選択ブランチは th で始まります。最初の複数選択ブランチが th に一致できない場合、2 番目の複数選択ブランチも明らかに一致できないため、労力を無駄にする必要はありません。したがって、th(?:is|at) を使用できます。このように、th は 1 回チェックするだけで済み、比較的高価な複数選択構造機能は本当に必要な場合にのみ使用されます。また、th(?:is|at) の先頭の平文文字は th なので、他の最適化が行われる可能性があります。
効率化や最適化は難しい場合もありますので、以下の点に注意してください。
- 役立つように見える変更を加えると、他の最適化の効果が妨げられる可能性があるため、逆効果になることがあります。
- 最適化措置をシミュレートするためにコンテンツを追加すると、節約できる時間よりも、それらの追加の処理に費やす時間の方が長くなる可能性があります。
- 現在提供されていない最適化をシミュレートするコンテンツを追加します。この最適化をサポートするためにソフトウェアが将来アップグレードされると、実際の最適化に影響が及ぶか、それが繰り返されます。
- 同様に、制御式は現在利用可能な特定の最適化をトリガーしようとしますが、将来のソフトウェアのアップグレード後には、より高度な最適化が利用できなくなる可能性があります。
- 効率を向上させるために式を変更すると、式の理解や保守が難しくなる可能性があります。
- 特定の変更によって生じる利益または害の程度は、基本的には式が適用されるデータによって異なります。あるタイプのデータにとって有益な変更が、別のタイプのデータにとって有害となる可能性があります。
極端な例を考えてみましょう。Perl スクリプトで (000|999)$ を見つけたいと思い、これらのキャプチャ ブラケットを非キャプチャ ブラケットに置き換えることにしました。これは、テキストをキャプチャするオーバーヘッドがなくなり、高速になると思うからです。しかし不思議なことに、この小さくて有益に見える変更により、実際には式が数桁遅くなります。どうしてこうなりました?ここではいくつかの要因が連携していることがわかり、非キャプチャ括弧を使用すると、「文字列の終わり/行アンカーの最適化」がオフになります。非捕捉括弧はほとんどの場合に有益ですが、場合によっては悲惨な結果を招く可能性があります。
目的のアプリケーションに対して同じ種類のデータを検出してパフォーマンス テストを行うことは、変更に価値があるかどうかを判断するのに役立ちますが、考慮すべき要素はまだたくさんあります。
1. 常識的な最適化
(1) 再コンパイルを避ける
正規表現のコンパイルと定義は、できるだけ少なくする必要があります。オブジェクト指向処理では、ユーザーはこれを正確に制御できます。たとえば、ループ内で正規表現を適用する場合は、ループの外で正規表現オブジェクトを作成し、それをループ内で再利用する必要があります。
GNU Emacs や Tcl などの関数処理の場合、ループで使用される正規表現の数がツールがキャッシュできる上限よりも少なくなるようにする必要があります。
Perl などの統合処理を使用している場合は、値が変わらない場合でもループごとに正規表現を再生成する必要があるため、ループ内の正規表現で変数補間を回避するように努める必要があります (ただし、Perl には変数補間を回避する効率的な方法が用意されています)この問題)。
(2) 非捕捉ブラケットを使用する
括弧内のテキストを引用する必要がない場合は、非キャプチャ括弧 (?:...) を使用します。これにより、キャプチャ時間が節約されるだけでなく、バックトラックに使用される状態の数も減り、2 つの点で速度が向上します。また、不要な括弧を削除するなど、さらに最適化を行うこともできます。
(3) 括弧を乱用しないでください
必要な場合は括弧を使用します。その他の場合、括弧を使用すると特定の最適化が妨げられます。.* が一致した最後の文字を知る必要がない限り、(.)* は使用しないでください。
(4) キャラクターグループを乱用しないでください
^.*[:] などの単一文字の文字グループは使用しないでください。これは文字グループが提供する複数文字マッチング機能を使用しませんが、文字グループを処理する代償を支払う必要があります。メタキャラクターを照合する場合は、文字グループの代わりにエスケープを使用します ([.] や [*] の代わりに \. や \* など)。^[Ff][Rr][Oo][Mm] のようなものを、大文字と小文字を区別しない一致に置き換えます。
(5) 開始アンカーポイントを使用する
非常にまれなケースを除き、.* で始まる正規表現の前には ^ または \A を付ける必要があります。正規表現が文字列の先頭で一致しない場合は、明らかに他の部分でも一致しません。アンカーポイントの追加(手動で追加するか、エンジンによって自動的に追加するかに関係なく)は、「先頭文字/文字グループ/部分文字列認識の最適化」と連携して、不要な作業を大幅に節約できます。
2. リテラルテキストを分離する
ここでは、テキストを公開し、エンジン認識の可能性を高め、エンジンのテキストの最適化と連携するのに役立つ手動の最適化措置をいくつか紹介します。
(1) 量指定子から必要な要素を「抽出」する
x+ を xx* に置き換えると、一致に必要な x が表示されます。同様に、-{5,7} は ------{0,2} と書くことができます。
(2) 複数選択構造の先頭で必要な要素を「抽出」する
th(?:is|at) を (?:this|that) に置き換えると、必要な th が表示されます。異なる複数選択分岐の終了部分が同じ場合は、(?:optim|standard) 化のように右から「抽出」することもできます。次のセクションで説明するように、抽出された部分にアンカー ポイントが含まれている場合、これは非常に価値があります。
3. アンカーポイントを分離する
一部の効果的な内部最適化では、^、$、\G などのアンカー ポイントを使用して式をターゲット文字列の一端に「バインド」します。これらの最適化を使用するときに役立つテクニックがいくつかあります。
(1) 式の前で ^ と \G を区切る
^(?:abc|123) と ^abc|^123 は論理的に同等ですが、多くの通常のエンジンは最初の式に「開始文字/文字グループ/部分文字列認識の最適化」のみを使用するため、最初の One アプローチの方がはるかに効率的です。 。PCRE とそれを使用するツールでは両方の効率は同じですが、他のほとんどの NFA ツールでは最初の式の方が効率的です。
(^abc) と ^(abc) を比較すると、別の違いが明らかになることがあります。前者の設定はあまり適切ではありません。アンカー ポイントはキャプチャする括弧内に「隠されています」。アンカー ポイントを検出するには、括弧を入力する必要があります。多くのシステムでは、これは非常に非効率的です。一部のシステム (PCRE、Perl、.NET) ではこの 2 つは同等に効率的ですが、他のシステム (Ruby と Java) では後者のみが最適化されます。
(2) 式の最後に$を区切る
この対策は前節の最適化の考え方と似ており、abc$|123$ と (?:abc|123)$ は論理的には等価ですが、最適化のパフォーマンスは異なる可能性があります。現在、「文字列の終わり/行アンカーの最適化」を提供しているのは Perl だけであるため、この最適化は現在 Perl でのみ利用できます。最適化は (...|...)$ に対しては機能しますが、(...$|...$) に対しては機能しません。
4. 最初に無視しますか、それとも最初に一致しますか? 特定の状況の詳細な分析
通常,使用忽略优先量词还是匹配优先量词取决于正则表达式的具体需求。举例来说,^.*: 完全不同于 ^.*?:,因为前者匹配到最后的冒号,而后者匹配到第一个冒号。但是,如果目标数据中只包含一个冒号,两个表达式就没有区别了,匹配到唯一的冒号为止,所以选择速度更快的表达式可能更合适。
不过并不是任何时候优劣都如此分明,大的原则是,如果目标字符串很长,而认为冒号会比较接近字符串的开头,就使用忽略优先量词,这样引擎能更快地找到冒号。如果认为冒号在接近字符串末尾的位置,就使用匹配优先量词。如果数据是随机的,又不知道冒号会靠近哪一头,就使用匹配优先量词,因为它们的优化一般来说要比其他量词更好,尤其是表达式的后面部分禁止进行“忽略优先量词之后的字符优化”时,更是如此。
如果待匹配的字符串很短,差别就不那么明显了。这时候两个正则表达式的速度都很快,不过如果很在乎那一点点的速度差别,就对典型数据做个性能测试吧。
一个与此有关的问题是,在忽略优先量词和排除型字符组之间(^.*?: 与 ^[^:]*:),应该如何选择?答案还是取决于所使用的编程语言和应用的数据,但是对大多数引擎来说,排除型字符组的效率比忽略优先量词高得多。Perl 是个例外,因为它能对忽略优先量词之后的字符进行优化。
5. 拆分正则表达式
有时候,应用多个小的正则表达式的速度比一个大正则表达式要快得多。举个极端的例子,如果要检查一个长字符串中是否包含月份的名字,依次检查 January、February、March 之类的速度,要比 January|February|March|... 快得多。因为对后者来说,不存在匹配成功必须的文字内容,所以不能进行“内嵌文字字符串检查优化”。“大而全”的正则表达式必须在目标文本中的每个位置测试所有的子表达式,速度相当慢。
別の興味深い例を見てみましょう。HASH(0x80f60ac) に類似したデータを見つけるために使用される正規表現は非常に簡単です: \b(?:SCALAR|ARRAY|...|HASH)\(0x[0-9a-fA- F]+\)。十分に高度なエンジンが (0x はあらゆる一致に必要であるため、「プリフェッチが必要な文字/部分文字列の最適化」を有効にする) ことを理解することを期待する人もいるでしょう。この正規表現が適用されるデータには、(0x、プリフェッチにより大幅に節約できます) が含まれることはほとんどありません。残念ながら、Perl はこれを行わず、各ターゲット文字列の各文字に対して正規表現全体の多数の複数選択分岐をテストしますが、十分な速度ではありません。
最適化方法の 1 つは、\(0x(?<=(?:SCALAR|ARRAY|...|HASH)\(0x)[0-9a-fA-F]+\) という複雑な方法です。このように、 Once\ (0x 一致した後、肯定的な逆検索により、前に一致したテキストが必須のテキストであることを確認し、後続のテキストが期待どおりであるかどうかを確認できます。この問題の理由は、正規表現で表示する必要があるテキストを取得できるようにするためです。 \(0x 。これにより、さまざまな最適化を実行できます。特に、事前チェックを実行する場合は、文字列を最適化し、「先頭文字/文字グループ/部分文字列の認識を最適化」する必要があります。
Perl が \(0x を自動的に検索しない場合は、手動で検索できます。
if ($data =~ m/\(0x/
and
$data =~ m/(?:SCALAR|ARRAY|...|HASH)\(0x[0-9a-fA-F]+\)/)
{
# 错误数据报警
}
\(0x チェックは実際にはほとんどのテキストを除外し、比較的遅い完全な正規表現は一致する可能性が高い行のみをチェックするため、効率と読みやすさのバランスが取れます。
6. 文字認識の開始をシミュレートする
使用している実装が文字認識を開始するために最適化されていない場合は、問題を自分で解決して、式の先頭に適切なルックアラウンド構造を追加できます。正規表現の残りの部分が一致する前に、リング構造を「先読み」して適切な開始位置を選択できます。
正規表現が Jan|Feb|...|Dec の場合、対応する値は (?=[JFMASOND])(?:Jan|Feb|...|Dec) となります。先頭の [JFMASOND] は、英語で月の最初の文字となる可能性のある単語を表します。ただし、構造内を探索するオーバーヘッドが節約される時間よりも大きくなる可能性があるため、この手法はすべての場合に適しているわけではありません。
通常のエンジンが[JFMASOND]を自動的に検出できれば、当然のことながら、ユーザーが手動で指定した速度よりもはるかに高速になります。多くのシステムでは、次の複雑な式を使用してエンジンを自動的に検出できます:
[JFMASOND](?:(?<=J)an|(?<=F)eb|...|(?<=D) ec)
式の先頭の文字は、ほとんどのシステムの「最初の文字/文字/部分文字列認識の最適化」によって利用できるため、送信を効率的に先読みできます [JFMASOND]。ターゲット文字列に一致する文字が含まれていない場合、結果は元の Jan|Feb|...|Dec または手動ルックアラウンドを使用した式よりも高速になります。ただし、ターゲット文字列に多くの文字グループが一致する文字が含まれている場合、追加の逆引きにより一致が実際に遅くなる可能性があります。
7. 固定グループ化と所有数量詞を使用する
多くの場合、固定化されたグループ化と所有量指定子によってマッチングの速度が大幅に向上しますが、マッチング結果は変わりません。たとえば、^[^:]+: のコロンが最初の試行で一致しない場合、バックトラッキングによって「返された」文字は定義上コロンになり得ないため、バックトラッキングは実際には無意味になります。固定化されたグループ化^(?>[^:]+): を使用するか、優先数量子^[^:]++: を占有すると、スタンバイ状態を直接破棄するか、多くのスタンバイ状態をまったく作成しなくなります。エンジンにはバックトラックできるコンテンツ状態がないため、不必要なバックトラックが回避されます。
ただし、これら 2 つの構造を不適切に使用すると、マッチング結果が誤って変更される可能性があるため、細心の注意を払う必要があることを強調しなければなりません。^.*: を使用せずに ^(?>.*): を使用すると、結果は確実に失敗します。テキスト行全体は .* と一致し、次の文字はどの文字とも一致しません。強化されたグループ化により、最後の : 一致で行う必要があるバックトラックが防止されるため、一致は失敗するはずです。
8. ドミナントエンジンのマッチング
正規表現マッチングの効率を向上させるもう 1 つの方法は、マッチング プロセスの「コントロール」をできるだけ正確に設定することです。たとえば、th(?:is|at) を使用して this|that を置き換えます。後者の式では、複数選択構造が最高レベルの制御を取得しますが、前者の式では、比較的高価な複数選択構造は、 th が一致した後にのみ制御を取得します。
(1) 一致する可能性が最も高い複数選択ブランチを最初に配置します。
複数選択分岐の順序が一致結果に関係がない場合は、一致する可能性が最も高い複数選択分岐が最初に配置される必要があります。たとえば、ホスト名を照合する正規表現では、ディストリビューションの数 (?:com|edu|org|net|...) で並べ替えると、より一般的な一致がすぐに得られる可能性が高くなります。
もちろん、これは従来の NFA エンジンにのみ適用され、一致するものが存在する場合にのみ適用されます。POSIX NFA が使用されている場合、または一致しない場合は、すべての複数選択ブランチをテストする必要があるため、順序は重要ではありません。
(2) エンディング部分を選択構造に分割する
(?:com|edu|...|[az][az])\b と com\b|edu\b|...\b|[az][az]\b を比較します。後者の式では、複数選択構造に続く \b が各複数選択分岐に分散されます。考えられる利点は、複数選択分岐の照合が可能になる可能性があることですが、後続の \b により照合が失敗する可能性があることです。複数選択構造に \b を追加すると、複数選択構造を終了せずに失敗を検出するため、一致の失敗が速くなります。
この最適化には危険が伴います。この機能を使用する場合は、他の方法で実行できる他の最適化を妨げないよう注意してください。たとえば、「dispersed」部分式がリテラル テキストの場合、(?:this|that): を this:|that: に置き換えることは、「リテラル テキストの分離」の考え方の一部に違反します。すべての最適化は同等であるため、最適化するときは注意して、小さなことでも大きなことを失わないようにしてください。
この問題は、独立した終了アンカーが可能なシステムで正規表現の末尾に $ を分散させた場合にも発生します。これらのシステムでは、(?:com|edu|...)$ は com$|edu$|...$ よりもはるかに高速です。
9. ループを排除する
システム自体がどのような最適化をサポートしているとしても、おそらく最も重要な利点は、エンジンの基本的な動作原理を理解し、エンジンで動作する式を作成することによって得られます。ここで言う「ループ」とは、実際には (this|that|...)* や、その前の無限一致 "(\\.|[^\\"]+) * などの式のアスタリスクで表される意味を使用します。一致しない場合、この式は試行に無限に近い時間がかかるため、改善する必要があります。
この手法を実装するには 2 つの異なる方法があります。
- さまざまな典型的な一致の中で、(\\.|[^\\"]+)* のどの部分が実際に一致するかを確認し、部分式の痕跡を残すことができます。次に、見つかったパターンに基づいて効率的な式を再構築します。この概念は、モデルは式 (...)* を表す大きなボールで、ボールはテキストの上を転がります。(...) 内の要素は常にテキストと一致するため、Leaves はトレースします。
- もう 1 つのアプローチは、高レベルで一致が期待される構造を調べ、一般的な状況に基づいて、ターゲット文字列がどのようなものである可能性があるかについて非公式の仮定を立てることです。この観点から有効な式を構築します。
(1) 方法 1: 経験に基づいて正規表現を構築する
"(\\.|[^\\"]+)*" を解析する場合、グローバル一致をチェックするためにいくつかの特定の文字列を使用するのが自然です。たとえば、ターゲット文字列が "hi" の場合、使用される部分式は次のようになります。 「[^\\"]+」。これは、グローバル マッチングでは最初の "、次に複数選択分岐 [^\\"]+、そして最後に " が使用されることを示しています。ターゲット文字列が "he Said \"hi there\" and left" の場合、 、対応する式は "[^\\"]+\\.[^\\"]+\\.[^\\"]+" です。入力文字列ごとに特定の式を作成することは不可能ですが、いくつかの一般的なパターンを見つけて、汎用性を損なうことなくより効率的な正規表現を作成することはできます。
次に、以下の表の最初の 4 行の例を見てください。
ターゲット文字列 |
対応する式 |
"やあ" |
「[^\\"]+」 |
「ここに\を1つだけ」 |
"[^\\"]+\\.[^\\"]+" |
「いくつかの「引用された」もの」 |
"[^\\"]+\\.[^\\"]+\\.[^\\"]+" |
「「a」と「b」を使用します。」 |
"[^\\"]+\\.[^\\"]+\\.[^\\"]+\\.[^\\"]+\\.[^\\"]+" |
"\"わかりました\"\n" |
"\\.[^\\"]+\\.\\." |
「空の \"\" 引用」 |
"[^\\"]+\\.\\.[^\\"]+" |
表2
いずれの場合も、引用符で始まり、[^\\"]+、さらにいくつかの \\.[^\\"]+ が続きます。組み合わせると、[^\\"]+(\\.[^\\"]+)* になります。この特定の例は、一般的なパターンを使用して多くの有用な式を構築できることを示しています。
二重引用符で囲まれた文字列と一致する場合、引用符自体とエスケープされたスラッシュは「特別」です。引用符は文字列の終わりを示し、バックスラッシュはその後の文字が文字列全体を終了しないことを意味するためです。他のケースでは、[^\\"] は通常のピリオドです。これらがどのように結合されて [^\\"]+(\\.[^\\"]+)* になるかを調べてください。まず、一般的なパターンに従っています。 Normal+ (specialnormal+)*。両端に引用符を追加すると、「[^\\"]+(\\.[^\\"]+)*」となります。
ただし、表 2 の最後の 2 行の例は、この式では一致しません。重要なのは、現在の式の 2 つの [^\\"]+ では、文字列が通常の文字で始まる必要があるということです。2 つのプラス記号をアスタリスク "[^\\"]*(\\.[ ^\\"]*)*"。これで望ましい結果が得られますか?さらに重要なのは、マイナスの影響を与えるかどうかです。
これで、「\"\"\"" のような文字列も含め、表 2 のすべての例が一致しました。ただし、そのような大きな変更が予期せぬ結果につながるかどうかを確認する必要があります。形式が正しくない引用符 文字列は一致しますか? その可能性はありますか?適切にフォーマットされた引用符で囲まれた文字列は一致しませんか?効率についてはどうですか?
「[^\\"]*(\\.[^\\"]*)*」を詳しく見てみましょう。先頭の "[^\\"]* は 1 回だけ適用され、先頭に表示する必要がある引用符とその後の通常の文字に一致しますが、これは問題ありません。次の (\\.[^\\"]*)* はアスタリスクで修飾されます。この部分が 0 回一致した場合、この部分を削除することと同じになり、「[^\\"]*」が得られます。 。これは明らかに問題なく、エスケープ要素がない一般的なケースを表しています。
(\\.[^\\"]*)* 部分が 1 回一致すると、実際には "[^\\"]*\\.[^\\"]*" と同等になります。 \ \"]* はどのテキストとも一致せず、実際には "[^\\"]*\\." であり、問題ありません。 このように分析すると、実際には問題がないことがわかります。この変更により、最終的に が得られ、エスケープされた引用符を含む引用符で囲まれた文字列の一致に使用される正規表現は、「[^\\"]*(\\.[^\\"]*)*」になります。これは元の文字列と一致します。式 結果は完全に一貫していますが、ループが除去された後、式は限られた時間内でマッチングを終了できるため、効率が大幅に向上するだけでなく、無限のマッチングも回避されます。
ループを排除する一般的な解決策は次のとおりです。
opening normal*(special normal*)* closing
"[^\\"]*(\\.[^\\"]*)*" での無限の一致を避けるには、次の 3 つの点が重要です。
1) 特別部分と通常部分の一致する先頭は重複できません。
特殊部分と通常部分の部分式は、同じ位置から開始して一致することはできません。上の例では、通常の部分は [^\\"] で、特殊な部分は \\ です。後者では最初にバックスラッシュが必要ですが、前者ではバックスラッシュが許可されないため、同じ文字に一致することは明らかにありません。
一方、\\. と [^"] はどちらも "Hello \n" のバックスラッシュから始まる一致するため、この解決策には当てはまりません。両方が文字列内の同じ位置から始まる場合、方法はありません。どれを使用するかを知る必要があり、この不確実性が無限のマッチングにつながります。Makudonarudo の例がこれを示しています。一致ができない場合 (または POSIX NFA エンジンがいずれの場合も一致する場合)、すべての可能性を試さなければなりません。これを改善する最初の理由この状況を避けるための表現です。
特殊部分と通常部分が同じ文字に一致できないことを確認した場合は、(...)* の各反復で通常部分が同じテキストに一致することによって生じる不確実性を排除するためのチェックポイントとして特殊部分を使用できます。特殊部分と通常部分が決して同じテキストに一致しないことが確認できれば、特定の対象文字列の一致において、特殊部分と通常部分の一意な「組み合わせ順序」が存在することになる。このシーケンスをチェックすることは、何千もの可能性をチェックするよりもはるかに高速であるため、無限の一致を回避できます。
2) 特殊部分は少なくとも 1 文字と一致する必要がある
2 番目の点は、特殊部分は少なくとも 1 文字と一致する必要があるということです。文字の特殊部分を占有せずに照合が成功した場合でも、後続の文字は (specialnormal*)* の異なる反復によって照合される必要があるため、元の (...*)* の問題に戻ります。
(\\.)* を特殊部分として選択すると、このルールに違反します。"[^\\"]*((\\.)*[^\\"]*)*" を使用して "Tubby" を照合する場合、エンジンは照合が失敗したと判断する前に複数の [^\\" を試行する必要があります。 ]* はタビーのあらゆる可能性と一致します。特殊部分はどの文字とも一致しないため、チェックポイントとして使用できません。
3) 特殊パーツはソリッド化された
特殊パーツである必要があり、このパーツを複数回繰り返すことでマッチング テキストを完成させることはできません。たとえば、Pascal に表示される可能性のあるコメント {...} と空白を一致させる必要があります。コメント部分と一致する正規表現は \{[^}]*\} であるため、正規表現全体は (\{[^}]*\}| +)* となります。' +' と \{[^}]*\} がそれぞれ特殊部分と通常部分に分割されていると仮定します。Normal*(special Normal*)* の解を使用すると、(\{[^}]*\})*( +(\{[^}]*\})*)* が得られます。次に、この文字列を見てください:
{comment} {another}
連続するスペースの一致は、単一の「+」、複数の「+」の一致(それぞれがスペースに一致)、または複数の「+」の組み合わせ(それぞれが異なる数のスペースに一致)の場合があります。これは、以前の「マクドナルド」問題と非常によく似ています。
問題の根本は、特別な部分が非常に長いテキストとその一部 (...)* の両方に一致する可能性があることです。非決定論は、「同じテキストを照合する複数の方法」への扉を開きます。
グローバルな一致がある場合、「 + 」は 1 回だけ一致する可能性がありますが、グローバルな一致がない場合 (たとえば、この式が別のより大きな式の一部である場合)、エンジンはすべての場合に '( +) をテストする必要があります。スペース *'すべての可能性。時間はかかりますが、グローバルマッチングには役に立ちません。
解決策は、特別な部分が固定長のスペースのみに一致できるようにすることです。少なくとも 1 つのスペースと一致する必要がありますが、さらに多くのスペースと一致する可能性があるため、特殊部分として ' ' を使用し、(...)* を使用して、special を複数回使用しても複数のスペースと一致するようにします。
この例は説明に適していますが、実際のアプリケーションでは、特殊な式と通常の式「*(\{[^}]*\} *)*」を交換する方が効率的です。Pascal プログラムにはコメントよりも多くのスペースが含まれていると推定されており、一般的な状況でより効果的な方法は、通常の部分を使用して一般的なテキストと一致させることであるためです。
(...*)* など、さまざまなレベルで複数の量指定子がある場合は注意が必要ですが、そのような式の多くはまったく問題ありません。例えば:
- (Re: *)* は、任意の数の「Re:」シーケンスと一致するために使用されます (電子メールの件名の「件名: Re: Re: re: hey」をクリアするために使用できます)。
- ( *\$[0-9]+)* はドル金額を照合するために使用され、スペースで区切ることもできます。
- (.*\n)+ は、1 行以上のテキストと一致するために使用されます。(実際、ドットが改行文字と一致せず、この部分式の後に一致を失敗させる他の要素がある場合、無限の一致が発生します。)
これらの表現には問題はありません。それぞれにチェックポイントがあるため、「同じテキストが複数の方法で一致する」という問題は発生しません。1 つ目は Re:、2 つ目は \$、3 つ目は \n (ピリオドが改行文字と一致しない場合) です。
(2) 方法2:トップダウンの視点
まず、ターゲット文字列の最も一般的な部分のみを照合してから、まれなケースの処理を追加します。無限に一致する式 (\\.|[^\\"]+)*、一致すると予想されるテキスト、およびそれが使用される場所を見てみましょう。通常、引用符で囲まれた文字列内の通常の文字はエスケープ文字よりも優れています。文字数が多いため、[^\\"]+ でほとんどの作業が行われます。\\. は、時折発生するエスケープ文字を処理する場合にのみ必要です。複数選択構造を使用してこれら 2 つの状況に対処できますが、少数のエスケープ文字を処理するために使用すると効率が低下します。
[^\\"]+ が文字列内のほとんどの文字に一致すると考えられる場合、一致が停止するということは、閉じ引用符またはエスケープ文字に遭遇したことを意味することがわかります。エスケープ文字の場合は、任意の文字が使用されます。は後で表示することができ、[^\\"]+ の新しいラウンドのマッチングを開始します。[^\\"]+ の一致が終了するたびに、終了引用符または別のエスケープが期待されるという同じ状況になります。
これを式で自然に表現すると、方法 1 と同じ結果が得られます:
"[^\\"]+(\\.[^\\"]+)*"
前と同様に、引用符で囲まれていない最初のコンテンツまたは引用符内のテキストは空でも構いません。2 つのプラス記号をアスタリスクに変更すると、方法 1 と同じ式が得られます。
(3) ホスト名を一致させる
ホスト名は基本的に、ドットで区切られた一連のサブドメイン名です。サブドメイン名の一致仕様を正確に定義するのは面倒なので、明確にするために、[az]+ を使用してサブドメイン名を一致させます。サブドメイン名が [az]+ で、ドットで区切られた一連のサブドメイン名を取得する場合は、最初に最初のサブドメイン名を一致させてから、他のサブドメイン名がドットで始まるようにする必要があります。正規表現表現は [az]+(\.[az]+)* です。
概念的には、ドット区切りのホスト名の問題は、二重引用符で囲まれた文字列、つまり「エスケープされた要素で区切られたエスケープされていない要素のシーケンス」の問題と考えることができます。ここでの通常部分は [az]+ であり、特殊部分 \. で区切られており、方法 1 のループ除去ソリューションを適用できます。
サブドメインの例は二重引用符で囲まれた文字列の例と同じカテゴリに属しますが、2 つの大きな違いがあります。
- ドメイン名の先頭と末尾には区切り文字はありません。
- サブドメイン名の通常の部分を空にすることはできません。つまり、2 つのドットを隣接させることはできず、ドメイン名全体の先頭または末尾にドットを表示することもできません。二重引用符で囲まれた文字列の場合、通常の部分は空になる可能性があるため、[^\\"]+ を [^\\"]* に変更する必要があります。この変更は、サブドメイン名の例では行うことができません。
二重引用符で囲まれた文字列の例を振り返ると、式 "[^\\"]*(\\.[^\\"]*)*" の長所と短所は明らかです。
欠点:
- 可読性:これが最大の問題です。元の「([^\\"]|\\.)*」の方が一目で分かりやすいのですが、ここでは効率を追求するため可読性を放棄しています。
- 保守性: 変更は両方の [^\\"] で同じままでなければならないため、保守性はさらに複雑になる可能性があります。ここでは、効率を追求するために保守性が犠牲になっています。
アドバンテージ:
- 速度: この正規表現は、一致できない場合、または POSIX NFA が使用されている場合、無限一致にはなりません。慎重に調整されているため、特定のテキストは独自の方法でのみ一致し、テキストが一致しない場合はエンジンがすぐに検出します。
- 繰り返しになりますが、速度: 正規表現は「動作フロー」に優れており、「スムーズに動作する正規表現」のテーマでもあります。従来の NFA のテストでは、ループを削除した後の式は常に、複数選択構造を使用した前の式よりもはるかに高速でした。これは、マッチングが成功した場合でも同様であり、エンドレスマッチング状態にはなりません。
(4) 固定化されたグループ化と所有優先度数量詞を使用する
式「(\\.|[^\\"]+)*」の問題点は、無限に一致する状態になってしまうことです。一致しない場合は無駄な試みに陥ることです。 [^\\"]+ はターゲット文字列のほとんどの文字と一致するため、これは前に説明した通常の部分です。[...]+ は一般に速度を重視して最適化されており、ほとんどの文字に一致するため、外側の (...)* 量指定子のオーバーヘッドが大幅に削減されます。
"(\\.|[^\\"]+)*" の問題は、一致しない場合、無駄なフォールバック状態で後戻りし続けることです。これらの状態は、同じものの異なる順列をチェックするだけであるため、価値がありません。オブジェクト。一致するものはありません。これらの状態を破棄できる場合、正規表現は一致の失敗をすぐに報告できます。これらの状態を破棄 (または無視) するには、固定化されたグループ化と所有優先度数量子という 2 つの方法があります。
バックトラッキングの排除を始める前に、複数選択ブランチの順序を入れ替えて、「(\\.|[^\\"]+)*」を「([^\\"]+|\\」に変更したいと考えています。 )*", このようにして、「通常の」テキストに一致する要素が最初に表示されます。2 つ以上の多肢選択分岐が同じ位置で照合できる場合、順序が照合結果に影響を与える可能性があります。ただし、この例では、異なる複数選択ブランチで一致するテキストは相互に排他的であり、ある複数選択ブランチが 1 か所で一致する場合、他の複数選択ブランチはここでは一致できません。正確なマッチングの観点からは、順序は重要ではないため、順序は明瞭さまたは効率の要件に基づいて選択できます。
- 無限のマッチングを避けるために所有数量詞を使用する
無限のマッチングを引き起こす式 "([^\\"]+|\\.)*" には 2 つの量指定子があります。そのうちの 1 つを所有優先量指定子に変更することも、両方を変更することもできます。 [...]+ で残された状態から変更するため、所有優先順位に変更すると、一致するものが見つからなかった場合でも非常に高速な式が生成されます。ただし、外側の (...)* を所有所有に変更します。 [...]+ と複数選択構造自体の代替状態を含む括弧内のすべての状態なので、どちらかを選択したい場合は後者を選択する必要があります。
両方を所有優先度数量子に変更することもできます。特定の速度は所有優先度数量子の最適化に依存する場合があります。MySQL でのテスト状況では、優先度を持つように外側の量指定子を変更するだけで、他の 2 つの変更よりも 2 倍以上高速になり、他の 2 つはほぼ同じ速度になります。
mysql> set @str:='"empty \\\"\\\" quote"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg1:='"([^\\\\"]++|\\\\.)*"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg2:='"([^\\\\"]+|\\\\.)*+"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg3:='"([^\\\\"]++|\\\\.)*+"';
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> call sp_test_regexp(@str, @reg1, 1000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 0 | 2023-07-17 09:18:46.766 | 2023-07-17 09:18:47.181 | 415.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (0.43 sec)
Query OK, 0 rows affected (0.43 sec)
mysql> call sp_test_regexp(@str, @reg2, 1000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 0 | 2023-07-17 09:18:47.191 | 2023-07-17 09:18:47.376 | 185.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (0.19 sec)
Query OK, 0 rows affected (0.19 sec)
mysql> call sp_test_regexp(@str, @reg3, 1000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 0 | 2023-07-17 09:18:47.386 | 2023-07-17 09:18:47.794 | 408.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (0.42 sec)
Query OK, 0 rows affected (0.42 sec)
- 無限のマッチングを避けるために固定化されたグループ化を使用する
"([^\\"]+|\\.)*" に対してソリッド グループ化を使用したい場合、最も簡単に考える方法は、通常の括弧をソリッド グループ化括弧に変更することです: "(?>[^\\" ]+| \\.)*"。ただし、状態の破棄に関しては、(?>...|...)* は優先度 (...|...)*+ とはまったく異なることを知っておく必要があります。
(...|...)*+ は完了時に状態を残しませんが、(?>...|...)* は複数選択構造の各反復で保存された状態を削除するだけです。アスタリスクは固化グループから独立しているため影響を受けず、この式は引き続き「この反復をスキップ」スタンバイ状態を保持します。つまり、バックトレースの状態はまだ最終的な最終状態ではありません。ここでは、外側の数量指定子のスタンバイ状態を同時に削除したいので、外側の括弧を固定化されたグループ化に変更する必要があります。これは、シミュレートされた所有優先度 (...|...)*+ を使用する必要があることを意味します。 (?>(... |...)*)。
(...|...)*+ と (?>...|...)* は両方とも、無限のマッチング問題を解決するのに役立ちますが、状態を破棄する選択とタイミングが異なります。MySQL でのテスト状況では、2 レベルの括弧に固定化グループ化を使用するのが最も速く、次に内側の括弧のみに固定化グループ化を使用し、外側の括弧のみに固定化グループ化を使用するのが最も遅いです。一般に、3 つの固定化されたグループ化の速度に大きな違いはなく、いずれも最速の所有量指定子メソッド「([^\\"]+|\\.)*+」よりも高速です。
mysql> set @str:='"empty \\\"\\\" quote"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg1:='"(?>[^\\\\"]+|\\\\.)*"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg2:='"(?>([^\\\\"]+|\\\\.)*)"';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg3:='"(?>(?>[^\\\\"]+|\\\\.)*)"';
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> call sp_test_regexp(@str, @reg1, 1000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 0 | 2023-07-17 09:54:29.521 | 2023-07-17 09:54:29.684 | 163.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (0.17 sec)
Query OK, 0 rows affected (0.17 sec)
mysql> call sp_test_regexp(@str, @reg2, 1000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 0 | 2023-07-17 09:54:29.694 | 2023-07-17 09:54:29.874 | 180.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (0.19 sec)
Query OK, 0 rows affected (0.19 sec)
mysql> call sp_test_regexp(@str, @reg3, 1000);
+------+-------------------------+-------------------------+----------+
| @ret | @startts | @endts | diff_ts |
+------+-------------------------+-------------------------+----------+
| 0 | 2023-07-17 09:54:29.884 | 2023-07-17 09:54:30.033 | 149.0000 |
+------+-------------------------+-------------------------+----------+
1 row in set (0.16 sec)
Query OK, 0 rows affected (0.16 sec)
(5) ループ解消の簡単な例
- 「複数文字」の引用における循環を排除する
文字列と一致します...<B>Billions</B> と <B>Zillions</B> の太陽...
mysql> set @str:='<B>Billions</B> and <B>Zillions</B> of suns';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg:='<B>(?>[^<]*)(?>(?!</?B>)<[^<]*)*</B>';
Query OK, 0 rows affected (0.00 sec)
mysql> select regexp_count(@str,@reg,'') c, regexp_extract(@str,@reg,'') s;
+------+---------------------------------+
| c | s |
+------+---------------------------------+
| 2 | <B>Billions</B>,<B>Zillions</B> |
+------+---------------------------------+
1 row in set (0.01 sec)
<B> は先頭の <B> と一致します; (?>[^<]*) は任意の数の「normal」と一致します; <B> でも </B> でもない場合は (?!</?B>); < 「special」に一致します。[^<]* は任意の数の「normal」に一致し続けます。</B> は末尾の </B> に一致します。ここでは固化グループ化は必要ありませんが、部分一致のみの場合は固化グループ化を使用すると速度が向上します。
- 連続する行の一致でループを排除する
mysql> set @str:=
-> 'SRC=array.c buildin.c eval.c field.c gawkmisc.c io.c main.c\\
'> missing.c msg.c node.c re.c version.c';
Query OK, 0 rows affected (0.01 sec)
mysql> set @reg:='^\\w+=((?>[^\\n]*)(?>\\n[^\\n]*)*)';
Query OK, 0 rows affected (0.00 sec)
mysql> select @reg r, regexp_count(@str,@reg,'') c, regexp_extract(@str,@reg,'') s\G
*************************** 1. row ***************************
r: ^\w+=((?>[^\n]*)(?>\n[^\n]*)*)
c: 1
s: SRC=array.c buildin.c eval.c field.c gawkmisc.c io.c main.c\
missing.c msg.c node.c re.c version.c
1 row in set (0.00 sec)
\w+ は先頭のテキストと等号に一致し、(?>[^\n]*) は「normal」に一致し、(?>\n[^\n]*) は「special」および「normal」に一致します。この例では、非 dotall モードを使用しており、\n のみが特殊文字です。dotall モードが使用されている場合、バックスラッシュのみが特殊文字となり、改行を含むその他の文字は通常の文字になります。
mysql> set @str:=
-> 'SRC=array.c buildin.c eval.c field.c gawkmisc.c io.c main.c\\
'> missing.c msg.c node.c re.c version.c';
Query OK, 0 rows affected (0.00 sec)
mysql> set @reg:='^\\w+=((?>[^\\\\]*)(?>\\\\.[^\\\\]*)*)';
Query OK, 0 rows affected (0.00 sec)
mysql> select @reg r, regexp_count(@str,@reg,'mn') c, regexp_extract(@str,@reg,'mn') s\G
*************************** 1. row ***************************
r: ^\w+=((?>[^\\]*)(?>\\.[^\\]*)*)
c: 1
s: SRC=array.c buildin.c eval.c field.c gawkmisc.c io.c main.c\
missing.c msg.c node.c re.c version.c
1 row in set (0.00 sec)
上の例のように、グループ化を強化する必要はありませんが、これによりエンジンは一致の失敗をより速く報告できるようになります。
- CSV 正規表現内のループを排除する
CSV 文字列の一致に使用される正規表現は (?:[^"]|"")* で、通常の部分と特殊な部分、[^"] と "" が区別されます。
mysql> set @s:='Ten Thousand,10000, 2710 ,,"10,000","It\'s ""10 Grand"", baby",10K';
Query OK, 0 rows affected (0.01 sec)
mysql> set @r:='\\G(?:^|,)(?:"((?>[^"]*)(?>""[^"]*)*)"|([^",]*))';
Query OK, 0 rows affected (0.00 sec)
mysql> select replace(trim(both '"' from (trim(leading ',' from regexp_substr(@s,@r,1,lv)))),'""','"') s
-> from (with recursive tab1(lv) as (select 1 lv union all select t1.lv + 1 from tab1 t1
-> where lv < regexp_count(@s, @r, '')) select lv from tab1) t;
+-----------------------+
| s |
+-----------------------+
| Ten Thousand |
| 10000 |
| 2710 |
| |
| 10,000 |
| It's "10 Grand", baby |
| 10K |
+-----------------------+
7 rows in set (0.00 sec)
\G を先頭に追加すると、ドライバープロセスによるトラブルを回避し、効率を向上させることができます。"((?>[^"]*)(?>""[^"]*)*)" は二重引用符で囲まれたフィールドに一致し、([^",]*) は引用符とカンマの外側のテキストに一致します。およびその他の例も同様、凝固グループ化は必要ありませんが、効率を向上させることができます。
- C言語のコメント内のループを排除する
C 言語では、コメントは /* で始まり */ で終わり、複数行を含めることができますが、入れ子にすることはできません (C++、Java、および C# でもこの形式のコメントは許可されています)。最も簡単な方法は、すべての文字に一致するドットを無視した優先数量子 /\*.*?\*/ を使用することです。
mysql> set @s:='/** some comment here **/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*.*?\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+-----------+------+---------------------------+
| @r | c | s |
+-----------+------+---------------------------+
| /\*.*?\*/ | 1 | /** some comment here **/ |
+-----------+------+---------------------------+
1 row in set (0.00 sec)
ループ除去手法を使用して C 言語の注釈を照合することも効率的です。ターミネータ */ は 2 文字であるため、/\*[^*]*\*/ を直接使用すると、コメント コンテンツ内のアスタリスクと一致できません。
mysql> set @s:='/** some comment here **/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*[^*]*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @s, @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+---------------------------+-------------+------+------+
| @s | @r | c | s |
+---------------------------+-------------+------+------+
| /** some comment here **/ | /\*[^*]*\*/ | 0 | |
+---------------------------+-------------+------+------+
1 row in set (0.00 sec)
より明確に確認するには、この例では /*...*/ の代わりに /x...x/ を使用します。このように、/\*[^*]*\*/ は /x[^x]*x/ になり、バックスラッシュのエスケープがなくなり、理解しやすくなります。
区切り文字内のテキストと一致する式は次のとおりです。
- 開始区切り文字と一致します。
- テキストの一致: 「終了区切り文字を除く任意の文字」と一致します。
- 終了区切り文字と一致します。
/x と x/ を開始区切り文字と終了区切り文字として使用すると、「終了区切り文字を除く任意の文字」との一致が困難になります。終了区切り文字が 1 文字の場合、排他的な文字グループを使用できますが、その文字グループを複数文字のマッチングに使用することはできません。ただし、負の順次参照 (?:(?!x/).)* は「終了区切り文字を除く任意の文字」であるため、/x(?:(?!x/).)*x/ が得られます。正常に動作しますが、非常に遅いです。
mysql> set @s:='/** some comment here **/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*(?:(?!\\*/).)*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+---------------------+------+---------------------------+
| @r | c | s |
+---------------------+------+---------------------------+
| /\*(?:(?!\*/).)*\*/ | 1 | /** some comment here **/ |
+---------------------+------+---------------------------+
1 row in set (0.00 sec)
逐次検索をサポートするほとんどすべてのジャンルは優先度数量詞の無視をサポートするため、/x.*?x/ を使用できるため、効率は問題になりません。
最初の x/ より前のテキストを照合するには 2 つの方法があります。1 つは、開始区切り文字と終了区切り文字として x を使用することです。つまり、x 以外の任意の文字と一致し、後続の文字はスラッシュではありません。このように、「終了区切り文字を除く任意の文字」は次のようになります。
- x を除く任意の文字: [^x]。
- 次の文字はスラッシュ x ではありません: x[^/]。
その結果、本文テキストと一致する場合は ([^x]|x[^/])* となり、コメント全体と一致する場合は /x([^x]|x[^/])*x/ になります。ただし、このパスは機能しません。
mysql> set @s:='/** some comment here **/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*([^*]|\\*[^/])*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+----------------------+------+------+
| @r | c | s |
+----------------------+------+------+
| /\*([^*]|\*[^/])*\*/ | 0 | |
+----------------------+------+------+
1 row in set (0.00 sec)
/x([^x]|x[^/])*x/ を使用して /xx foo xx/ と一致させる場合、「foo 」の後、最初の x は x[^/] と一致しますが、問題ありません。ただし、後者の x は [^/] と一致し、この x はコメントの終わりをマークする必要があります。したがって、次の反復ラウンドに進み、[^x] はスラッシュと一致し、結果は x/ の後のテキストと一致しますが、最後のスラッシュとは一致しません。
もう 1 つの方法は、x の直後に続くスラッシュを終了区切り文字として扱うことです。つまり、「終了区切り文字を除く任意の文字」は次のようになります。
- スラッシュを除く任意の文字: [^/]。
- x の直後のスラッシュではありません: [^x]/。
したがって、([^/]|[^x]/)* は本文と一致するために使用され、/x([^/]|[^x]/)*x/ はコメント全体と一致するために使用されます。残念ながら、これも行き止まりです。
mysql> set @s:='/*/ some comment here /*/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*([^/]|[^*]/)*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+---------------------+------+------+
| @r | c | s |
+---------------------+------+------+
| /\*([^/]|[^*]/)*\*/ | 0 | |
+---------------------+------+------+
1 row in set (0.00 sec)
/x([^/]|[^x]/)*x/ は /x/ foo /x/ と一致できません。コメントの末尾にスラッシュが続く場合、式はコメントの終了区切り文字よりも多く一致します。これは前の方法の場合にも当てはまります。
mysql> set @s:='/** some comment here **// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*([^*]|\\*[^/])*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+----------------------+------+----------------------------------+
| @r | c | s |
+----------------------+------+----------------------------------+
| /\*([^*]|\*[^/])*\*/ | 1 | /** some comment here **// foo*/ |
+----------------------+------+----------------------------------+
1 row in set (0.00 sec)
mysql> set @s:='/*/ some comment here /*// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*([^/]|[^*]/)*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+---------------------+------+------------+
| @r | c | s |
+---------------------+------+------------+
| /\*([^/]|[^*]/)*\*/ | 1 | /*// foo*/ |
+---------------------+------+------------+
1 row in set (0.00 sec)
それでは、これらの式を修正しましょう。最初のケースでは、x[^/] は末尾のスラッシュの前の xx と一致します。/x([^x]|x+[^/])*x/ を使用する場合、プラス記号を追加した後、x+[^/] はスラッシュ以外の文字で終わる一連の x と一致します。確かにこのように一致する可能性はありますが、「スラッシュ以外の文字」をバックトラックすると一致が多すぎる可能性があるためです。
mysql> set @s:='/** some comment here **// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*([^*]|\\*+[^/])*\\*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+-----------------------+------+----------------------------------+
| @r | c | s |
+-----------------------+------+----------------------------------+
| /\*([^*]|\*+[^/])*\*/ | 1 | /** some comment here **// foo*/ |
+-----------------------+------+----------------------------------+
1 row in set (0.00 sec)
この問題を解決するには、「スラッシュではない文字に続く x」を x+[^/x] として使用する必要があります。これにより、'...xxx/' の最初の x 位置にバックトラックして停止します。コメントの終わりの前にある任意の数の x に一致するには、このケースを処理するために x+ を追加する必要があります。したがって、最終コメントと一致する /x([^x]|x+[^/x])*x+/ を取得します。
mysql> set @r:='/\\*([^*]|\\*+[^/*])*\\*+/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @s:='/** some comment here **// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+-------------------------+------+---------------------------+
| @r | c | s |
+-------------------------+------+---------------------------+
| /\*([^*]|\*+[^/*])*\*+/ | 1 | /** some comment here **/ |
+-------------------------+------+---------------------------+
1 row in set (0.00 sec)
mysql> set @s:='/*/ some comment here /*// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+-------------------------+------+---------------------------+
| @r | c | s |
+-------------------------+------+---------------------------+
| /\*([^*]|\*+[^/*])*\*+/ | 1 | /*/ some comment here /*/ |
+-------------------------+------+---------------------------+
1 row in set (0.00 sec)
式の効率を向上させるには、この式のループを削除する必要があります。次の表は、より「ループを削除する」式を示しています
。
要素 |
目的 |
正規表現 |
オープニング |
コメント開始 |
/バツ |
普通* |
1 つ以上の「x」を含むコメント テキスト |
[^x]*x+ |
特別 |
終了境界文字に属さない文字 |
[^/x] |
閉鎖 |
末尾のスラッシュ |
/ |
表3
和子域名的例子一样,normal* 必须匹配至少一个字符。本例中必须的结束分隔符包含两个字符。任何以结束分隔符的第一个字符结尾的任何 normal 序列,只有在紧跟字符不能组成结束分隔符的情况下,才会把控制权交给 special 部分。所以按照通用的消除套路得到:
/x[^x]*x+([^/x][^x]*x+)*/
把每个 x 替换为 \*(字符组中的 x 替换为 *),得到实际的表达式。
mysql> set @r:='/\\*[^*]*\\*+([^/*][^*]*\\*+)*/';
Query OK, 0 rows affected (0.00 sec)
mysql> set @s:='/** some comment here **// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+------------------------------+------+---------------------------+
| @r | c | s |
+------------------------------+------+---------------------------+
| /\*[^*]*\*+([^/*][^*]*\*+)*/ | 1 | /** some comment here **/ |
+------------------------------+------+---------------------------+
1 row in set (0.00 sec)
mysql> set @s:='/*/ some comment here /*// foo*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+------------------------------+------+---------------------------+
| @r | c | s |
+------------------------------+------+---------------------------+
| /\*[^*]*\*+([^/*][^*]*\*+)*/ | 1 | /*/ some comment here /*/ |
+------------------------------+------+---------------------------+
1 row in set (0.00 sec)
实际情况中,注释通常会包含多行,这个表达式也能应付。
mysql> set @s:=
-> '/*/ some comment here / foo
'> * some comment here * foo*
'> /**/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s\G
*************************** 1. row ***************************
@r: /\*[^*]*\*+([^/*][^*]*\*+)*/
c: 1
s: /*/ some comment here / foo
* some comment here * foo*
/**/
1 row in set (0.00 sec)
这个正则表达式在实际中会遇到许多问题,它能识别 C 的注释,但不能识别 C 语法的其他重要方面。例如,下面的 /*...*/ 部分尽管不是注释,也能匹配。
mysql> set @s:='const char *cstart = "/*", *cend = "*/"';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+------------------------------+------+------------------+
| @r | c | s |
+------------------------------+------+------------------+
| /\*[^*]*\*+([^/*][^*]*\*+)*/ | 1 | /*", *cend = "*/ |
+------------------------------+------+------------------+
1 row in set (0.00 sec)
下节接着讨论这个例子。
10. 流畅运转的正则表达式
正则表达式 /\*[^*]*\*+([^/*][^*]*\*+)*/ 存在错误匹配问题,比如下面这行 C 代码:
char *CommentStart = "/*"; /* start of comment */
匹配的结果是:/*"; /* start of comment */,然而希望的匹配结果应该是:/* start of comment */。
mysql> set @s:='char *CommentStart = "/*"; /* start of comment */';
Query OK, 0 rows affected (0.00 sec)
mysql> set @r:='/\\*[^*]*\\*+([^/*][^*]*\\*+)*/';
Query OK, 0 rows affected (0.00 sec)
mysql> select @r, regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s;
+------------------------------+------+-----------------------------+
| @r | c | s |
+------------------------------+------+-----------------------------+
| /\*[^*]*\*+([^/*][^*]*\*+)*/ | 1 | /*"; /* start of comment */ |
+------------------------------+------+-----------------------------+
1 row in set (0.00 sec)
问题在于遇到双引号时该如何匹配。类似的情况还有单引号的 C 常量,双斜线方式的单行注释等。可以为每种情况定义一个分支进行匹配:
- 非单引号、双引号、斜杠符串:[^"'/]
- 双引号字符串:"[^\\"]*(?:\\.[^\\"]*)*"
- 单引号字符串:'[^'\\]*(?:\\.[^'\\]*)*'
- 单行或多行注释:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/
- 单行注释://[^\n]*
これら 5 つの別々の式を | で複数選択分岐として連続して連結しても、それらの間に重複がないため、まったく問題ありません。従来の NFA は一致が見つかるとすぐに停止するため、最も一般的に使用される複数選択ブランチ [^"'/] が最初に配置されます。 | で連結された正規表現を左から右にスキャンすると、次のことがわかります。文字列に適用する場合、一連の試行では次の可能性があります。
- 単一の非一重引用符、二重引用符、またはスラッシュ文字と一致します
- 二重引用符で囲まれた文字列をその末尾まで一度に直接照合します。
- 一重引用符で囲まれた文字列の末尾に直接一致します。
- コメントの最後まで直接複数行のコメントを一度に照合します。
- 単一行のコメント部分をコメントの最後まで一度に一致させます。
このようにして、正規表現は単一引用符または二重引用符で囲まれた文字列やコメント内から試行することはありません。これが成功の鍵です。バックスラッシュと一重引用符のエスケープに注意して、MySQL 変数を使用して 5 分岐の正規表現を表します。
set @other:='[^"\'/]';
set @double:='"[^\\\\"]*(?:\\\\.[^\\\\"]*)*"';
set @single:='\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
set @comment1:='/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/';
set @comment2:='//[^\\n]*';
5 つの別々のブランチからの式を連結します。
set @r:=concat('(',@other,'+','|',@double,@other,'*','|',@single,@other,'*',')','|',@comment1,'|',@comment2);
ここで注意すべき点は次の 3 つです。
- 同じ行にある任意の数の @other 文字を 1 つの単位にグループ化できるため、@other+ が使用されます。後から強制的に後戻りする要素がないので、延々とマッチングする心配はありません。
- 引用符付き文字列の後、他の引用符付き文字列およびコメントの前に、@other と一致する可能性があります。各引用符付き文字列の後に @other* を追加して、すぐに次のステップに進むのではなく @other と一致するようにエンジンに指示します。1 サイクル。
これはループ除去手法に似ており、正規表現エンジンのマッチングを支配するため速度が向上します。ここでは、グローバル マッチングに関する知識を使用してローカル最適化を実行し、高速動作に必要な条件をエンジンに提供します。
引用符で囲まれた文字列と一致する各部分式の後の @other に使用される量指定子がアスタリスクであることが非常に重要であり、複数選択構造の先頭の @other はプラス記号の量指定子とともに使用する必要があります。@other の前にアスタリスク量指定子がある場合は、どのような状況でも一致します。引用符で囲まれた文字列の後の @other でプラス記号量指定子が使用されている場合、2 つの引用符で囲まれた文字列が接続されている場合にエラーが発生します。
- コメントを除くすべてのブランチをキャプチャ グループに入れます。このようにして、非コメントブランチが一致した場合、$1 は対応するコンテンツを保存します。コメント分岐が一致した場合、$1 は空になります。その結果、regexp_replace関数でコメント部分を$1に置き換えることで削除できるようになりました。
最後に正規表現 @r を取得します。
mysql> select @r;
+-------------------------------------------------------------------------------------------------------------------+
| @r |
+-------------------------------------------------------------------------------------------------------------------+
| ([^"'/]+|"[^\\"]*(?:\\.[^\\"]*)*"[^"'/]*|'[^'\\]*(?:\\.[^'\\]*)*'[^"'/]*)|/\*[^*]*\*+(?:[^/*][^*]*\*+)*/|//[^\n]* |
+-------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
コメントを照合して削除した結果は次のとおりです。
mysql> set @s:=
-> 'char *CommentStart = "/*"; /* start of comment */
'> char *CommentEnd = "*/"; // end of comment';
Query OK, 0 rows affected (0.00 sec)
mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s\G
*************************** 1. row ***************************
c: 6
s: char *CommentStart = ,"/*"; ,/* start of comment */,
char *CommentEnd = ,"*/"; ,// end of comment
1 row in set (0.00 sec)
mysql> select @s, regexp_replace(@s, @r, '$1', 1, 0) c\G
*************************** 1. row ***************************
@s: char *CommentStart = "/*"; /* start of comment */
char *CommentEnd = "*/"; // end of comment
c: char *CommentStart = "/*";
char *CommentEnd = "*/";
1 row in set (0.00 sec)
先頭の @other+ は 2 つの状況でのみ照合できます: 1) 照合されたテキストがターゲット文字列全体の先頭にあり、現時点では引用文字列が照合されていない、2) コメントの後。コメントの後に @other+ を追加することを考えるかもしれません。これは素晴らしいことですが、ここでは、最初の括弧のペア内の式を保持したいすべてのテキストと一致させる必要がある点を除きます。
では、@other+ がコメントの後にある場合でも、先頭に @other+ を置く必要があるのでしょうか? これはアプリケーション データによって異なります。引用符で囲まれた文字列よりもコメントの方が多い場合は、それを最初に配置し、そうでない場合は後から配置するのが合理的です。