改进正则表达式的性能

2017-07-19  本文已影响0人  戴小白

正则表达式的应用原理

正则表达式应用到目标字符串的过程大致分为下面几步:

  1. 引擎开始依次测试表达式的各个元素和目标字符串。
  2. 在检测相连元素时,引擎会在某个元素匹配失败时停止。
  3. 量词修饰的元素,控制权在量词和被限定的元素之间轮换。

控制权在捕获型括号内外切换会带来额外的开销。捕获型括号内表达式匹配的文本必须保留,才能通过$1\1来引用。括号也可能属于某个回溯分支,括号内的状态就是用于回溯的状态的一部分,所以进入和退出捕获型括号时需要修改状态。

全面考察回溯

下面我们通过几个例子来考察回溯在匹配成功和不成功时的各种细节。如表达式".*"对字符串The name "McDonald's" is said "makudonarudo" in Japanese的匹配过程如下:

<small>图1</small>
我们知道".*"!无法匹配上述字符串,但引擎在报告匹配失败之前仍会进行多次尝试:
<small>图2</small>
如果我们将.换成[^&quot;][^&quot;]*匹配的内容就能不包括双引号,这减少了匹配和回溯的次数:
<small>图3</small>
但是需要注意的是"[^&quot;]*"".*"在此例中的匹配结果并不一样。

一个简单的例子

假设字符串"2 \"x3\" likeness",为了匹配双引号及之内的字符串,且允许出现转义的双引号。"(\\\\.|[^&#92;&#92;&quot;])*"的匹配结果虽然正确,但在效率方面有所欠缺,通过优化能加快匹配速度。

调整多选结构的顺序

对于一般的双引号字符串而言,普通字符的数量要比转义字符多,将[^&#92;&#92;&quot;]放到\\\\.之前可以有效减少回溯次数。

<small>图4</small>
调整分支的顺序必须要保证排序与匹配成功无关。同时,这种改动并不能加快报告失败的速度,因为在报告匹配失败之前,所有可能的匹配都已经被尝试。
目标字符串 "(\\.|[^\\"])*" "([^\\"]|\\.)*"
"2"x3" likeness" 32次测试,14次回溯 22次测试,4次回溯
"... 99 more chars ..." 218次测试,109次回溯 111次测试,2次回溯
"no "match" here 124次测试,86次回溯 124次测试,86次回溯

消除循环

由于*控制着捕获型括号内的多选结构,每次进出括号都意味着状态的切换,为避免这部分的消耗,可以通过消除循环的技巧对表达式进行改进。这项技巧我们将在下文讲到,这里给出当前例子在消除循环之后的表达式是"[^&#92;&#92;&quot;]+(\\\\.[^&#92;&#92;&quot;]+)*"

错误的优化

为了减少*的迭代次数,在[^&#92;&#92;&quot;]后引入+。对于不存在转义字符的字符串而言,这样会一次性读入整个字符串,而不用进行回溯。这改动似乎带来了不错的收益,在匹配成功的时候也确是如此。但在匹配失败时,却会造成指数级的回溯。例如目标字符串是makudonarudo+会对字符串做任意长度的切割,*再在切割的基础上进行多次迭代。长度为n的字符串,回溯的次数是$2^{n+1}$,独立测试的次数为$2^{n+1}+2^n$。如"2\"x3\" likeness and makudonarudo这种长度的目标字符串时就会造成应用程序的未响应。

常见的优化措施

对于正则引擎,各流派有自己的实现和优化措施。实现方案互有差异,优化措施也不尽相同,但通常可以归纳为两类:

对某个正则表达式的改动,在某个流派的实现方式中可能带来收益,而在另一个实现方式中却与期望背道而驰。在进行优化时,检测并性能测试实际期望应用的同类型数据,总是有助于判断改动是否有益。

应用正则表达式之前的优化措施

用户通过Pattern.compile在正则表达式应用之前,完成对正则表达式的编译。尤其是循环之前编译正则表达式,可以有效减少构建表达式内部形式的次数。这种方式,被称为“面向对象式处理中的编译缓存” 。此外还有“集成式处理中的编译缓存” 和“程序式处理中的编译缓存”,此处就不做介绍了。

通过传动装置进行优化

行锚点优化。能使用这种优化措施的引擎知道,在锚定位置才能满足表达式的匹配条件,传动装置会直接略过目标字符串中的其它位置的字符。

优化正则表达式本身

消除循环

所谓“循环”,指得是(this|that|...)*这类表达式中*代表的意义。消除循环这项技巧对于某些常用的表达式来说,加速效果非常明显。消除循环常用的解法是:

opening normal* (special normal*)* closing

这种解法中最重要的是要区分表达式中的specialnormal部分。一般而言,对于目标字符串中最常见的部分用normal子表达式处理,用special子表达式作为检查点处理其余不常见的部分。为避免(special normal*)*中的无休止匹配(即指数型回溯),需要确保以下三点:

例1:<B>(?>[^&lt;]*)(?>(?!</?B>)<[^&lt;]*)*</B>代替<B>((?!</?B>).)*</B>,这里的固化分组不是必须的,但如果只能部分匹配,使用固化分组可以提高速度。
例2:/\*[^&#42;]*\*+([^&#47;&#42;][^&#42;]*\*+)*/匹配java文件中的多行注释。
回过头来看消除循环这项技巧,它对表达式的可读性和可维护性造成了一定影响,但是测试证明其带来的速度收益也同样十分明显。

上一篇 下一篇

猜你喜欢

热点阅读