web-dev-qa-db-ja.com

正規表現が遅すぎませんか?単純な非正規表現の代替が優れている実際の例

ここの人々が「正規表現が遅すぎる!」や「なぜ正規表現を使ってこんなに簡単なことをするのか!」などのコメントをしているのを見たことがあります。 (そして、代わりに10行以上の代替案を提示します)など。

私は実際に産業環境で正規表現を使用したことがないので、正規表現が明らかに遅すぎるアプリケーションがあるかどうか知りたいです[〜#〜] and [〜#〜] where asimple非正規表現の代替手段が存在し、パフォーマンスが大幅に向上します(おそらく漸近的にも!)。

明らかに、高度な文字列アルゴリズムを使用した高度に専門化された文字列操作の多くは、正規表現を簡単に上回りますが、単純な解決策が存在し、大幅に正規表現を上回っている場合について話しています。 。

もちろん、単純と見なされるのは主観的ですが、StringStringBuilderなどのみを使用する場合、おそらく単純であるというのが妥当な基準だと思います。


:次のことを示す回答をいただければ幸いです。

  1. ひどく実行されるおもちゃ以外の現実の問題に対する初心者レベルの正規表現ソリューション
  2. 単純な非正規表現ソリューション
  3. 同等のパフォーマンスを発揮するエキスパートレベルの正規表現の書き換え
26

正規表現が悪くなった教科書の例を覚えています。 本番環境での使用には次のアプローチは推奨されません!代わりに適切なCSVパーサーを使用してください。

この例での間違いは非常に一般的です。狭い文字クラスが適している場所でドットを使用します。

各行にコンマで区切られた正確に12個の整数を含むCSVファイルで、6番目の位置に13がある行を見つけます(13がどこにあっても)。

_1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10,11,12 -- don't match
42,12,13,12,32,13,14,43,56,31,78,10 -- match
42,12,13,12,32,14,13,43,56,31,78,10 -- don't match
_

正確に11個のコンマを含む正規表現を使用します。

_".*,.*,.*,.*,.*,13,.*,.*,.*,.*,.*,.*"
_

このように、各「。*」は単一の番号に制限されます。この正規表現はタスクを解決しますが、パフォーマンスが非常に悪くなります。 (私のコンピューターでは、文字列あたり約600マイクロ秒で、一致する文字列と一致しない文字列の違いはほとんどありません。)

単純な非正規表現の解決策は、各行をsplit()して、6番目の要素を比較することです。 (はるかに高速:文字列あたり9マイクロ秒。)

正規表現が非常に遅い理由は、「*」数量詞がデフォルトで貪欲であるため、最初の「。*」が文字列全体に一致しようとし、その後、文字ごとにバックトラックを開始します。ランタイムは、行の数の数で指数関数的です。

したがって、貪欲な数量詞を消極的な数量詞に置き換えます。

_".*?,.*?,.*?,.*?,.*?,13,.*?,.*?,.*?,.*?,.*?,.*?"
_

これは、一致した文字列の場合はパフォーマンスが大幅に向上しますが(100倍)、一致しない文字列の場合のパフォーマンスはほとんど変わりません。

パフォーマンスの正規表現は、ドットを文字クラス "[^、]"に置き換えます。

_"[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,13,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*"
_

(これには、コンピューター上の一致する文字列の場合は文字列ごとに3.7マイクロ秒、一致しない文字列の場合は2.4マイクロ秒が必要です。)

29

さまざまな構成のパフォーマンスを少し実験しましたが、残念ながら、Java正規表現は非常に実行可能な最適化を実行しないことがわかりました。

Java正規表現は、O(N)と一致するために"(?s)^.*+$"を取ります

これは非常に残念です。 _".*"_がO(N)を取ることは理解できますが、アンカー(_^_および_$_)および単一行モードPattern.DOTALL/(?s)、繰り返しを所有格(つまりバックトラックなし)にしても、正規表現エンジンはこれがすべての文字列に一致することを認識できず、O(N)で一致する必要があります。

もちろん、このパターンはあまり役に立ちませんが、次の問題を検討してください。

Java正規表現は、O(N)と一致するために"(?s)^A.*Z$"を取ります

繰り返しになりますが、正規表現エンジンが、アンカーと単一行モードのおかげで、これがO(1)non-regexと本質的に同じであることを確認できることを期待していました。

_ s.startsWith("A") && s.endsWith("Z")
_

残念ながら、いいえ、これはまだO(N)です。期待はずれの。それでも、素晴らしく単純な非正規表現の代替手段が存在するため、あまり説得力がありません。

Java正規表現は、O(N)と一致するために"(?s)^.*[aeiou]{3}$"を取ります

このパターンは、3つの小文字の母音で終わる文字列に一致します。素晴らしく単純な非正規表現の代替手段はありませんが、最後の3文字をチェックするだけでよいので、これに一致する非正規表現をO(1)に書き込むことができます(簡単にするために、文字列の長さは少なくとも3であると想定できます)。

また、正規表現エンジンに他のすべてを無視し、最後の3文字をチェックするように指示するために、"(?s)^.*$(?<=[aeiou]{3})"も試しましたが、もちろんこれはO(N)です(上記の最初のセクション)。

ただし、この特定のシナリオでは、正規表現をsubstringと組み合わせることで便利にできます。つまり、文字列全体がパターンに一致するかどうかを確認する代わりに、パターンを手動で制限して、最後の3文字substringのみに一致するようにすることができます。一般に、パターンの最大一致が有限であることが事前にわかっている場合は、非常に長い文字列の末尾から必要な数の文字をsubstringして、その部分だけを正規表現できます。


テストハーネス

_static void testAnchors() {
    String pattern = "(?s)^.*[aeiou]{3}$";
    for (int N = 1; N < 20; N++) {
        String needle = stringLength(1 << N) + "ooo";
        System.out.println(N);
        boolean b = true;
        for (int REPS = 10000; REPS --> 0; ) {
            b &= 
              needle
              //.substring(needle.length() - 3) // try with this
              .matches(pattern);
        }
        System.out.println(b);
    }
}
_

このテストの文字列の長さは指数関数的に増加します。このテストを実行すると、_10_(つまり、文字列の長さ1024)の後で実際に速度が低下し始めることがわかります。ただし、substring行のコメントを外すと、テスト全体がすぐに完了します(これにより、_Pattern.compile_を使用しなかったために問題が発生したわけではないことも確認されます。これにより、最良ですが、パターンが一致するためにO(N)を必要とするためです。これは、Nの漸近的成長が指数関数的である場合に問題になります)。


結論

Java regexは、パターンに基づいて最適化をほとんどまたはまったく行わないようです。特に、正規表現は文字列の全長を通過する必要があるため、サフィックスのマッチングには特にコストがかかります。

ありがたいことに、substringを使用して切り刻まれたサフィックスに対して正規表現を実行すると(一致の最大長がわかっている場合)、入力文字列の長さに関係なく、時間内にサフィックスの一致に正規表現を使用できます。

// update:実際、これはプレフィックスマッチングにも当てはまることに気づきました。 Java正規表現は、O(1)O(N)長さのプレフィックスパターンと一致します。つまり、"(?s)^[aeiou]{3}.*$"は、文字列がO(N)に最適化可能である必要があるときに、O(1)の3つの小文字で始まるかどうかをチェックします。

プレフィックスマッチングの方が正規表現に適していると思いましたが、上記に一致するO(1)ランタイムパターンを考え出すことはできないと思います(誰かが私を間違っていると証明できない限り)。

明らかに、s.substring(0, 3).matches("(?s)^[aeiou]{3}.*$") "trick"を実行できますが、パターン自体はO(N)のままです。 Nを使用して、手動でsubstringを定数に減らしました。

したがって、非常に長い文字列のあらゆる種類の有限長のプレフィックス/サフィックスマッチングでは、正規表現を使用する前にsubstringを使用して前処理する必要があります。それ以外の場合は、O(N)です。O(1)で十分です。

11

正規表現が遅すぎませんか?

正規表現はnot本質的に遅いです。基本的なパターンマッチングはO(n)であり、重要なパターンの場合、改善するのは困難です。

5
Henk Holterman

私のテストでは、次のことがわかりました。

JavaのString.splitメソッド(正規表現を使用)を使用すると、1,000,000回の反復で2176ミリ秒かかりました。このカスタム分割方法を使用すると、1,000,000回の反復で43ミリ秒かかりました。

もちろん、「正規表現」が完全にリテラルである場合にのみ機能しますが、その場合ははるかに高速になります。

List<String> array = new ArrayList<String>();
String split = "ab";
String string = "aaabaaabaa";
int sp = 0;
for(int i = 0; i < string.length() - split.length(); i++){              
    if(string.substring(i, i + split.length()).equals(split)){
        //Split point found
        array.add(string.substring(sp, i));
        sp = i + split.length();
        i += split.length();
    }
}
if(sp != 0){
    array.add(string.substring(sp, string.length()));
}
return array;

それで、あなたの質問に答えるために、それは理論的に速いですか?はい、絶対に、私のアルゴリズムはO(n)です。ここで、nは分割する文字列の長さです。 (正規表現がどうなるかはわかりません)。それは実質的に速いですか?ええと、100万回以上の反復で、基本的に2秒節約できました。したがって、私が推測するニーズによって異なりますが、正規表現を使用するすべてのコードを非正規表現バージョンにバックポートすることについてはあまり心配しません。実際、パターンが非常に複雑な場合は、文字通り、とにかく必要になる可能性があります。このように分割しても機能しません。ただし、たとえばコンマで分割する場合は、この方法の方がはるかに優れていますが、ここでは「はるかに優れている」という主観があります。

2
LadyCailin

まあ、常にではありませんが、時々遅いですが、パターンと実装に依存します。

簡単な例です。通常の交換より2倍遅いですが、それほど遅いとは思いません。

>>> import time,re
>>>
>>> x="abbbcdexfbeczexczczkef111anncdehbzzdezf" * 500000
>>>
>>> start=time.time()
>>> y=x.replace("bc","TEST")
>>> print time.time()-start,"s"
0.350999832153 s
>>>
>>> start=time.time()
>>> y=re.sub("bc","TEST",x)
>>> print time.time()-start,"s"
0.751000165939 s
>>>
1
YOU