改进正则表达式的性能

正则表达式的应用原理

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

  • 编译正则表达式。检查正则表达式的正确性,如果正确,将其编译为内部形式。
  • 开始传动。将正则引擎定位到目标字符串的起始位置。
  • 检测元素。
  1. 引擎开始依次测试表达式的各个元素和目标字符串。
  2. 在检测相连元素时,引擎会在某个元素匹配失败时停止。
  3. 量词修饰的元素,控制权在量词和被限定的元素之间轮换。

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

  • 寻找匹配结果。若找到匹配结果,传统型NFA立即报告匹配成功(POSIX NFA则在尝试完所有可能的情况之后,返回最长的匹配)。若没有找到匹配,从当前传动位置前进一位,开始下一轮尝试。
  • 匹配彻底失败。在所有可能的尝试都失败之后,报告失败。

全面考察回溯

下面我们通过几个例子来考察回溯在匹配成功和不成功时的各种细节。如表达式".*"对字符串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在正则表达式应用之前,完成对正则表达式的编译。尤其是循环之前编译正则表达式,可以有效减少构建表达式内部形式的次数。这种方式,被称为“面向对象式处理中的编译缓存” 。此外还有“集成式处理中的编译缓存” 和“程序式处理中的编译缓存”,此处就不做介绍了。

通过传动装置进行优化

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

优化正则表达式本身

  • 消除不必要的括号。对于不需要捕获的分组,用(?: expression)代替(expression)
  • 消除不必要的字符组。没必要对单个字符应用字符组,特别是[.],完全可以用\.来代替。
  • 避免指数级的回溯。对于(.+)*之类的量词结合结构,+*二者分隔目标字符串为任意长度的子字符串,制造出指数级的回溯。
  • 使用占有优先量词/固化分组削减状态。在确定量词限定的元素与其之后的元素不会匹配重叠的情况下,可以使用占有优先量词,或者固化分组减少存储的备用状态。当然这种做法必须建立在引擎支持占有优先量词,或者固化分组的基础上。
  • 用字符组代替多选结构。[uvwxyz]代替u|v|w|x|y|z,因为前者只进行匹配操作,而后者可能需要在目标字符串的每个位置进行6次回溯。由这个例子可以看出多选结构或许是造成回溯的主要原因。
  • 控制多选结构的顺序。将最可能匹配的分支放在前面,减少回溯。
  • 将多选结构中开头相同的字符提取到多选结构之前。this|that就可以改成th(?:is|at)。这样th只需检查一遍,只有在确实需要的时候才会用到代价相对高昂的多选结构功能。
  • 按实际情况选择忽略优先量词和匹配优先量词。忽略优先量词通常要比匹配优先量词要慢(这不是言之确凿的)。另外对大多数引擎而言,排除型字符组的效率要比忽略优先量词快的多。如"[^&quot;]*"".*?"
  • 避免过于复杂的正则表达式。

消除循环

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

opening normal* (special normal*)* closing

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

  • special部分和normal部分匹配的开头不能重合;
  • normal部分必须能匹配至少一个字符;
  • special部分是固化的。当存在多种方式匹配同样的文本时,(special normal*)*最终也难免沦为(expression*)*,也就无从避免无休止匹配了。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容