正则表达式回溯

前几天有小伙伴来求救说页面上有一个input框,随着用户不断输入内容页面响应会越来越慢直到完全失去响应。

简单沟通过后得知具体场景是这样的:

  • input框中允许用户输入一连串逗号分隔的商品id
  • 用户输入的过程中实时检测用户输入的内容是否符合规则,若不符合则给出提示信息

小伙伴的解决方案也很直接:

  • input框绑定keyup事件。
  • keyup事件回调函数中通过正则表达式判断是否符合规则,决定是否展示提示信息。

经过反复验证得到如下规律:

  • 用户在输入商品 id 的过程中(连续输入多个数字)不会卡顿
  • 用户输入逗号时,出现卡顿。随着输入商品 id 的数量增加,卡顿越来越明显,直至浏览器失去响应。

于是打开 Chrome 开发者工具,选择 Performance (原 Timeline) 标签页。将整个过程记录下来,得到如下时间线:

其中黄色宽条表示 JavaScript 主线程的执行情况。连续的黄条越长,表示单次 JavaScript 运行的时间越长。也就意味着 UI 失去响应的时间越长。这一点从截图中的蓝色框中也可以得到印证。蓝色框中的红色长条表示浏览器一帧(一次渲染)所需要的时间。

那么到底是 JavaScript 中的哪些代码占中了这么长 cpu 时间呢?我们在底部的选项卡中选中Bottom-Up,按Total Time降序排列。得到如下结果:

可以看出,72.% 的 cpu 时间用在了一条正则表达式上。你肯定想到了,这就是小伙伴用来检查用户输入是否合法的正则表达式。

完整的正则表达式是这样的:

/^\s*((\d+(\,|,)\d+)*|(\d+))\s*$/

接着去regex101上测试一下,测试数据如下,由 10 个商品 ID 组成的字符串:

123456789,123456789,123456789

执行结果如下:

可以看到执行速度非常快,只用了不到 1ms。

接下来在测试数据结尾加一个逗号,以模拟不符合规则的情况:

正则表达式执行的时间暴增到 4.15s。

经过多次测试发现:每次正常匹配执行的时间都很短。每次不匹配时,执行的时间都很长,且随着字符串长度的增加,时间成倍的增长。

接下来让我们认真的观察一下这个正则表达式:

 去掉匹配首尾的空白字符,其核心结构只有两部分((\d+(\,|,)\d+)*(\d+)。前者用于匹配多个商品 ID 的情况,后者匹配只有一个商品 ID 的情况。

前者的基本模式是这样的商品ID,商品ID,然后把该模式重复多次。仔细观察后很快我就发现了第一个问题,假设用户输入的内容,商品ID无法与基本模式形成匹配。

这的是这样吗?

测试发现,依然可以匹配。但匹配的内容和我们预期的并不一致。

最后一次匹配的内容是,9,123456789。不难想象第一次的匹配结果就是123456789,12345678

这里可以看出小伙伴编写的正则有两个问题:

  1. 逻辑错误。通过测试结果可以看出无法匹配出正确的商品 ID。如果商品 ID 运行只有 1 位数字,则匹配失败。
  2. 性能差。

在了解需求后,我给小伙伴提供了一种正则写法:

^\s*(\d+(,|,))*\d+\s*$

经过测试,这种写法在保证逻辑无误的前提下还保证了执行效率(在有数百个商品 ID 的情况下依然可以在几毫秒内执行完毕)。

讲到这里,你可能会有两个问题:

  1. 为何第一种写法的正则表达式匹配结果和我们预想的不一致。
  2. 为何两种写法的性能差别如此之大。

要回答这个问题,还要从正则表达式中*符号的执行逻辑说起。

回溯

大家都知道*表示匹配前面的子表达式 0 次或多次(且尽可能多的匹配)。但这个逻辑具体是如何执行的呢?让我们通过几个小例子来看一下。

Round 1

假设有正则表达式/^(a*)b$/和字符串aaaaab。如果用该正则匹配这个字符串会得到什么呢?

答案很简单。两者匹配,且捕获组捕获到字符串aaaaa

Round 2

这次让我们把正则改写成/^(a*)ab$/。再次和字符串aaaaab匹配。结果如何呢?

两者依然匹配,但捕获组捕获到字符串aaaa。因为捕获组后续的表达式占用了 1 个a字符。但是你有没有考虑过这个看似简单结果是经过何种过程得到的呢?

让我们一步一步来看:

  1. 匹配开始(a*)捕获尽可能多的字符a
  2. (a*)一直捕获,直到遇到字符b。这时(a*)已经捕获了aaaaa
  3. 正则表达式继续执行(a*)之后的ab匹配。但此时由于字符串仅剩一个b字符。导致无法完成匹配。
  4. (a*)从已捕获的字符串中“吐”出一个字符a。这时捕获结果为aaaa,剩余字符串为ab
  5. 重新执行正则中ab的匹配。发现正好与剩余字符串匹配。整个匹配过程结束。返回捕获结果aaaa

从第3,4步可以看到,暂时的无法匹配并不会立即导致整体匹配失败。而是会从捕获组中“吐出”字符以尝试。这个“吐出”的过程就叫回溯

回溯并不仅执行一次,而是会一直回溯到另一个极端。对于*符号而言,就是匹配 0 次的情况。

Round 3

这次我们把正则改为/^(a*)aaaab$/。字符串依然为aaaaab。根据前边的介绍很容易直到。此次要回溯 4 次才可以完成匹配。具体执行过程不再赘述。

悲观回溯

了解了回溯的工作原理,再来看悲观回溯就很容易理解了。

Round 4

这次我们的正则改为/^(a*)b$/。但是把要匹配的字符串改为aaaaa。去掉了结尾的字符b

让我们看看此时的执行流程:

    (a*)首先匹配了所有aaaaa
  1. 尝试匹配b。但是匹配失败。
  2. 回溯 1 个字符。此时剩余字符串为a。依然无法匹配字符b
  3. 回溯一直进行。直到匹配 0 次的情况。此时剩余字符串为aaaaa。依然无法匹配b
  4. 所有的可能性均已尝试过,依然无法匹配。最终导致整体匹配失败。

可以看到,虽然我们可以一眼看出二者无法匹配。但正则表达式在执行时还要“傻傻的”逐一回溯所有可能性,才能确定最终结果。这个“傻傻的”回溯过程就叫悲观回溯

虽然这个过程看起来有点傻,但是不是感觉也没什么大问题?为何会有性能问题呢?让我们回到最初的那个正则表达式。

Round 5

这次正则表达式回到^\s*((\d+(\,|,)\d+)*|(\d+))\s*$。字符串为 让我们统计一下:

  1. 首轮执行过后的捕获结果是。但这时剩余字符串仅剩一个字符。于是开始悲观回溯。
  2. 首先看第一个匹配不变的情况下,第二个匹配组回溯的情况。

    a. 回退 1 个字符。剩余字符串为 b. 回退 2 个字符。剩余字符串为89,。不匹配。但是89又进行一次回溯。共回溯 2 次。
    c. 以此类推。最多回退 8 个字符。此时剩余字符串为23456789,。共可以回溯 8 次。
    d. 累计回溯 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36 次。

  3. 接着,第一个捕获组回溯 1 个字符。捕获结果变为。此时又将循环一遍 2 中的所有逻辑。累计回溯 36 + 1次。

  4. 以此类推,全部回溯完成,需要回溯 324 次。

假设我们增加一个商品 ID,字符串变为次数增加到 2628 次。

以此类推可得。

商品 ID 个数 回溯次数
3 324
4 2628
5 21060
6 168516
7 1348164
8 10785348
9 86282820
10 690262596

可见问题在于,随着商品 ID 个数的增长,回溯次数会成指数级增长。最终导致 JavaScript 主进程忙于进行计算,使页面失去响应。

但是我当时给出的解决方案:

 也使用了*符号,按说也会进行悲观回溯。为何没有性能问题呢?

答案在于,对于同一字符串是否有多种可行的匹配模式。也就是说对于某个固定的字符串,你的正则表达式是否有“唯一解”。

举例对于我给出的正则,对于字符串123456789。因此,在回溯时只需进行一次线性的回溯即可(24 次)。而不会像前面分析的第一种正则一样,有多种“可能”的匹配方式。

解决方

在了解了悲观回溯为何会导致性能问题后,就可以考虑如何解决这个问题。要解决这个问题,大概有以下几个思路:

思路一: 禁止回溯

这个思路很直接,既然回溯可能有性能问题,那我们是否可以禁止正则表达式进行回溯呢。

答案是:可以。

有两种语法可以防止回溯:

  • 有限量词(Possessive Quantifiers)
  • 原子分组(Atomic Grouping)

关于这两种语法,感兴趣的同学可以自行 Google。在此不详细解释。因为这两种语法在 JavaScript 中均不被支持

思路二:避免导致性能问题的回溯

这个思路也比较容易想到。其实经过思考不难想到。两种模式的正则表达式很可能会导致有性能问题的回溯。

  • 前后重复的模式。 例如/x*x*/。虽然这个例子看起来很“弱智”,但是当规则变复杂时,每一个x又可能是由多个子表达式组成的。当这些子表达式存在逻辑上的交集时,就可能会出现性能问题。
  • 嵌套的量词。例如/(x*)*/包括文中提到的第一个正则也属于这种模式。

当我们在编写正则表达式时写出了这种模式的时候,大家就要谨慎起来。考虑一下是否有潜在的性能问题,是否有更好的写法了。

思路三:不使用正则表达式

其实像文中举的这个例子,甚至都没必要使用正则表达式。直接写一个 JavaScript 函数,按逗号切分字符串,逐个字符判断即可。而且可以保证代码性能是线性的。


More:

求正则回溯分析工具,借助工具,更好的优化正则

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


正则替换html代码中img标签的src值在开发富文本信息在移动端展示的项目中,难免会遇到后台返回的标签文本信息中img标签src属性按照相对或者绝对路径返回的形式,类似:<img src="qinhancity/v1.0.0/ima
正则表达式
AWK是一种处理文本文件的语言,是一个强大的文件分析工具。它是专门为文本处理设计的编程语言,也是行处理软件,通常用于扫描,过滤,统计汇总等工作,数据可以来自标准输入也可以是管道或文件。当读到第一行时,匹配条件,然后执行指定动作,在接着读取第二行数据处理,不会默认输出。如果没有定义匹配条件,则是默认匹配所有数据行,awk隐含循环,条件匹配多少次,动作就会执行多少次。逐行读取文本,默认以空格或tab键为分割符进行分割,将分割所得的各个字段,保存到内建变量中,并按模式或或条件执行编辑命令。与sed工作原理相比:s
正则表达式是特殊的字符序列,利用事先定义好的特定字符以及他们的组合组成了一个规则,然后检查一个字符串是否与这种规则匹配来实现对字符的过滤或匹配。我们刚才在学习正则表达式的时候,我们表示数字,字母下划线的时候是用w表示的,为什么我们在书写的时候用的是w?我们可以发现我们分割空格的话,并没有达到我们预期的效果,这里我们可以使用正则表达式的方式进行分割。我们可以发现,我们和上面得到的结果不一致,既然出错了,肯定是我们的使用方式不对。看到这里我们就能感受到正则表达式的作用了,正则表达式是字符串处理的有力工具。
Python界一名小学生,热心分享编程学习。
收集整理每周优质开发者内容,包括、、等方面。每周五定期发布,同步更新到和。欢迎大家投稿,,推荐或者自荐开源项目/资源/工具/文章~
本文涉及Shell函数,Shell中的echo、printf、test命令等。
常用正则表达,包括: 密码、 手机号、 身份证、 邮箱、 中文、 车牌号、 微信号、 日期 YYYY-MM-DD hh:mm:ss、 日期 YYY-MM-DD、 十六进制颜色、 邮政编号、 用户名、 QQ号
一、python【re】的用法1、re.match函数·单一匹配-推荐指数【★★】2、re.search函数·单一匹配-推荐指数【★★★★★】3、re.findall函数·多项匹配-推荐指数【★★★★★】4、re.finditer函数·多项匹配-推荐指数【★★★★】5、re.sub函数·替换函数-推荐指数【★★★★】二、正则表达式示例·总有一款适合你1、正则表达式匹配HTML指定id/class的标签2、正则表达式匹配HTML中所有a标签中的各类属性值3、获取标签的文本值
1.借助词法分析工具Flex或Lex完成(参考网络资源)2.输入:高级语言源代码(如helloworld.c)3.输出:以二元组表示的单词符号序列。通过设计、编制、调试一个具体的词法分析程序,加深对词法分析原理的理解,并掌握在对程序设计语言源程序进行扫描过程中将其分解为各类单词的词法分析方法。由于各种不同的高级程序语言中单词总体结构大致相同,基本上都可用一组正则表达式描述,所以构造这样的自动生成系统:只要给出某高级语言各类单词词法结构的一组正则表达式以及识别各类单词时词法分析程序应采取的语义动作,该系统
正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。例如:我们在写登录注册功能的时候使用的表单验证(对用户名、密码进行一些字符或长度进行限制) ===> (`匹配`) - 正则表达式还常用于过滤掉页面内容的一些敏感词汇。例如:我们平常在打游戏时候的口吐芬芳被换成了***:full_moon_with_face: ===> (`替换`) - 正则表达式从字符串中获取我们想要的特定部分。例如:我们在逛淘宝的时候在搜索框中搜索内容,会弹出很多与搜索相关的提示内容 ===> (`提取`) etc..
通过上面几个简单的示例,可以了解到常见的基础正则表达式的元字符主要包括以下几个^ 匹配输入字符串的开始位置。除非在方括号表达式中使用,表示不包含该字符集合。要匹配”^”字符本身,请使用"^"$ 匹配输入字符串的结尾位置。如果设置了RegExp对象的 Multiline属性,则"$”也匹配'n'或'r’,。要匹配”$"字符本身,请使用”$". 匹配除"rn"之外的任何单个字符 反斜杠,又叫转义字符,去除其后紧跟的元字符或通配符的特殊意义* 匹配前面的子表达式零次或多次。...
给出补充后描述 C 语言子集单词符号的正则文法,设计并实现其词法分析程序。
正则表达式(Regular Expression),又称规则表达式,它不是某个编程语言所特有的,是计算机科学的一个概念,通常被用来检索和替换符合某些规则的文本。
Python Re 正则表达式 数据匹配提取 基本使用
正则表达式:是用来描述字符串内容格式,使用它通常用于匹配一个字符串的内容是否符合格式要求
python的学习还是要多以练习为主,想要练习python的同学,推荐可以去牛客网看看,他们现在的IT题库内容很丰富,属于国内做的很好的了,而且是课程+刷题+面经+求职+讨论区分享,一站式求职学习网站,最最最重要的里面的资源全部免费!