[No0000100]正则表达式匹配解析过程分析(正则表达式匹配原理)&regexbuddy使用&正则优化

常见正则表达式引擎
引擎决定了正则表达式匹配方法及内部搜索过程,了解它至关重要的。目前主要流行引擎有:DFA,NFA两种引擎。

 

引擎

区别点

DFA
Deterministic finite automaton
确定型有穷自动机

DFA引擎它们不要求回溯(并因此它们永远不测试相同的字符两次),所以匹配速度快!DFA引擎还可以匹配最长的可能的字符串。

不过DFA引擎只包含有限的状态,所以它不能匹配具有反向引用的模式,还不可以捕获子表达式

代表性有:awk,egrep,flex,lex,MySQL,Procmail

NFA
Non-deterministic finite automaton

非确定型有穷自动机

又分为传统NFA,Posix NFA

传统的NFA引擎运行所谓的“贪婪的”匹配回溯算法(longest-leftmost),

以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。

传统的NFA回溯可以访问完全相同的状态多次,在最坏情况下,它的执行速度可能非常慢,但它支持子匹配

代表性有:GNU Emacs,Java,ergp,less,more,.NET语言

,PCRE library,Perl,PHP,Python,Ruby,sed,vi等,

一般高级语言都采用该模式。

 

DFA以字符串字符为主,逐个在正则表达式匹配查找,而NFA以正则表达式为主,在字符串中逐一查找。尽管速度慢,但是对操作者来说更简单,因此应用更广泛!下面所有以NFA引擎举例说明,解析过程!

 

解析引擎眼中的字符串组成
对于字符串“DEF”而言,包括D、E、F三个字符和 0、1、2、3 四个数字位置(零宽空间):0D1E2F3,对于正则表达式而言所有源字符串,都有字符和位置。正则表达式会从0号位置(可以匹配^),逐个去匹配的。

占有字符和零宽度
正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的(IsMatch开始为true);如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是零宽度的(IsMatch不为true)。占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。常见零宽字符有:^,(?=)等

正则表达式匹配过程详解实例
我们掌握了上面几个概念,我们接下来分析下几个常见的解析过程。结合使用软件regexBuddy来分析。

regexbuddy正则表达式测试工具使用方法(图文)

1、安装完regexbuddy

该工具支持多种程序语言正则表达式,如:perl,pcre,javascript,python,ruby,c#,java等等,还能自动生成程序代码,并且内部带有大量的常用正则表达式。 

2、一般切换到side by side:

3、匹配过程

匹配完,点击“Test”里面Debug(Here),自动切换到Debug界面:

匹配过程:\w+一下子贪婪匹配aaa27111ab,然后\d+没有匹配字符串了。开始回逆了,逐个字符减少,直到发现最后一个字符“1”与\d+匹配为止。最终匹配到字符串是:“aaa27111”

从上面一个匹配看,这个简单一个匹配,搜索了8次,进行了不断查找。如果我们已经准确知道自己要匹配什么样字符,我们可以对源正则表达式修改下,减少匹配次数。就达到优化正则表达式目的,提高匹配效率!

如果我们知道源字符串只是a-z字符,进行修改发现,只要用2次搜索就匹配到所需字符。

为什么需要性能测试工具 
我们都知道,正则表达式使用进行搜索查找,没有字符串直接查找快!而且性能是几何倍数下降。那么,为什么正则表达式速度会比字符串搜索慢呢。我们来看看,正则表达式查找字符串的匹配过程吧。正则表达式由一些元字符,普通字符,量词字符组合成。默认情况下,这些量词元字符(*,+,?)都是贪婪模式,会最大长度匹配字符串。我们知道,正则表达式往往搜索路径会有多个,我们看看,下面匹配过程。就知道,主要影响正则表达式执行性能有哪些了。 

正则表达式匹配过程如:\d+abc,元字符是:”12345bdc”,查找会从左向右进行,\d+,贪婪模式,一下子匹配到12345,然后bdc与\d+不能匹配,”abc”中,”a”字符,开始匹配”bdc”,发现匹配失败。正则表达式开始回溯匹配(贪婪模式量词开始逐一减少匹配字符长度),\d+只匹配”1234”,”5bdc”与”abc”匹配,任然失败。\d+继续减少匹配长度为:”123”,”45bdc”与”abc”匹配,任然失败。继续回退,直到\d+匹配”1”,用”2345bdc”与”bdc”匹配,任然失败。整个匹配就失败了。 

从上面过程中,我们发现,每次回溯,要重新操作匹配因此匹配搜索次数,直接影响正则表达式的性能。做正则表达式性能优化,一般就是优化查询的次数。这个是我们分析过程,如果有个工具能够实实在在看到每一步匹配过程,对于我们优化正则表达式将带来太多方便了。这里介绍工具是:regexbuddy软件,它就是一个实实在在看到匹配过程工具。  

Demo1: 源字符DEF,对应标记是:0D1E2F3,匹配正则表达式是:“DEF

过程可以理解为:首先由正则表达式字符 “D” 取得控制权,从位置0开始匹配,由 “D” 来匹配“D”,匹配成功,控制权交给字符 “E” ;由于“D”已被 “D” 匹配,所以 “E” 从位置1开始尝试匹配,由“E” 来匹配“E”,匹配成功,控制权交给 “F”;由“F”来匹配“F”,匹配成功。

Demo2:源字符DEF,对应标记是:0D1E2F3,匹配正则表达式是:/D\w+F/

过程可以理解为:首先由正则表达式字符 /D/ 取得控制权,从位置0开始匹配,由 /D/ 来匹配“D”,匹配成功,控制权交给字符 /\w+/ ;由于“D”已被 /D/ 匹配,所以 /\w+/ 从位置1开始尝试匹配,\w+贪婪模式,会记录一个备选状态,默认会匹配最长字符,直接匹配到EF,并且匹配成功,当前位置3了。并且把控制权交给 /F/ ;由 /F/ 匹配失败,\w+匹配会回溯一位,当前位置变成2。并把控制权交个/F/,由/F/匹配字符F成功。因此\w+这里匹配E字符,匹配完成!

Demo3:源字符DEF,对应标记是:0D1E2F3,匹配正则表达式是:/^(?=D)[D-F]+$/

过程可以理解为:元字符 /^/ 和 /$/ 匹配的只是位置,顺序环视(匹配完开头,从左往右依次匹配) /(?=D)/ (匹配当前位置,右边是否有字符“D”字符出现)只进行匹配,并不占有字符,也不将匹配的内容保存到最终的匹配结果,所以都是零宽度的。 首先由元字符 /^/ 取得控制权,从位置0开始匹配, /^/ 匹配的就是开始位置“位置0”,匹配成功,控制权交给顺序环视 /(?=D)/;/(?=D])/ 要求它所在位置右侧必须是字母”D”才能匹配成功,零宽度的子表达式之间是不互斥的,即同一个位置可以同时由多个零宽度子表达式匹配,所以它也是从位置0尝试进行匹配,位置0的右侧是字符“D”,符合要求,匹配成功,控制权交给 /[D-F]+/ ;因为 /(?=D)/ 只进行匹配,并不将匹配到的内容保存到最后结果,并且 /(?=D)/ 匹配成功的位置是位置0,所以 /[D-F]+/ 也是从位置0开始尝试匹配的, /[D-F]+/ 首先尝试匹配“D”,匹配成功,继续尝试匹配,直到匹配完”EF”,这时已经匹配到位置3,位置3的右侧已没有字符,这时会把控制权交给 /$/,元字符 /$/ 从位置3开始尝试匹配,它匹配的是结束位置,也就是“位置3”,匹配成功。此时正则表达式匹配完成,报告匹配成功。匹配结果为“DEF”,开始位置为0,结束位置为3。其中 /^/ 匹配位置0, /(?=D)/ 匹配位置0, /[D-F]+/ 匹配字符串“DEF”, /$/ 匹配位置3。

 

匹配详解

  1. 用“(\$)”匹配“$1.30”

匹配结果:

 

1.1.尝试从“ $ 1 . 3 0 ”的第一个“零宽空间”开始匹配“(\$)”: “ $ 1 . 3 0 ”匹配到,IsMatch=true。

1.2.   尝试从“ $ 1 . 3 0 ”的第二个“零宽空间”开始匹配“(\$)”:“1 . 3 0 ”依次均不匹配。

2. 用“(\$*)”匹配“$1.30”

匹配结果:

2.1.尝试从“ $ 1 . 3 0 ”的第一个“零宽位”开始匹配“(\$*)”: “ $ 1 . 3 0 ”匹配。(\$*是尽可能多的匹配$,此处匹配了1次$)

2.2尝试从“ $ 1 . 3 0 ”的第二个“零宽位”开始匹配“(\$*)”:由于1符合\$*(\$*是尽可能多的匹配$,此处匹配了0次$),所以“ 1”中的零宽空间被捕获,但1未被捕获。

2.3尝试从“ $ 1 . 3 0 ”的第三个“零宽位”开始匹配“(\$*)”:由于.符合\$*(\$*是尽可能多的匹配$,此处匹配了0次$),所以“ .”中的零宽空间被捕获,但.未被捕获。

2.4尝试从“ $ 1 . 3 0 ”的第四个“零宽位”开始匹配“(\$*)”:由于3符合\$*(\$*是尽可能多的匹配$,此处匹配了0次$),所以“ 3”中的零宽空间被捕获,但3未被捕获。

2.5尝试从“ $ 1 . 3 0 ”的第五个“零宽位”开始匹配“(\$*)”:由于0符合\$*(\$*是尽可能多的匹配$,此处匹配了0次$),所以“ 0”中的零宽空间被捕获,但0未被捕获。

2.6尝试从“ $ 1 . 3 0 ”的第六个“零宽位”开始匹配“(\$*)”:由于$(结尾)符合\$*(\$*是尽可能多的匹配$,此处匹配了0次$),所以“ $”(结尾)中的零宽空间被捕获,但结尾未被捕获。

3. 用“((1))”匹配“$1”

匹配结果:

posted @ 2017-07-07 10:48  CharyGao  阅读(...)  评论(...编辑  收藏

返回顶部返回顶部