正则表达式太慢了吗?现实生活中的例子,简单的非正则表达式替代方案更好

时间:2010-04-19 11:41:25

标签: java regex performance algorithm string

我看到这里的人发表评论,比如“正则表达式太慢了!”,或者“为什么你会用正则表达式做一些简单的事情!” (然后提出10+行代替)等等。

我还没有真正在工业环境中使用正则表达式,所以我很好奇是否有正则表达式显然太慢的应用程序, AND 其中简单非-regex替代存在,表现得更好(甚至可能渐近!)。

显然,许多具有复杂字符串算法的高度专业化的字符串操作将轻松胜过正则表达式,但我所说的是存在简单解决方案且显着优于正则表达式的情况。

当然,简单的主观是主观的,但我认为合理的标准是如果它只使用StringStringBuilder等,那么它可能很简单。


注意:我非常感谢能够证明以下内容的答案:

  1. 初学者级正则表达式解决方案,解决非玩具现实生活中可执行的问题
  2. 简单的非正则表达式解决方案
  3. 执行比较的专家级正则表达式重写

5 个答案:

答案 0 :(得分:29)

我记得一本教科书的例子。请注意,建议不要使用以下方法进行生产!改为使用正确的CSV解析。

这个例子中的错误很常见:使用一个较窄的字符类更适合的点。

在一个CSV文件中,每行包含12个以逗号分隔的整数,找到第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微秒。)

答案 1 :(得分:11)

我尝试了各种构造的性能,不幸的是我发现Java正则表达式没有执行我认为非常可行的优化。

Java正则表达式使O(N)"(?s)^.*+$"

匹配

这非常令人失望。 ".*"采用O(N),但以锚点(^$)和单行模式{{1}的形式优化“提示”是可以理解的,即使重复占有(即没有回溯),正则表达式引擎仍然无法看到这将匹配每个字符串,仍然必须匹配Pattern.DOTALL/(?s)

当然,这种模式不是很有用,但请考虑下一个问题。

Java正则表达式使O(N)O(N)

匹配

同样,我希望正则表达式引擎可以看到由于锚点和单行模式,这基本上与"(?s)^A.*Z$"非正则表达式相同:

O(1)

不幸的是,不,这仍然是 s.startsWith("A") && s.endsWith("Z") 。非常失望。但是,不是很有说服力,因为存在一个简单的非正则表达式。

Java正则表达式使O(N)O(N)

匹配

此模式匹配以3个小写元音结尾的字符串。没有好的和简单的非正则表达式替代方案,但您仍然可以在"(?s)^.*[aeiou]{3}$"中编写与此匹配的非正则表达式,因为只需要检查最后3个字符(为简单起见) ,我们可以假设字符串长度至少为3)。

我还尝试O(1),试图告诉正则表达式引擎忽略其他所有内容,只检查最后3个字符,但当然这仍然是"(?s)^.*$(?<=[aeiou]{3})"(后面是上面第一节)。

但是,在这种特殊情况下,通过将正则表达式与O(N)组合,可以使正则表达式变得有用。也就是说,您可以手动限制模式以尝试仅匹配最后3个字符substring,而不是查看整个字符串是否与模式匹配。一般来说,如果您事先知道模式具有有限长度的最大匹配,则可以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); } } 之后开始真正变慢(即字符串长度为1024)。但是,如果您取消注释10行,则整个测试将立即完成(这也证实问题不是因为我没有使用substring,这将导致最好的持续改进,但是因为patttern需要Pattern.compile来匹配,这在O(N)的渐近增长呈指数时是有问题的。


结论

似乎Java正则表达式几乎没有根据模式进行优化。特别是后缀匹配特别昂贵,因为正则表达式仍然需要遍历字符串的整个长度。

值得庆幸的是,使用N对切断的后缀执行正则表达式(如果知道匹配的最大长度)仍然可以允许您在时间上使用正则表达式进行后缀匹配,而与输入字符串的长度无关。

// update:实际上我刚才意识到这也适用于前缀匹配。 Java正则表达式匹配substring 中的O(1)长度前缀模式。也就是说,O(N)检查字符串是否以"(?s)^[aeiou]{3}.*$"中的3个小写字母开头,并且应该可以优化O(N)

我认为前缀匹配对正则表达式更友好,但我认为不可能提出O(1)运行时模式来匹配上述内容(除非有人可以证明我错了)。

显然你可以做O(1)“技巧”,但模式本身仍然是s.substring(0, 3).matches("(?s)^[aeiou]{3}.*$");您只需使用O(N)手动将N缩减为常量。

因此,对于真正长字符串的任何类型的有限长度前缀/后缀匹配,您应该在使用正则表达式之前使用substring进行预处理;否则substring O(N)就足够了。

答案 2 :(得分:5)

  

正则表达式太慢了吗?

正则表达式本质上很慢。基本模式匹配是O(n),难以改进,当然对于非平凡模式。

答案 3 :(得分:2)

在我的测试中,我发现了以下内容:

使用java的String.split方法(使用正则表达式)在1,000,000次迭代下花了2176ms。 使用这种自定义拆分方法在1,000,000次迭代下耗时43ms。

当然,它只有在你的“正则表达式”完全是字面意义的情况下才有效,但在这种情况下, 它会快得多。

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秒。所以,这取决于你的需求,但我不会过分担心将所有使用正则表达式的代码移植到非正则表达式版本,事实上,如果模式非常复杂,那么这可能是必要的。像这样分开是行不通的。但是,如果你正在分裂,比如逗号,这种方法会表现得更好,虽然“好得多”在这里是主观的。

答案 4 :(得分:1)

嗯,并非总是但有时很慢,取决于模式和实现。

一个简单的示例,比正常替换慢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
>>>