正则引擎分为两种:DFA(确定型有穷自动机)和NFA(非确定型有穷自动机)。两类引擎要顺利工作,都必须有一个正则式和一个文本串。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:“某年某月某日在某处匹配上了!”,然后接着往下匹配。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。
一、两种正则引擎有两条普适规则:
- 优先选择最左端(最靠开头)的匹配结果。
- 标准的匹配量词*、+、?、{m,n}是匹配优先的。
规则1举例:如果用cat去匹配The drading belly indicates that your cat is too fat.
- indicates中的cat会优先被匹配到,而不会匹配cat单词。
规则2举例:用[0-9]+匹配字符串March19985
- 该字符串中的数字19985全部都会被匹配。
[0-9]+的意思是“匹配数字,且数字出现的次数至少为一次。其实匹配到March1时就已经算是匹配通过了,但是引擎还是继续匹配一直到数字结束,这就是"匹配优先"的意思。换句话说,这些匹配优先的元字符都有一个特点,就是尽可能多的匹配字符。
二、过度匹配优先
(.*)(.*)
由于匹配优先的规则,第二个括号是不会匹配到任何字符的。如果用perl的$2来取值,$2是取不到任何值的。
用^.*([0-9][0-9])去匹配字符串about24characters
整个匹配流程是:
- .*先将整个字符串都匹配到,匹配第一个[0-9]时要求.*释放一个字符’s’,但是字符’s’并不能让[0-9]匹配,所以还会继续释放,直到释放到字符’4’为止。
- 但是此时,第二个[0-9]还无法匹配,为了匹配整个正则表达式,表达式.*必须再释放一个字符,这次是’2’,由第一个[0-9]匹配,现在’4’能够由第二个[0-9]匹配。
三、表达式主导和文本主导
以to(nite|knight|night)匹配文本…tonight…的过程为例:
- 表达式主导:表达式的第一个部分是t,它会重复尝试,直到在字符串中找到t,之后就检查随后的o,如果能匹配就继续检查下面的元素。这个例子中,下面的元素是(nite|knight|night),意思是nite或者knight或者night,引擎会依次尝试这三种可能。
- 文本主导:当引擎读入文本t时,记录匹配的位置是to(nite|knight|night);接着读入o,匹配位置to(nite|knight|night);读入n,匹配位置to(nite|knight|night),两个位置,knight被淘汰出局;读到i时,就不会再尝试匹配knight了。
整个过程,控制权在表达式的元素之间转换,因此被称之为“表达式主导”。“表达式主导”的特点是每个子表达式都是独立的,不存在内在联系。“文本主导”被叫做文本主导是因为被扫描的字符串,控制了引擎的执行过程。
NFA具有表达式主导的特性,引擎的匹配原理就非常重要。通过改变表达式的编写方式,会有不同的效果。NFA为创造性思维提供了丰富的施展空间。
四、回溯一些相关概念
- 回溯
- 匹配优先
- 忽略优先
以下分别进行解释:
1.回溯:NFA引擎会依次处理各个子表达式或组成元素,遇到需要再两个可能成功的可能中进行选择的时候,它会选择其一,同时记住另一个,以备稍后可能的需要。无论选择哪一种途径,如果它能匹配成功,而且正则表达式的余下部分也成功了,匹配即告完成。如果正则表达式余下部分最终匹配失败,引擎会知道需要回溯到之前做出选择的地方,选择其他的备用分支继续尝试。
- 如果在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,而对于忽略优先量词,会选择“跳过尝试”。
- 引擎会把可能回溯的位置状态保存在备用状态序列里。我们需要了解回溯使用的是哪个之前保存的分支,答案是距离当前最近存储的选项就是本地失败强制回溯时返回的。使用的原则是LIFO(last in first out,后进先出)。
2.匹配优先:?是匹配优先的。以ab?c为例匹配ac为例。
- 当字符串abc中的a被匹配时,引擎匹配的位置是:a ↑ \uparrow ↑b?c(此时备用状态序列中没有备用的位置)
- 此时轮到用正则中的b?去匹配字符,由于?是匹配优先的,此时引擎直接用正则中的b去匹配字符串中的c。同时将ab? ↑ \uparrow ↑c存储到备用状态序列里。(此时备用状态序列中的备用位置是ab? ↑ \uparrow ↑c)
- 用b?去匹配c,结果必然失败,然后引擎会返回状态序列中的备用位置ab? ↑ \uparrow ↑c,然后从此位置开始(忽略了b),用正则里面的c匹配字符串里面的c。匹配成功。
- 匹配成功后,引擎删除备用状态里面存储的全部备用位置。
3.忽略优先:??是忽略优先的。以ab??c为例匹配abc为例。
- 当字符串abc中的a被匹配时,引擎匹配的位置是:a ↑ \uparrow ↑b??c(此时备用状态序列中没有备用的位置)
- a匹配完之后,轮到匹配b??了,由于??是忽略优先的,此时引擎匹配的位置变为ab?? ↑ \uparrow ↑c。但是会将a ↑ \uparrow ↑b??c存储到备用状态序列里。(此时备用状态序列中的备用位置是a ↑ \uparrow ↑b??c)
- 此时引擎会用正则的c去匹配字符串的b,匹配失败。
- 然后引擎会回到在备用状态中保存的a ↑ \uparrow ↑b??c,尝试用正则中的b匹配字符串中的b。匹配成功。
- 直到字符串所有的字符都被匹配成功后,引擎会删除备用状态里面存储的全部备用位置。
4.补充:
- DFA不支持忽略优先。
- 正则x*相当于x?x?x?x?……,其中x?的数量正好是要匹配的字符串的长度。在每次测试星号作用的元素之前,如果测试失败,还能从保存的状态开始匹配,这个过程不断重复,直到包含星号的尝试完全失败为止。
- 如果用正则表达式[0-9]+来匹配字符串a1234num,[0-9]遇到4之后无法匹配,此时+能够回溯的位置有四个备用状态:a1$\uparrow 234 n u m 、 a 12 234num、a12 234num、a12\uparrow 34 n u m 、 a 123 34num、a123 34num、a123\uparrow 4 n u m 、 a 1234 4num、a1234 4num、a1234\uparrow n u m , 也 就 是 说 , 每 个 位 置 , [ 0 − 9 ] 的 尝 试 都 代 表 一 种 可 能 。 在 [ 0 − 9 ] 匹 配 失 败 时 , 引 擎 会 回 溯 到 最 近 保 存 的 状 态 ( 满 足 后 进 先 出 ) a 1234 num,也就是说,每个位置,[0-9]的尝试都代表一种可能。在[0-9]匹配失败时,引擎会回溯到最近保存的状态(满足后进先出)a1234 num,也就是说,每个位置,[0−9]的尝试都代表一种可能。在[0−9]匹配失败时,引擎会回溯到最近保存的状态(满足后进先出)a1234\uparrow$num,此时匹配结束。
- 前面提到的过度匹配优先的第二个例子^.*([0-9][0-9]),中提到,当匹配到第一个[0-9]时,释放字符的过程就是回溯的体现。
- 回溯就是引擎匹配失败后的退路。
五、固化分组
- 语法:(?>……)
- 意义:使用固化分组的正则匹配与正常的匹配并无差别,但是如果匹配能够进行到此结构之后(闭括号结束之后),那么此结构中的所有备用状态都会被抛弃(也可以理解为被锁定,成为不可用状态)。换句话说,在固化分组结束以时,他已经匹配的文本已经固化为一个单元,只能作为整体而保留或者放弃。括号内的子表达式中未尝试过的备用状态都不复存在了,所以回溯永远也不能选择其中的状态。