一起学习正则表达式(七)回溯陷阱

思维导图

转载请注明出处:https://www.jianshu.com/p/40f8988ba339

本文出自 容华谢后的博客

往期回顾:

《一起学习正则表达式(一)那些让人头晕的元字符》

《一起学习正则表达式(二)量词与贪婪》

《一起学习正则表达式(三)分组与引用》

《一起学习正则表达式(四)常见的4种匹配模式》

《一起学习正则表达式(五)断言匹配》

《一起学习正则表达式(六)正则匹配原理》

《一起学习正则表达式(七)回溯陷阱》

0.写在前面

在上一篇文章中,我们学习了正则表达式的匹配原理,在我们常用的开发语言中,大多数都是采用的传统型 NFA 引擎,也就是非确定性有穷自动机。

NFA 引擎以正则为主导,就是拿着正则看文本,会发生回溯,文本的同一部分,有可能会被反复测试很多次,在最坏的情况下,它的执行速度可能会非常慢,那什么是最坏的情况呢,一起来看一看。

1.回溯

在第二篇文章量词与贪婪中,我们了解到,表示次数的量词默认是贪婪的,在贪婪模式下,会尽可能最大长度的去匹配目标文本。

举个栗子,我们现在的需求是找到以字母 dy 结尾的单词或句子,我们可以很快的写出正则表达式:

目标文本:Toady is such a beautify day.

正则表达式:.+dy

看下结果:

贪婪匹配

可以看到匹配了31次才找到我们想要的内容,这31次都做了哪些工作,来看下:

贪婪匹配过程

正则中的 .+ 在一开始就匹配到了文本的全部内容,然后拿着正则看文本,正则中还剩下 dy 没有匹配,去文本中查找,发现已经没有字符可以匹配了,于是向前回溯,吐出句号,用正则再次对比,发现还没有匹配上,于是继续向前回溯,吐出字母 a,再次对比,还没有匹配上,就这样反反复复的向前回溯,终于找到了 Toady 这个单词,匹配结束。

这样写出的正则表达式性能肯定是有问题的,如果目标文本再长一点,再加上几个选择分支,回溯步骤直接就要上天,于是我们利用学过的知识,简单修改下:

目标文本:Toady is such a beautify day.

正则表达式:.+?dy

在量词后面加上一个问号,就代表当前量词的匹配行为是非贪婪模式,非贪婪模式虽然也会发生回溯,但是不会一上来就把目标文本一口气都吃掉,从而节省了很多匹配步骤,先再看结果:

非贪婪匹配

可以看到只用了6次就成功的匹配到了,再看下匹配的过程:

非贪婪匹配过程

2.Lazada 卖家中心案例

这是一个真实的案例,来自阿里技术微信公众号上的发文,Lazada 卖家中心店铺名检验规则比较复杂,名称中可以出现下面这些组合:

  • 英文字母大小写

  • 数字

  • 越南文

  • 一些特殊字符,如 “&”、“-”、“_” 等

负责开发的同学随手就写出了下面的正则表达式:


^([A-Za-z0-9._()&'\- ]|[aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ])+$

看起来很长,但其实就是两个选择分支,我们来简化下:

^([条件分支1]|[条件分支2])+$

在内部测试的时候,没有发现什么问题,然后就上线运行了,在运营的过程中,经常会遇到服务器CPU狂飙到100%的情况,于是就找啊找,终于找到竟然是这样一个正则引起的问题,一个小小的正则竟然可以让服务器直接崩溃,这是为什么呢,一起来看下:

我们用一段文本来测试下:

Lazada 正则校验

示例:https://regex101.com/r/dTzUyx/1

可以看到,一个很短的字符串,NFA 引擎竟然尝试了1万多次,由于是贪婪匹配,第一个分支可以匹配上 Talk is Cheap 这部分,接着后面的逗号匹配失败,使用第二个分支继续匹配,还是失败,此时贪婪匹配的过程就结束了。

接着 NFA 引擎用后面的 $ 来进行匹配,但此处不是文本结尾,匹配不上,开始回溯,吐出第一个分支匹配上的最后一个字母 p,用第二个分支匹配,第二个分支匹配上了字母 p,但是还是匹配不上逗号。

Lazada 匹配过程

继续回溯,吐出第二个分支匹配上的字母 p,然后再继续吐出第一个分支匹配上的字母 a,再用第二个分支匹配字母 a 和 p,最后还是没有匹配上,继续吐出第二个分支匹配上的字母 p,用第一个分支再次匹配,由此进入了死亡循环,如果测试的文本增加一位,那么整体的匹配步骤就会直接翻倍。

那么该如何解决这个问题呢,也很简单,直接使用独占模式就可以,在量词加号的后面,再加上一个 + 号,就变成了独占模式,独占模式不会发生回溯,匹配失败就直接返回失败了,不会引发性能问题:

^([A-Za-z0-9._()&'\- ]|[aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ])++$
Lazada 独占模式

还可以去掉两个分支中的重复条件 A-Za-z:

^([0-9._()&'\- ]|[aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ])+$
Lazada 去除重复条件

这样修改之后,只用了57次就匹配完成了,所以切记,如果你写的正则表达式中有多个分支条件,里面的条件千万不要重复。

3.避坑指南

在见识了正则回溯的威力之后,发现如果对正则不了解,还是很容易写出存在性能问题的正则表达式的,下面我们来总结下那些我们在平时开发中容易踩到的坑:

3.0 避免不同分支重复匹配

在上文中,我们已经讲到了,如果正则中存在多个条件分支,其中各个分支中的条件千万不要重复。

3.1 提前编译好正则

编程语言中一般都自带“编译”方法,我们可以在使用正则之前提前编译好,这样就不用每次使用的时候去反复构造状态机,从而提升正则匹配的性能:

import re

# 先编译好,再使用
reg = re.compile(r'ab?c')  
reg.findall('abc')

# 直接使用
re.findall(r'ab?c', 'abc')

3.2 提取出公共部分

比如我们匹配 http 或 https,有的同学会写成 http|https,可以直接优化成 https?

3.3 出现可能性大的放左边

由于正则是从左到右匹配的,把出现概率大的放左边,域名中 .com 的使用是比 .net 多的,所以我们可以写成 .(?:com|net)\b,而不是 .(?:net|com)\b。

3.4 只在必要时才使用子组

有些同学为了写出来的正则表达式更加易读,会故意在表达式中加一些括号用于区分,因为正则是默认保存子组的,正则引擎必须做一些额外工作来保存匹配到的内容,这会降低正则的匹配性能,如果后续不会再用到这个子组,记得加上 ?: 不保存子组。

3.5 警惕嵌套的子组重复

如果一个组里面包含重复,接着这个组整体也可以重复,比如 (.) 这个正则,匹配的次数会呈指数级增长,所以尽量不要写这样的正则。

3.6 测试性能的方法

我们写好正则之后,可以通过 regex101.com 这个网站来测试正则的匹配次数和性能,还可以debug调试,非常方便。

4.写在最后

最后在总结下上面讲到的内容:

思维导图

到这里,正则表达式的回溯陷阱就讲完了,如果有问题可以给我留言评论,谢谢。

正则表达式在线校验工具:https://regex101.com/

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

推荐阅读更多精彩内容