前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入正则表达式(3):正则表达式工作引擎流程分析与原理释义

深入正则表达式(3):正则表达式工作引擎流程分析与原理释义

原创
作者头像
周陆军
修改于 2020-06-08 06:25:20
修改于 2020-06-08 06:25:20
1.9K00
代码可运行
举报
文章被收录于专栏:前端架构前端架构
运行总次数:0
代码可运行

作为正则的使用者也一样,不懂正则引擎原理的情况下,同样可以写出满足需求的正则,但是不知道原理,却很难写出高效且没有隐患的正则。所以对于经常使用正则,或是有兴趣深入学习正则的人,还是有必要了解一下正则引擎的匹配原理的。

有兴趣可以回顾《深入正则表达式(0):正则表达式概述》

正则引擎类型

正则引擎主要可以分为两大类:一种是DFA(Deterministic Finite Automatons/确定性有限自动机—),一种是NFA(Nondeterministic Finite Automatons/非确定性有限自动机)。总的来说,

  • DFA可以称为文本主导的正则引擎
  • NFA可以称为表达式主导的正则引擎

NFA与DFA工作的区别:

我们常常说用正则去匹配文本,这是NFA的思路,DFA本质上其实是用文本去匹配正则

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'for tonight's'.match(/to(nite|knite|night)/);
  • 如果是NFA引擎,表达式占主导地位。在字符串先查找字符串中的t,然后依次匹配,如果是o,则继续(以此循环)。匹配到to后,到n,就面临三种选择,每一种都去尝试匹配一下(它也不嫌累),第一个分支也是依次匹配,到t这里停止(nite分到t这里直接被淘汰);同理,接着第二个分支在k这里也停止了;终于在第三个分支柳暗花明,找到了自己的归宿。 NFA 工作方式是以正则表达式为标准,反复测试字符串,这样同样一个字符串有可能被反复测试了很多次!
  • 如果是DFA引擎呢,文本占主导地位。从整个字符串第一个字符开始f开始查找t,查找到t后,定位到t,以知其后为o,则去查看正则表达式其相应位置后是否为o,如果是,则继续(以此循环),再去查正则表达式o后是否为n(此时淘汰knite分支),再后是否为g(淘汰nite分支),这个时候只剩一个分支,直接匹配到终止即可。

只有正则表达式才有分支和范围,文本仅仅是一个字符流。这带来什么样的后果?就是NFA引擎在匹配失败的时候,如果有其他的分支或者范围,它会返回,记住,返回,去尝试其他的分支而DFA引擎一旦匹配失败,就结束了,它没有退路。

这就是它们之间的本质区别。其他的不同都是这个特性衍生出来的。

NFA VS DFA

首先,正则表达式在计算机看来只是一串符号,正则引擎首先肯定要解析它。NFA引擎只需要编译就好了;而DFA引擎则比较繁琐,编译完还不算,还要遍历出表达式中所有的可能。因为对DFA引擎来说机会只有一次,它必须得提前知道所有的可能,才能匹配出最优的结果。

所以,在编译阶段,NFA引擎比DFA引擎快

其次,DFA引擎在匹配途中一遍过,溜得飞起。相反NFA引擎就比较苦逼了,它得不厌其烦的去尝试每一种可能性,可能一段文本它得不停返回又匹配,重复好多次。当然运气好的话也是可以一遍过的。

所以,在运行阶段,NFA引擎比DFA引擎慢

最后,因为NFA引擎是表达式占主导地位,所以它的表达能力更强,开发者的控制度更高,也就是说开发者更容易写出性能好又强大的正则来,当然也更容易造成性能的浪费甚至撑爆CPU。DFA引擎下的表达式,只要可能性是一样的,任何一种写法都是没有差别(可能对编译有细微的差别)的,因为对DFA引擎来说,表达式其实是死的。而NFA引擎下的表达式,高手写的正则和新手写的正则,性能可能相差10倍甚至更多。

也正是因为主导权的不同,正则中的很多概念,比如非贪婪模式、反向引用、零宽断言等只有NFA引擎才有。

所以,在表达能力上,NFA引擎秒杀DFA引擎

但是NFA以表达式为主导,因而NFA更容易操纵,因此一般程序员更偏爱NFA引擎!

当今市面上大多数正则引擎都是NFA引擎,应该就是胜在表达能力上。

总体来说,两种引擎的工作方式完全不同,一个(NFA)以表达式为主导,一个(DFA)以文本为主导!两种引擎各有所长,而真正的引用则取决与你的需要以及所使用的语言。

这两种引擎都有了很久的历史(至今二十多年),当中也由这两种引擎产生了很多变体!

因为NFA引擎比较灵活,很多语言在实现上有细微的差别。所以后来大家弄了一个标准,符合这个标准的正则引擎就叫做POSIX NFA引擎,其余的就只能叫做传统型NFA引擎咯。 Deterministic finite automaton,Non-deterministic finite automaton,Traditional NFA,Portable Operating System Interface for uniX NFA

于是POSIX的出台规避了不必要变体的继续产生。这样一来,主流的正则引擎又分为3类:DFA,传统型NFA,POSIX NFA。

正则引擎三国

DFA引擎

DFA引擎在线性时状态下执行,因为它们不要求回溯(并因此它们永远不测试相同的字符两次)。

DFA引擎还可以确保匹配最长的可能的字符串。但是,因为 DFA 引擎只包含有限的状态,所以它不能匹配具有反向引用的模式;并且因为它不构造显示扩展,所以它不可以捕获子表达式。

DFN不回溯,所以匹配快速,因而不支持捕获组,支持反向引用和$number引用

传统的 NFA引擎

传统的 NFA 引擎运行所谓的“贪婪的”匹配回溯算法,以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。因为传统的 NFA 构造正则表达式的特定扩展以获得成功的匹配,所以它可以捕获子表达式匹配和匹配的反向引用。但是,因为传统的 NFA 回溯,所以它可以访问完全相同的状态多次(如果通过不同的路径到达该状态)。因此,在最坏情况下,它的执行速度可能非常慢。因为传统的 NFA 接受它找到的第一个匹配,所以它还可能会导致其他(可能更长)匹配未被发现

大多数编程语言和工具使用的是传统型的NFA引擎,它有一些DFA不支持的特性:

  • 捕获组、反向引用和$number引用方式;
  • 环视(Lookaround,(?<=…)、(?<!…)、(?=…)、(?!…)),或者有的有文章叫做预搜索;
  • 忽略优化量词(??、*?、+?、{m,n}?、{m,}?),或者有的文章叫做非贪婪模式;
  • 占有优先量词(?+、*+、++、{m,n}+、{m,}+,目前仅Java和PCRE支持),固化分组(?>…)。

POSIX NFA引擎

POSIX NFA引擎主要指符合POSIX标准的NFA引擎,与传统的 NFA 引擎类似,不同的一点在于:提供longest-leftmost匹配,也就是在找到最左侧最长匹配之前,它将继续回溯(可以确保已找到了可能的最长的匹配之前它们将继续回溯)。因此,POSIX NFA 引擎的速度慢于传统的 NFA 引擎;并且在使用 POSIX NFA 时,您恐怕不会愿意在更改回溯搜索的顺序的情况下来支持较短的匹配搜索,而非较长的匹配搜索。

同DFA一样,非贪婪模式或者说忽略优先量词对于POSIX NFA同样是没有意义的。

三种引擎的使用情况

  • 使用传统型NFA引擎的程序主要有(主流):
    • Java、Emacs(JavaScript/actionScript)、Perl、PHP、Python、Ruby、.NET语言
    • VI,GNU Emacs,PCRE library,sed;
  • 使用POSIX NFA引擎的程序主要有:mawk,Mortice Kern Systems’ utilities,GNU Emacs(使用时可以明确指定);
  • 使用DFA引擎的程序主要有:awk,egrep,flex,lex,MySQL,Procmail等;
  • 也有使用DFA/NFA混合的引擎:GNU awk,GNU grep/egrep,Tcl。

《精通正则表达式》书中说POSIX NFA引擎不支持非贪婪模式,很明显JavaScript不是POSIX NFA引擎。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'123456'.match(/\d{3,6}/);
// ["123456", index: 0, input: "123456", groups: undefined]
'123456'.match(/\d{3,6}?/);
// ["123", index: 0, input: "123456", groups: undefined]

JavaScript的正则引擎是传统型NFA引擎。

为什么POSIX NFA引擎不支持也没有必要支持非贪婪模式?

回溯

现在我们知道,NFA引擎是用表达式去匹配文本,而表达式又有若干分支和范围,一个分支或者范围匹配失败并不意味着最终匹配失败,正则引擎会去尝试下一个分支或者范围。

正是因为这样的机制,引申出了NFA引擎的核心特点——回溯。

首先我们要区分备选状态和回溯。

什么是备选状态?就是说这一个分支不行,那我就换一个分支,这个范围不行,那我就换一个范围。正则表达式中可以商榷的部分就叫做备选状态。

备选状态可以实现模糊匹配,是正则表达能力的一方面。

回溯可不是个好东西。想象一下,面前有两条路,你选择了一条,走到尽头发现是条死路,你只好原路返回尝试另一条路。这个原路返回的过程就叫回溯,它在正则中的含义是吐出已经匹配过的文本。

我们来看两个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'abbbc'.match(/ab{1,3}c/);
// ["abbbc", index: 0, input: "abbbc", groups: undefined]
'abc'.match(/ab{1,3}c/);
// ["abc", index: 0, input: "abc", groups: undefined]

第一个例子,第一次a匹配a成功,接着碰到贪婪匹配,不巧正好是三个b贪婪得逞,最后用c匹配c成功。

正则

文本

/a/

a

/ab{1,3}/

ab

/ab{1,3}/

abb

/ab{1,3}/

abbb

/ab{1,3}c/

abbbc

第二个例子的区别在于文本只有一个b。所以表达式在匹配第一个b成功后继续尝试匹配b,然而它见到的只有黄脸婆c。不得已将c吐出来,委屈一下,毕竟贪婪匹配也只是尽量匹配更多嘛,还是要臣服于匹配成功这个目标。最后不负众望用c匹配c成功。

正则

文本

/a/

a

/ab{1,3}/

ab

/ab{1,3}/

abc

/ab{1,3}/

ab

/ab{1,3}c/

abc

请问,第二个例子发生回溯了吗?

并没有。

诶,你这样就不讲道理了。不是把c吐出来了嘛,怎么就不叫回溯了?

回溯是吐出已经匹配过的文本。匹配过程中造成的匹配失败不算回溯

为了让大家更好的理解,我举一个例子:

你和一个女孩子(或者男孩子)谈恋爱,接触了半个月后发现实在不合适,于是提出分手。这不叫回溯,仅仅是不合适而已。 你和一个女孩子(或者男孩子)谈恋爱,这段关系维持了两年,并且已经同居。但由于某些不可描述的原因,疲惫挣扎之后,两人最终还是和平分手。这才叫回溯。 虽然都是分手,但你们应该能理解它们的区别吧。

为了让大家更好的理解,我举一个例子:

你和一个女孩子(或者男孩子)谈恋爱,接触了半个月后发现实在不合适,于是提出分手。这不叫回溯,仅仅是不合适而已。

你和一个女孩子(或者男孩子)谈恋爱,这段关系维持了两年,并且已经同居。但由于某些不可描述的原因,疲惫挣扎之后,两人最终还是和平分手。这才叫回溯。

虽然都是分手,但你们应该能理解它们的区别吧。

网络上有很多文章都认为上面第二个例子发生了回溯。至少根据我查阅的资料,第二个例子发生的情况不能被称为回溯。当然也有可能我([马蹄疾]是错的,欢迎讨论。

我们再来看一个真正的回溯例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'ababc'.match(/ab{1,3}c/);
// ["abc", index: 2, input: "ababc", groups: undefined]

匹配文本到ab为止,都没什么问题。后面既匹配不到b,也匹配不到c。引擎只好将文本ab吐出来,从下一个位置开始匹配。因为上一次是从第一个字符a开始匹配,所以下一个位置当然就是从第二个字符b开始咯。

正则

文本

/a/

a

/ab{1,3}/

ab

/ab{1,3}/

aba

/ab{1,3}/

ab

/ab{1,3}c/

aba

/a/

ab

/a/

aba

/ab{1,3}/

abab

/ab{1,3}/

ababc

/ab{1,3}/

abab

/ab{1,3}c/

ababc

一开始引擎是以为会和最早的ab走完余生的,然而命运弄人,从此天涯。

这他妈才叫回溯!

还有一个细节。上面例子中的回溯并没有往回吐呀,吐出来之后不应该往回走嘛,怎么往后走了?

我们再来看一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
'"abc"def'.match(/".*"/);
// [""abc"", index: 0, input: ""abc"def", groups: undefined]

因为.*是贪婪匹配,所以它把后面的字符都吞进去了。直到发现目标完不成,不得已往回吐,吐到第二个"为止,终于匹配成功。这就好比结了婚还在外面养小三,几经折腾才发现家庭才是最重要的,自己的行为背离了初衷,于是幡然悔悟。

正则

文本

/"/

"

/".*/

"a

/".*/

"ab

/".*/

"abc

/".*/

"abc"

/".*/

"abc"d

/".*/

"abc"de

/".*/

"abc"def

/".*"/

"abc"def

/".*"/

"abc"de

/".*"/

"abc"d

/".*"/

"abc"

我想说的是,不要被回溯的回字迷惑了。它的本质是把已经吞进去的字符吐出来。至于吐出来之后是往回走还是往后走,是要根据情况而定的。

优化正则表达式

现在我们知道了控制回溯是控制正则表达式性能的关键。

控制回溯又可以拆分成两部分:第一是控制备选状态的数量,第二是控制备选状态的顺序。

备选状态的数量当然是核心,然而如果备选状态虽然多,却早早的匹配成功了,早匹配早下班,也就没那么多糟心事了。

传统NFA工作流程

许多因素影响正则表达式的效率,首先,正则表达式适配的文本千差万别,部分匹配时比完全不匹配所用的时间要长。上面提到过,JavaScript是传统NFA引擎,当然每种浏览器的正则表达式引擎也有不同的内部优化。

为了有效地使用正则表达式,重要的是理解它们的工作原理。下面是一个正则表达式处理的基本步骤:

第一步:编译

当你创建了一个正则表达式对象之后(使用一个正则表达式直接量或者RegExp构造器),浏览器检查你的模板有没有错误,然后将它转换成一个本机代码例程,用于执行匹配工作。如果你将正则表达式赋给一个变量,你可以避免重复执行此步骤。

第二步:设置起始位置

当一个正则表达式投入使用时,首先要确定目标字符串中开始搜索的位置。它是字符串的起始位置,或由正则表达式的lastIndex属性指定,但是当它从第四步返回到这里的时候(因为尝试匹配失败),此位置将位于最后一次尝试起始位置推后一个字符的位置上。

      浏览器优化正则表达式引擎的办法是,在这一阶段中通过早期预测跳过一些不必要的工作。例如,如果一个正则表达式以^开头,IE 和Chrome通常判断在字符串起始位置上是否能够匹配,然后可避免愚蠢地搜索后续位置。另一个例子是匹配第三个字母是x的字符串,一个聪明的办法是先找到x,然后再将起始位置回溯两个字符。

第三步:匹配每个正则表达式的字元

      正则表达式一旦找好起始位置,它将一个一个地扫描目标文本和正则表达式模板。当一个特定字元匹配失败时,正则表达式将试图回溯到扫描之前的位置上,然后进入正则表达式其他可能的路径上。

      第四步:匹配成功或失败

      如果在字符串的当前位置上发现一个完全匹配,那么正则表达式宣布成功。如果正则表达式的所有可能路径都尝试过了,但是没有成功地匹配,那么正则表达式引擎回到第二步,从字符串的下一个字符重新尝试。只有字符串中的每个字符(以及最后一个字符后面的位置)都经历了这样的过程之后,还没有成功匹配,那么正则表达式就宣布彻底失败。

      牢记这一过程将有助于您明智地判别那些影响正则表达式性能问题的类型。

工具

[ regex101 ]是一个很多人推荐过的工具,可以拆分解释正则的含义,还可以查看匹配过程,帮助理解正则引擎。如果只能要一个正则工具,那就是它了。

[ regexper ]是一个能让正则的备选状态可视化的工具,也有助于理解复杂的正则语法。

参考文章:

https://baike.baidu.com/item/正则表达式

正则表达式工作原理 https://www.cnblogs.com/aaronjs/archive/2012/06/30/2570800.html

一次性搞懂JavaScript正则表达式之引擎 https://juejin.im/post/5becc2aef265da6110369c93

转载本站文章《深入正则表达式(3):正则表达式工作引擎流程分析与原理释义》, 请注明出处:https://www.zhoulujun.cn/html/theory/algorithm/IntroductionAlgorithms/8430.html

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
正则表达式的回溯[转]
近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求: 1. 英文字母大小写 2. 数字 3. 越南文 4. 一些特殊字符,如“&”,“-”,“_”等 看到这个要求的时候,自然而然地想到了正则表达式。于是就有了下面的表达式(写的比较龊):
流柯
2020/01/15
1.1K0
正则表达式的回溯[转]
正则表达式-引擎
现在基本所有的文字编辑软件都会包含正则表达式的功能,但是不同的编辑器所使用的引擎实现原理是不一样的,现在大家用的有三种引擎:
Dylan Liu
2019/07/01
9060
正则表达式-引擎
正则表达式引发的惨痛代价
在一次小型项目开发中,我遇到过这样一个问题。为了宣传新品,我们开发了一个小程序,按照之前评估的访问量,这次活动预计参与用户量 30W+,TPS(每秒事务处理量)最高 3000 左右。
田维常
2020/05/19
2K0
正则表达式引发的惨痛代价
第四章 正则表达式回溯法原理
第四章 正则表达式回溯法原理 学习正则表达式,是需要懂点儿匹配原理的。 而研究匹配原理时,有两个字出现的频率比较高:“回溯”。 听起来挺高大上,确实还有很多人对此不明不白的。 因此,本章就简单扼要地
程序猿DD
2018/02/01
1.2K0
第四章 正则表达式回溯法原理
[翻译]正则引擎的几种分类
正则表达式引擎是正则表达式匹配算法的基础。其有多种不同的实现,但大多数都是基于Henry Spencer的NFA引擎。
xindoo
2024/08/07
1020
正则表达式 引擎分类
DFA是文本主导,DFA引擎在扫描字符串时,会记录“当前有效”的所有匹配可能,因此目标文本中的每个字符最多只会检查一遍。
luoheng
2022/08/28
3540
一条正则表达式跑一天,这Bug我服了
前两天,因为一个没有经过深思熟虑的建议,让一位粉丝朋友写的一行代码,足足跑了一下午还没跑完,深感内疚;而且发现这个问题在实际的开发中也很容易遇到,且很难发现,今天来反思总结一下;
一行Java
2022/04/07
5780
一条正则表达式跑一天,这Bug我服了
精通正则表达式 - 打造高效正则表达式
        总的来说,提高正则表达式效率的关键在于彻底理解回溯背后的过程,掌握技巧来避免可能的回溯。
用户1148526
2023/10/14
9300
精通正则表达式 - 打造高效正则表达式
正则表达式简介
正则表达式(Regular Expression)是一门通用的知识,我们的工作中随处可见,掌握了它,可以显著提升我们的工作效率。它的主要作用是根据一串规则串用来匹配我们的目标内容。主流的编辑器(如notepad++,sublime等)通常都自带正则表达式的功能,很多编程语言也都有相应的库来支持,比如Python的re库。
未来sky
2019/11/21
5460
一文掌握开发利器:正则表达式
作者:mathe,腾讯QQ音乐前端开发工程师 正则表达式具有伟大技术发明的一切特点,它简单、优美、功能强大、妙用无穷。对于很多实际工作来讲,正则表达式简直是灵丹妙药,能够成百倍地提高开发效率和程序质量。 1. 正则常见规则 1.1 字符匹配 字符说明\转义符\d[0-9]。表示是一位数字。\D[^0-9]。表示除数字外的任意字符。\w[0-9a-zA-Z_]。表示数字、大小写字母和下划线。\W[^0-9a-zA-Z_]。非单词字符。\s[\t\v\n\r\f]。表示空白符,包括空格、水平制表符、 垂直
腾讯技术工程官方号
2020/09/17
1.3K0
Java 正则表达式 StackOverflowError 问题及其优化
正则可以看做一门 DSL,但它却应用极其广泛,可以轻松解决很多场景下的字符串匹配、筛选问题。同时呢有句老话: “ 如果你有一个问题,用正则表达式解决,那么你现在就有两个问题了。” Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems. 今天我们就来聊聊 Java 正则表达式 StackOverflowError 的问题及其一些
用户1177713
2018/02/24
3.4K0
正则表达式优化
DFA (Deterministic Finite Automaton 确定有穷自动机): 常见的只有MySQL,文本主导,不支持反向引用和捕获括号,但快
林万程
2019/08/05
1.2K0
正则表达式之进阶篇
本文主要通过介绍正则表达式中的一些进阶内容,让读者了解正则表达式在日常使用中用到的比较少但是又比较重要的一部分内容,从而让大家对正则表达式有一个更加深刻的认识。
黄Java
2018/11/01
6840
一篇值得收藏的正则表达式文章
目前越来越多的网站、编辑器、编程语言都已支持一种叫“正则表达式”的字符串查找“公式”,有过编程经验的同学都应该了解正则表达式(Regular Expression 简写regex)是什么东西,它是一种字符串匹配的模式(pattern),更像是一种逻辑公式。
龙哥
2020/02/26
8200
藏在正则表达式里的陷阱
前几天线上一个项目监控信息突然报告异常,上到机器上后查看相关资源的使用情况,发现 CPU 利用率将近 100%。通过 Java 自带的线程 Dump 工具,我们导出了出问题的堆栈信息。
Java团长
2019/05/22
6110
正则表达式的用法及原理
由于工作中和正则表达式打交道比较多,所以花了几天的时间系统学习了正则,在此总结一下。
Ritchie
2022/06/29
1.4K1
正则表达式基础
基本语法 基本语法_菜鸟教程 用\表示特殊形式或允许使用特殊字符,而不调用其特殊含义 不以任何特殊方式在字符串字面值中以'r'前缀处理反斜杠 所以r'\n'包含'\'和'n'两个字符,而'\n'
JNJYan
2019/01/18
7610
正则表达式性能优化
正则表达式是计算科学的一个概念,很多语言都实现了他,正则表达式使用一些特定的元字符来检索,匹配以及替换符合规则的字符串。
小土豆Yuki
2020/11/03
2.2K0
刨根究底正则表达式之二——正则表达式基础
虽然本系列文章开篇会简单介绍正则表达式的一些基础知识,但主要限于本系列文章所想强调的要点,因此本系列文章并不适合用于入门。
笨笨阿林
2019/01/18
1.2K0
正则表达式和 CPU 100%有什么故事?
前几天线上一个项目监控信息突然报告异常,上到机器上后查看相关资源的使用情况,发现 CPU 利用率将近 100%。通过 Java 自带的线程 Dump 工具,我们导出了出问题的堆栈信息。 我们可以看到所
纯洁的微笑
2018/07/20
1.5K1
相关推荐
正则表达式的回溯[转]
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验