正则的匹配原理

正则引擎的分类

正则引擎主要可以分为三类:DFA、传统NFA(Traditional NFA)和POSIX NFA

用了这么久的正则,js属于哪一种呢?

测试引擎的类型

是否是传统型NFA

首先看看忽略优先量词是否得到支持?如果是,基本就能确定就是NFA。忽略优先量词是DFA不支持的,在POSIX NFA中也没有意义。只需要用/nfa|nfa not/去匹配“nfa not”。如果只有“nfa”匹配了,就是传统型NFA。如果整个“nfa not”都能匹配,则此引擎要么是POSIX NFA,要么是DFA。

匹配优先:尽可能多的匹配

忽略优先:尽可能少的匹配

匹配优先量词: * + ? {min,max}

忽略优先量词:*? +? ?? {min,max}?

例如:

/^\w+?a/g.exec('12a34a56');   // ["12a"]
/^\w+a/g.exec('12a34a56');   // ["12a34a"]
"nfa not".match(/nfa|nfa not/);   // "nfa"

所以js的引擎是传统型NFA。

如果得到的结果是“nfa not”,那么需要进一步判断是POSIX NFA还是DFA。

X(.+)+X去匹配“XX===========================”

如果执行时间长,就是POSIX NFA。

如果执行时间短,就是DFA,或者是支持某些高级优化的NFA。

console.time('/X(.+)+X/ test');
"==XX=============================".match(/X(.+)+X/);
console.timeEnd('/X(.+)+X/ test');
// /X(.+)+X/ test: 12686.867919921875ms

匹配时间这么长,是NFA引擎的回溯失控导致的。

所以,js是Traditional NFA。

匹配执行的实际流程

  1. 优先选择最左端的匹配结果
  2. 标准的匹配量词(*+?{m,n})是匹配优先的

优先选择最左端的匹配结果

根据这条规则,起始位置最靠左的匹配结果总是优先于其他可能的匹配结果。

这条规则的由来是:匹配先从需要查找的字符串的起始位置尝试匹配,在这里,“尝试匹配”的意思是,在当前位置测试整个正则表达式能匹配的每样文本。如果在当前位置测试了所有的可能之后不能找到匹配结果,就需要从字符串的第二个字符之前的位置开始重新尝试。在找到匹配结果之前必须在所有的位置重复此过程。只有在尝试过所有的起始位置都不能匹配结果的情况下,才会报告“匹配失败”。

例如:

var reg = /fat|cat|belly|your/;
var str = 'the gragging belly indicates that your cat is too fat.';
var arr = str.match(reg);
console.log(arr);
// ["belly"]

标准量词是匹配优先的

标准的匹配量词(*+?{m,n})是匹配优先的。如果用这些量词来约束某个表达式,例如(expr)*中的(expr)a?中的a,在匹配成功之前,进行尝试的次数是存在上限和下限的。

标准匹配量词的结果“可能”并非所有可能中最长的,但它们总是尝试匹配尽可能多的字符,直到匹配上限为止。简而言之,匹配优先量词是以大局为重。

例子?:

  1. .*去匹配字符串"Hey,guys!",将会得到"Hey,guys!"整个字符串。因为*是匹配优先的。

  2. \b\w+s\b来匹配包含“s”的字符串,比如说“dogs”,\w+完全能够匹配整个单词,但如果用\w+来匹配整个单词,s就无法匹配了。为了完成匹配,\w+必须匹配“dogs”中的“dog”,把最后的s\b留出来。\w+表达式会**“强迫”之前匹配优先部分“释放”某些字符**(这里是“s”)。

  3. 再来看看^.*([0-9][0-9])匹配"I was 14 years old 10 years ago"的过程。.*匹配整个字符串以后,第一个[0-9]的匹配要求.*释放一个字符o,但是这并不能让[0-9]匹配,所以.*必须继续释放字符,如此循环,直到.*最终释放0为止。不过即使第一个[0-9]能够匹配0,第二个[0-9]仍然不能匹配,为了匹配整个正则表达式,.*必须再次释放一个字符。即最后匹配了"I was 14 years old 10"

思考?:如果用^.*([0-9]+)来匹配“copyright 2019”,括号会捕获什么呢?

记住,大橘为重!!!

忽略优先量词

忽略优先量词有*?+???{num,num}?,也就是在匹配优先量词上加一个?忽略优先也叫惰性匹配。

例子?,我们把上面第三个例子改为忽略优先:

^.*?([0-9][0-9])匹配"I was 14 years old 10 years ago"。*?会因为是忽略优先而放弃匹配,然后控制权交给[0-9],发现匹配失败,回溯,接着匹配,以此往复当匹配到4时,匹配成功。即最后匹配了"I was 14"

我们发现,匹配优先量词会先贪婪的把能匹配的都匹配了,然后最后再以大局为重被迫交出几个字符,这样看好像是从后往前来匹配的,而忽略优先量词会懒惰的不想匹配,然后再以大局为重被迫匹配几个字符,在这样看好像是顺序匹配的。

假如有一篇长篇大论,然而只想匹配开头的子串的话,应该首先选择忽略优先量词,如果只想匹配后面的子串,应该首先选择匹配优先量词

表达式主导与文本主导

DFA和NFA反映了将正则表达式在应用算法上的根本差异。我们把NFA称为“表达式主导”引擎,把DFA称为“文本主导”。

NFA引擎:表达式主导

看下这个例子:to(nite|knight|night)匹配文本“tonight”。正则中第一个元素是t,它将会重复尝试,直到在目标字符串中找到“t”为止。之后,就检查紧随其后的字符是否能由o匹配,如果能,就检查(nite|knight|night),它的真正含义是niteknightnight。引擎会依次尝试这3种可能。表达式主导的引擎必须完全测试,才能得出结论。

DFA引擎:文本主导

DFA引擎在扫描字符串时,会记录“当前有效”的所有匹配可能。

继续扫描两个字符之后的情况:

这时有效的匹配变为两个,当扫描到g时,就只剩下一个可能匹配了。

比较NFA和DFA:

一般情况下,文本主导的DFA引擎会比正则表达式主导的NFA引擎更快。

在NFA的匹配过程中,目标文本中的某个字符可能会被正则表达式中的不同部分重复检测。即使某个子表达式能够匹配,为了检查表达式中剩下的部分,找到匹配,它也可能需要再一次应用。

DFA引擎是确定型的 —— 目标文本中的每个字符只会检查(最多)一次。对于一个已经匹配的字符,引擎同时记录了所有可能的匹配,这个字符只需要检测一次。

因为NFA具有表达式主导的特性,引擎的匹配原理就非常重要。

回溯

NFA引擎最重要的性质是,它会依次处理各个子表达式或组成元素,遇到需要在两个可能成功的可能中进行选择的时候,它会选择其一,同时记住另一个,以备稍后可能的需要。

需要做出选择的情形包括量词(决定是否尝试另一次匹配)和多选结构(决定选择哪个多选分支,留下哪个稍后尝试)。

如果正则表达式的余下部分匹配失败,引擎会知道需要回溯到之前做出选择的地方,选择其他的备用分支继续尝试。

我们来看一个简单的?:
to(nite|knight|night)匹配"hot tonic tonight":

正则中第一个元素t从字符串最左端开始匹配,因无法匹配,驱动引擎向后移动。到第三个时,‘t’能匹配,接下来的o无法匹配。本轮匹配失败。

to匹配成功之后,剩下的3个多选分支都成为可能。引擎选择其中之一进行尝试,留下其他的备用。

回溯的两个要点

  • 如果需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,而对于忽略优先量词,会选择“跳过尝试”。
  • 距离当前最近存储的选项就是当本地失败强制回溯时返回的。使用的原则是LIFO(后进先出)。

用NFA正则表达式的术语来说,那些备选分支处于“备用状态”。它们用来标记:在需要是时候,匹配可以从这里重新开始尝试。它们保留了两个位置:正则表达式中的位置和未尝试的分支在字符串中的位置。

例如:

ab?c匹配’ac’,a匹配之后,需要决定是否匹配b?,因为?是匹配优先的,它会尝试匹配。但是这时无法匹配。因为有一个备用状态,这里匹配失败不会导致整体匹配失败。引擎会进行回溯,把“当前状态”切换为最近保存的状态。

a▴c—ab?▴c

ab?c匹配’abX’,b能够匹配,但是因为c无法匹配X。于是引擎会回溯到之前的状态,交还b给c来匹配,显然这次也会失败。因为不存在其他状态,字符串中当期位置开始的整个匹配也会宣告失败。

a▴bX — ab?▴c

你以为事情到这里就结束了么?并没有。传动装置会继续在字符串中前行,再次尝试匹配。这里我们可以叫它伪回溯。

匹配重新开始于:a▴bX — ▴ab?c

a▴bX、ab▴X和abX▴都会匹配失败

忽略优先的匹配

ab??c匹配’abc’。a匹配之后:a▴bc — a▴b??c

接下来到b??,因为??是忽略优先的,它会首先尝试忽略,但是为了能从失败的分支中恢复,引擎会保存状态:a▴bc — a▴bc

接下来:a▴bc — ab??▴c

c无法匹配b,回溯到保存的备用状态。a▴bc — a▴bc

此时就能匹配成功了。

有了更详细的了解之后,我们再看看之前^.*([0-9][0-9])匹配"I was 14 years old 10 years ago"的例子。

.*成功匹配到字符串末尾时,保存了许多备用状态。现在我们到了字符串的末尾,并把控制权交给第一个[0-9],这里不能匹配成功,将进行回溯,把当前状态设为最近保存的状态,即.*匹配最后的o的状态。交还这个匹配,于是用[0-9]匹配o,同样会失败。“回溯-尝试”会不断循环,直到引擎交还到0为止,这里第一个[0-9]能匹配成功,但是第二个仍然无法匹配,所以必须继续回溯。当前的回溯会把字符串中的位置设置到1之前,所以现在匹配成功。$1得到20。

有了上面的理解,我们看下之前的一个例子:

需要对小数格式做处理:通常是保留小数点后两位数字,如果第三位不为零,也需要保留,去掉其他的数字。结果就是12.3750000034或者12.375会被修正为12.375,而37.500会被修正为37.50。

var str = '34.345200000234';
str = str.replace(/(.\d\d[1-9]?)\d*/,'$1');
// 34.345

但现在我们想想,如果str的数据本身规范格式,例如str是23.345,最后就相当于用‘.345’替换‘.345’。这个替换是白费功夫的。

那是否存在更有效率的办法呢?即只有当\d*确实匹配到字符时才替换。这时我们会想到把\d*替换为\d+。那这个能解决我们的问题么?

这时再看看匹配23.345。(.\d\d[1-9]?)\d+,括号中的内容在匹配‘.345’之后,\d+无法匹配。但是这并不会影响整个表达式的匹配,因为[1-9]?匹配5只是可选分支之一,还有一个备选状态。它容许[1-9]?匹配一个空字符,而把5留给至少必须匹配一个字符的\d+。并不能得到我们想要的结果。

如果匹配能够进行到(.\d\d[1-9]?)▴\d+中的位置,不进行回溯的话,那就能得到我们想要的结果。固化分组能实现我们的需求。

固化分组

固话分组:(?>...)

使用固化分组与正常的匹配并无区别,但是如果匹配进行到此结构之后(即进行到闭括号之后),那么此结构体中的所有备用状态都会被放弃(不能被回溯)。

也就是说,在固话分组匹配结束时,它已经匹配的文本已经固化成为了一个单元,只能作为整体保留或放弃。括号内的子表达式中未尝试过的备用状态都不复存在了,所以回溯也不能选择其中状态。

(.\d\d(?>[1-9]?))\d+,在固话分组(?>[1-9]?)内,如果[1-9]不能匹配,正则表达式会返回?留下的备用状态。然后匹配脱离固化分组,继续前进到\d+。在这种情况下,当控制权离开固化分组时,没有备用状态需要放弃。

如果[1-9]能够匹配。例如‘.345’,因为\d+无法匹配,正则引擎需要回溯,但回溯又无法进行,因为备用状态已经不存在了。所以匹配失败,‘.345’不需要处理。

使用固化分组加快匹配失败的速度

我们一眼就能看出,^\w+:无法匹配’Subject’,但是正则引擎必须经过尝试才能得到这个结论,及此次匹配中所有的回溯都是白费功夫。如果冒号无法匹配最后的字符,那么它当然无法匹配+交换的任何字符。

所以可以改为:^(?>\w+):

但是很遗憾,js引擎不支持固化分组。

用肯定顺序环视模拟固化分组

(?>exp)可以用?=(exp)\1来模拟。这里的关键就是,后面的\1捕获的就是环视结构捕获的单词,而这当然会匹配成功。在这里\1并非多此一举,而是为了把匹配从这个单词结束的位置进行下去。

上面的例子即可改为:^(?=(\w+))\1:

正则表达式(三)—— 正则的匹配原理相关推荐

  1. 正则表达式之 NFA 引擎匹配原理详解

    文章目录 一.为什么要了解引擎匹配原理 二.正则表达式引擎 三.预备知识 (一)字符串组成 (二)占有字符和零宽度 (三)控制权和传动 四.正则表达式简单匹本过程 (一)基础匹配过程 (二)含有匹配优 ...

  2. 深入入门正则表达式(java) - 匹配原理 - 1 - 引擎分类与普适原则

    本节第一部分主要介绍正则引擎的分类,由于java属于NFA,所以只重点介绍此类.其余类型简要或不做介绍. 分类的内容全部来自<精通正则表达式>v3 引擎类型 程序 DFA awk(大多数版 ...

  3. 快速解决正则----模糊匹配、字符类、量词

    主体概要 正则表达式是什么 是匹配模式.要么匹配字符.要么匹配位置 内容 掌握字符类和量词就能解决大部分常见正则问题 关键内容:字符组.量词.贪婪.惰性.模糊.横向.纵向.分支.位置 重要的基础   ...

  4. 正则学习(2)--- 简单匹配原理

    写写对简单的匹配原理的理解,还是以php为主. 首先,正则引擎主要可分为两大类:DFA和NFA,反正引擎见多了就不奇怪了,简单理解就是不同的匹配方式,就好比在数组中查找数据时,有的是从头开始顺序,查找 ...

  5. 正则表达式三种匹配模式:贪婪模式,勉强模式,占有模式的区别

    正则表达式一共有三种模式:贪婪模式.勉强模式.侵占模式. 我在做项目的时候,曾领教过正则"回溯陷阱"的厉害,所以,今天做个总结下正则常见的两种模式的区别: [贪婪模式]:正则表达式 ...

  6. 正则基础之 NFA引擎匹配原理

    来源:http://www.jb51.net/article/19332.htm 1 为什么要了解引擎匹配原理 一个个音符杂乱无章的组合在一起,弹奏出的或许就是噪音,同样的音符经过作曲家的手,就可以谱 ...

  7. java 正则 惰性匹配_正则表达式 - 贪婪与非贪婪(惰性)

    使用场景 有时,我们想用正则匹配以某个子串开头,且以某个子串或字符结尾的子字符串,但是结尾的字串或字符在原字符串中出现了多次,但我们只想匹配从开始处到第一次出现的地方,换句话说,想得到开始和结尾之间内 ...

  8. java正则出现次数_正则表达式(二)—匹配次数的正则

    正则表达式 匹配次数的正则 在前一篇文章中,我们总结了和位置相关的正则,在这篇文章中,我们将继续学习正则表达式其它的内容. 假如现在给你一个regular_1.txt文件,让你找出含有连续2个a的行, ...

  9. JS 正则表达式(正则匹配RegExp)

    JavaScript实现对象深拷贝的方法(5种) 知识回调(不懂就看这儿!) 场景复现 核心干货 举例引入 关于RegExp对象 语法 修饰符--区分大小写和全局匹配 方括号--查找某个范围内的字符 ...

最新文章

  1. 从复现人类智能到挑战AI大工程,智能计算正经历什么考验?
  2. C#_数据库基本交互
  3. 如何配置IIS服务器使用的中间证书( 微软 KB954755)
  4. (旧)走遍美国——(三、文化1)
  5. python输出星号_Python的星号(*、**)的作用
  6. android EditText 控件中加入图片(非背景图片)
  7. java怎样调用图像做按钮_swing-Java:使用图像作为按钮
  8. C#利用CDOSYS组件发邮件的一些小结
  9. 凸二次规划问题 库函数
  10. DEA模型及matlab应用3:SBM-DEA 模型
  11. OpenLayers 3加载矢量地图源
  12. sunshine in the rain
  13. 黑帆第一季/全集Black Sails迅雷下载
  14. 前端程序员拿到新电脑第一天,该做些什么?
  15. kafka安装及配置
  16. Android app本地图片转换成Drawable对象的方法
  17. Jolla 宣布 Sailfish 系统浏览器开源
  18. 通过view实现实时监测数据的实时更新展示
  19. 热点事件情境下微博舆情反转预测
  20. 小程序配置服务器域名

热门文章

  1. 视频要怎么一键锐化并修改尺寸?
  2. Unity动画播放BUG
  3. C语言断点测试无法输入,VS中无法加入断点进行调试解决方案
  4. JAVA入门基础进阶(十四)—— 实现多线程、线程同步、生产者消费者
  5. 【GNZ48-章泽婷应援会】基于Java的SNH48Group应援会机器人(三)发送消息
  6. 301跳转:http跳转https不带www跳转到带www
  7. jQuery获取兄弟元素的各种方法总结
  8. 【AD18】原理图去除网格并更改底色
  9. xms 缩写_Java中-XMX -xmn 是什么的缩写
  10. 解析企业数字化转型的驱动力