正则表达式 Regular Expression
字符的表示
1. 普通字符,特殊字符:
特殊字符:.|*?+(){}[]^$\,相当于语言的关键字,这些字符前面加转义符"\"表示字符本身,否则就作为正则表达式特殊用途字符。
特殊转义字符:下表主要针对.Net的正则表达式
除了上面的特殊字符之外,其它普通字符都直接去匹配输入文本串中的字符。
2. 字符枚举:中括号括起来,例如[abc]表示出现a, b, c中任意一个字符都可以;[^abc]则匹配除了a, b, c之外的任意一个字符。
3. 字符范围:[a-zA-Z0-9]。
4. 综合表示:
Alternation, Condition Constructs(等价、条件式结构)
1. Alternation Construct: (pattern1)|(pattern2),子表达式1或者2任意一个匹配就匹配成功。
2. Condition Construct: (?(expression)(patternYes)|(patternNo)),如果符合expression,则使用patternYes子表达式进行匹配,否则使用patternNo进行匹配。expression也可以是backreference中的group名字,backreference后面详细讲述。这个语法形式是.Net正则表达式的,其它正则表达式引擎也有支持条件式结构的,但语法可能不一样。
例如正则式(?(\d{4})((19|20)\d{2})|(\d{2}))匹配文本串"2004年 98年",结果是"2004", "98",其中"2004"是使用patternYes部分匹配出来的,而"98"是使用patternNo部分。
Quantifier(量词)
指表达式需要重复匹配多少次。
* 0或多次
+ 1或多次
? 作为量词时表示0或1次,在其它量词后面出现时作为greedy, lazy/non-greedy模式开关。
{n} n次
{n,} 最少n次
{n,m} 最少n次,最多m次
*?, +?, {n}?, {n,}?, {n,m}? 开启lazy/non-greedy模式,例如{n,m}表示最少n次,最多m次,在这个范围内尽可能少的匹配。
greedy, lazy/non-greedy模式参考下面相关部分。
Zero-Width Assertions(零宽度断言)
这种结构产生的匹配结果长度为0(所以称作零宽度),只是用于对上下文环境做判断(所以称作断言)。
^ 如果使用Multiline选项,匹配每一行的开始位置;如果使用Singleline选项,匹配整个字符串开始位置。^如果出现在[]中就不是Zero-Width Assertion了。
$ 如果使用Multiline选项,匹配每一行的结束位置(\n之前);如果使用Singleline选项,匹配整个字符串结束位置。
\A 忽略Multiline选项(相当于取消Multiline选项并设置Singleline选项),匹配整个字符串开始位置。
\Z 忽略Multiline选项,匹配整个字符串结束位置,中间的\n不会考虑。.Net中\z和\Z的效果完全一样,不知道是bug还是怎么回事。
前面提到的条件式结构,以及\b如果不是出现在[]中,都是Zero-Width Assertions;后面将会讲到的lookahead, lookbehind也是一种Zero-Width Assertion。
Multiline, Singleline等,参考正则表达式选项部分。
Greedy, Lazy/Non-greedy(贪婪模式,惰性模式)
在使用quantifier量词修饰,需要执行重复n次的匹配中,greedy模式尽可能多的匹配更多字符,而lazy/non-greedy模式则尽可能少的匹配字符。
NFA(参考后面NFA, DFA部分)默认都是采用greedy模式,而当今主要正则表达式引擎都是NFA,因此注意greedy模式的影响。将greedy模式改为non-greedy,在量词修饰符后面添加"?"实现。例如NFA中\w+为greedy模式,\w+?为non-greedy模式。
文本串为:"dxxxdxxxd",greedy模式:
文本串为:"dxxxdxxxd",non-greedy模式:
Group, Back Reference(分组、反向引用)
正则表达式引擎不仅记录最终的匹配结果,使用()括起来的子表达式匹配到的文本串,在匹配过程中也会记录下来。在.Net中,可以使用Match.Groups访问某个匹配结果的所有分组。
没有指定名称的分组称为匿名分组,对所有的匿名分组都默认有一个组号。.Net中组号为0的分组是整个正则表达式,而不管这个正则表达式是否使用()括起来了。对于其它的匿名分组,根据左括号出现的先后位置依次从1开始编号。例如表达式((abc)\d+)?(xyz)(.*)总共有5个分组,依次为0:((abc)\d+)?(xyz)(.*), 1:((abc)\d+), 2:(abc), 3:(xyz), 4:(.*)。
可以对分组命名,.Net中命名方法:(?<groupName>patterns)。如果存在命名分组,则所有命名分组的编号将从最后一个匿名分组的位置开始,依次递增。例如表达式((?<group1>abc)\d+)?(?<group2>xyz)(.*)的5个分组依次为0:((?<group1>abc)\d+)?(?<group2>xyz)(.*), 1:((?<group1>abc)\d+), 2:(.*), 3:(?<group1>abc), 4:(?<group2>xyz)。
在表达式的后面部分引用前面的某一个子表达式分组叫做反向引用。
对于匿名分组的反向引用方法是"\" 加上分组编号,对于命名分组,.Net中的引用方法是\k<groupName>。
注意上面表格中第二个例子的表达式与\w{5,}的区别,(\w)\1{4,}表示同一个字符重复至少5次以上,而\w{5,}表示连续5个以上字符(不必是同一个字符)。
可以禁止正则表达式记录某个分组的匹配结果,这样的分组也就不会被编号,无法被反向引用。禁止分组使用(?:patterns),例如表达式abc(?:\w{3})(\d+)abc总共有两个分组,组号0为整个表达式,组号1为(\d+)。
.Net中,\1到\9总是被当作反向引用;\12这种类型,如果存在编号为12的分组,则作为反向引用,否则将\12转义为ASCII字符,为了消除这种歧意,可以使用\k<n>这种方式。
注意:.Net中分组命名时尖括号<, >可以使用单引号代替。
Lookahead, Lookbehind(正向预搜索、反向预搜索)
匹配当前的某一个子表达式时,可能需要根据前面或者后面出现的字符进行判断(即上下文环境),lookahead、lookbehind就是用于这个目的。
NFA以文本串作为有穷输入字符集Σ,从文本串逐个读取字符进行匹配,所以沿着字符读取方向的是lookahead,与字符读取方向相反则为lookbehind。
lookahead: (?=patterns), (?!patterns)。lookbehind: (?<=patterns), (?<!patterns)。
Options(正则表达式选项)
JavaScript的正则表达式,使用/gi这样的开关控制正则表达式选项 。.Net中可以使用RegexOptions枚举进行全局设置,可以在分组表达式中使用(?imnsx-imnsx:patterns)方式,在这个分组内开启或禁用某些选项,也可以在表达式的中间使用(?imnsx-imnsx),从中间这个位置开始开启或禁用某些选项。全局RegexOptions的优先级低于嵌入方式。
嵌入方式中imnsx表示打开某种选项或选项的组合,前面添加减号"-"表示关闭这些选项。例如(?ix-ms)表示从这个位置开始,打开IgnoreCase、IgnorePatternWhiteSpace选项,关闭Multiline、Singleline选项。
嵌入方式修改正则表达式选项,也叫做Modifier。
NFA, DFA
正则表达式引擎的两种类型,NFA: Nondeterministic Finite Automata, DFA: Deterministic Finite Automaton。
NFA基于正则表达式去匹配文本(文本作为有穷字母表Σ),而DFA是基于文本去匹配正则表达式。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。把多吃的字符吐出部分重新匹配的过程叫做backtracking(回溯)。
.Net中可以使用(?>patterns),禁止对这个子表达式进行回溯,即对输入字符backtracking过程中,一旦遇到这个子表达式已经匹配的字符,就停止backtracking。下面示例演示了这个效果:
NFA、DFA主要对比:
1. DFA对文本串只扫描一次,速度快(时间与有穷字母表Σ的大小成线性关系),但特性少;NFA需反复扫描文本串,速度慢,但特性多。目前主要正则表达式引擎基本都是NFA,例如Perl、Java、.Net、Python、Td、Emacs,DFA的引擎有awk、egrep、lex。
2. NFA最左子正则式优先匹配,DFA是最长左子正则式优先匹配。
3. 只有NFA支持lazy、backtracking、backreference,NFA缺省使用greedy模式,NFA可能陷入递归陷阱导致性能极差。DFA只包含有穷状态,匹配过程中无法捕获子表达式(分组)的匹配结果,因此也无法支持backreference。
有另一种NFA引擎,叫做POSIX NFA引擎。传统NFA在backtracking时,只要当前位置上的最左子正则式匹配成功就停止;而POSIX NFA会继续尝试backtracking,以试图像DFA一样找到最长左子正则式。因此POSIX NFA速度更慢。
详细的NFA、DFA定义、算法,参考编译原理。
附加说明
1. 正则表达式在发展过程中,形成了很多版本的引擎,最基本的为grep,为了使grep具备更多特性而扩展出egrep, 目前使用的大多数正则引擎都是backtracking型的NFA。不同的正则表达式引擎之间,实现上多少也都有些差别,并且开发商还可能作出特有的扩展、语法形式等。 因此,这就意味着并不是同一个正则表达式就会适用于所有的环境,例如.Net中的正则表达式,就不一定能在Java、Python、Unix中工作,这在网上查找正则表达式资源时需要注意。
2. 使用传统NFA,书写正则表达式需要特别注意性能问题,否则很容易导致死循环、性能极差等情况。
对正则表达式依赖性比较强的系统(大量使用正则做搜索匹配),最好完全掌握NFA->DFA算法,充分理解所使用的正则表达式引擎的原理和特性。
可以通过减少表达式中的模糊匹配、限制回溯等方法,将传统NFA的性能从多项式优化到线形关系,这完全取决于正则式的写法。
参考:
1. MSDN Regular Expression Language Elements
2. MSDN Details of Regular Expression Behavior
3. Python Library Reference Regular Expression Syntax
4. 孟岩《理解正则表达式》
5. 龙的天空 《揭开正则表达式的神秘面纱》
1. 普通字符,特殊字符:
特殊字符:.|*?+(){}[]^$\,相当于语言的关键字,这些字符前面加转义符"\"表示字符本身,否则就作为正则表达式特殊用途字符。
特殊转义字符:下表主要针对.Net的正则表达式
Escape sequence | Character code | Meaning |
\a |
0x07 | 响铃字符 |
\f |
0x0C | Form feed. |
\n |
0x0A | 换行符 |
\r |
0x0D | 回车符 |
\t |
0x09 | Tab character. |
\v |
0x0B | Vertical tab. |
\e |
0x1B | ASCII的Esc字符 |
\b | 0x08 |
1. 在[]里面出现时表示删除键的ASCII字符(Backspace) 2. 不是出现在[]里面,则表示单词的边界,例如表达式\b\w+\b匹配文本串"Hi <strong>Hello Kitty</strong>的结果是:"Hi", "strong", "Hello", "Kitty", "strong"。 |
\0dd |
0dd | An octal character code, where dd is one or more octal digits. |
\xXX |
0xXX | A hexadecimal character code, where XX is one or more hexadecimal digits. |
\u0020 |
|
Matches a Unicode character using hexadecimal representation (exactly four digits). |
\cZ |
Ctl+Z | Matches an ASCII control character; for example, \cC is control-C. |
2. 字符枚举:中括号括起来,例如[abc]表示出现a, b, c中任意一个字符都可以;[^abc]则匹配除了a, b, c之外的任意一个字符。
3. 字符范围:[a-zA-Z0-9]。
4. 综合表示:
\w |
所有大小写英文字母、数字字符、下划线,等同于[a-zA-Z0-9_] |
\W |
除了\w之外的所有字符,等同于[^a-zA-Z0-9_],包括空白字符、各种特殊字符等 |
\s |
所有空白字符,等同于[\t\n\r\f\v] |
\S |
除了\s之外的所有字符,等同于[^\t\n\r\f\v] |
\d |
所有的数字字符,等同于[0-9] |
\D |
所有的非数字字符,等同于[^0-9] |
\A |
匹配字符串的开始位置(不代表任何字符,匹配结果长度为0) |
\Z |
匹配字符串的结束位置(不代表任何字符,匹配结果长度为0) |
. |
在DOTALL模式下,表示任何字符(包括空白字符、各种特殊字符等),等同于[\w\W]、[\s\S]、[\d\D]等 在非DOTALL模式下,表示除了换行符之外的所有字符 DOTALL模式,参考下面正则表达式选项部分 |
Alternation, Condition Constructs(等价、条件式结构)
1. Alternation Construct: (pattern1)|(pattern2),子表达式1或者2任意一个匹配就匹配成功。
2. Condition Construct: (?(expression)(patternYes)|(patternNo)),如果符合expression,则使用patternYes子表达式进行匹配,否则使用patternNo进行匹配。expression也可以是backreference中的group名字,backreference后面详细讲述。这个语法形式是.Net正则表达式的,其它正则表达式引擎也有支持条件式结构的,但语法可能不一样。
例如正则式(?(\d{4})((19|20)\d{2})|(\d{2}))匹配文本串"2004年 98年",结果是"2004", "98",其中"2004"是使用patternYes部分匹配出来的,而"98"是使用patternNo部分。
Quantifier(量词)
指表达式需要重复匹配多少次。
* 0或多次
+ 1或多次
? 作为量词时表示0或1次,在其它量词后面出现时作为greedy, lazy/non-greedy模式开关。
{n} n次
{n,} 最少n次
{n,m} 最少n次,最多m次
*?, +?, {n}?, {n,}?, {n,m}? 开启lazy/non-greedy模式,例如{n,m}表示最少n次,最多m次,在这个范围内尽可能少的匹配。
greedy, lazy/non-greedy模式参考下面相关部分。
Zero-Width Assertions(零宽度断言)
这种结构产生的匹配结果长度为0(所以称作零宽度),只是用于对上下文环境做判断(所以称作断言)。
^ 如果使用Multiline选项,匹配每一行的开始位置;如果使用Singleline选项,匹配整个字符串开始位置。^如果出现在[]中就不是Zero-Width Assertion了。
$ 如果使用Multiline选项,匹配每一行的结束位置(\n之前);如果使用Singleline选项,匹配整个字符串结束位置。
\A 忽略Multiline选项(相当于取消Multiline选项并设置Singleline选项),匹配整个字符串开始位置。
\Z 忽略Multiline选项,匹配整个字符串结束位置,中间的\n不会考虑。.Net中\z和\Z的效果完全一样,不知道是bug还是怎么回事。
前面提到的条件式结构,以及\b如果不是出现在[]中,都是Zero-Width Assertions;后面将会讲到的lookahead, lookbehind也是一种Zero-Width Assertion。
Multiline, Singleline等,参考正则表达式选项部分。
Greedy, Lazy/Non-greedy(贪婪模式,惰性模式)
在使用quantifier量词修饰,需要执行重复n次的匹配中,greedy模式尽可能多的匹配更多字符,而lazy/non-greedy模式则尽可能少的匹配字符。
NFA(参考后面NFA, DFA部分)默认都是采用greedy模式,而当今主要正则表达式引擎都是NFA,因此注意greedy模式的影响。将greedy模式改为non-greedy,在量词修饰符后面添加"?"实现。例如NFA中\w+为greedy模式,\w+?为non-greedy模式。
文本串为:"dxxxdxxxd",greedy模式:
Regular Expression | Result |
(d)(\w+) | "\w+"将匹配第一个"d"之后的所有字符"xxxdxxxd" |
(d)(\w+)(d) | "\w+"将匹配第一个"d"和最后一个"d"之间的所有字符"xxxdxxx"。虽然"\w+"也能够匹配上最后一个"d",但是为了使整个表达式匹配成功,"\w+"可以"让出"它本来能够匹配的最后一个"d" |
文本串为:"dxxxdxxxd",non-greedy模式:
Regular Expression | Result |
(d)(\w+?) | "\w+?"将尽可能少的匹配第一个"d"之后的字符,结果是:"\w+?"只匹配了一个"x"(第二个字符) |
(d)(\w+?)(d) | 为了让整个表达式匹配成功,"\w+?"不得不匹配"xxx"才可以让后边的"d"匹配,从而使整个表达式匹配成功。因此,结果是:"\w+?"匹配"xxx" |
Group, Back Reference(分组、反向引用)
正则表达式引擎不仅记录最终的匹配结果,使用()括起来的子表达式匹配到的文本串,在匹配过程中也会记录下来。在.Net中,可以使用Match.Groups访问某个匹配结果的所有分组。
没有指定名称的分组称为匿名分组,对所有的匿名分组都默认有一个组号。.Net中组号为0的分组是整个正则表达式,而不管这个正则表达式是否使用()括起来了。对于其它的匿名分组,根据左括号出现的先后位置依次从1开始编号。例如表达式((abc)\d+)?(xyz)(.*)总共有5个分组,依次为0:((abc)\d+)?(xyz)(.*), 1:((abc)\d+), 2:(abc), 3:(xyz), 4:(.*)。
可以对分组命名,.Net中命名方法:(?<groupName>patterns)。如果存在命名分组,则所有命名分组的编号将从最后一个匿名分组的位置开始,依次递增。例如表达式((?<group1>abc)\d+)?(?<group2>xyz)(.*)的5个分组依次为0:((?<group1>abc)\d+)?(?<group2>xyz)(.*), 1:((?<group1>abc)\d+), 2:(.*), 3:(?<group1>abc), 4:(?<group2>xyz)。
在表达式的后面部分引用前面的某一个子表达式分组叫做反向引用。
对于匿名分组的反向引用方法是"\" 加上分组编号,对于命名分组,.Net中的引用方法是\k<groupName>。
Regular Expression | Input String | Result |
('|")(.*?)(\1) | 'hello' "world" |
1. 'hello' 2. "world" |
(\w)\1{4,} | aa bbbb abcdefg ccccc 111121111 999999999 |
1. ccccc 2. 99999999 |
<(?<tag1>\w+)>[\w\W]*</\k<tag1>> | <strong>Hello Kitty</strong> is the name | <strong>Hello Kitty</strong> |
可以禁止正则表达式记录某个分组的匹配结果,这样的分组也就不会被编号,无法被反向引用。禁止分组使用(?:patterns),例如表达式abc(?:\w{3})(\d+)abc总共有两个分组,组号0为整个表达式,组号1为(\d+)。
.Net中,\1到\9总是被当作反向引用;\12这种类型,如果存在编号为12的分组,则作为反向引用,否则将\12转义为ASCII字符,为了消除这种歧意,可以使用\k<n>这种方式。
注意:.Net中分组命名时尖括号<, >可以使用单引号代替。
Lookahead, Lookbehind(正向预搜索、反向预搜索)
匹配当前的某一个子表达式时,可能需要根据前面或者后面出现的字符进行判断(即上下文环境),lookahead、lookbehind就是用于这个目的。
NFA以文本串作为有穷输入字符集Σ,从文本串逐个读取字符进行匹配,所以沿着字符读取方向的是lookahead,与字符读取方向相反则为lookbehind。
lookahead: (?=patterns), (?!patterns)。lookbehind: (?<=patterns), (?<!patterns)。
|
Regular Expression | Input String | Result |
lookahead | Windows (?=NT|XP) | Windows 98, Windows NT, Windows 2000 |
仅匹配"Windows NT"中的"Windows " 解释:匹配"Windows ",后面必须是字符"NT"或者"XP" |
lookahead | (\w)((?=\1\1\1)(\1))+ | aaa ffffff 999999999 |
匹配6个"f"的前4个以及9个"9"的前7个 解释:以6个f的地方为例,第一个f匹配(\w)。接下来是一个子表达式((?=\1\1\1)(\1)),需要出现1次以上。这个子表达式由两个部分组成,第一部分(?=\1\1\1)是一个lookahead,可以把它跟最后那个(\1)放在在一起考虑,所以对于第4个f,能够匹配(\1),也满足它的前面一个位置(第三个f)后面还有三个f(第4、5、6三个) |
lookahead | do(?!\w) | done, do, dog |
只匹配"do,"这个位置上的"do" 解释:匹配"do",后面不能有任何[a-zA-Z0-9_]这些字符 |
lookbehind | (?<=\d{4})\d+(?=\d{4}) | 1234567890123456 | "56789012" |
Options(正则表达式选项)
JavaScript的正则表达式,使用/gi这样的开关控制正则表达式选项 。.Net中可以使用RegexOptions枚举进行全局设置,可以在分组表达式中使用(?imnsx-imnsx:patterns)方式,在这个分组内开启或禁用某些选项,也可以在表达式的中间使用(?imnsx-imnsx),从中间这个位置开始开启或禁用某些选项。全局RegexOptions的优先级低于嵌入方式。
嵌入方式中imnsx表示打开某种选项或选项的组合,前面添加减号"-"表示关闭这些选项。例如(?ix-ms)表示从这个位置开始,打开IgnoreCase、IgnorePatternWhiteSpace选项,关闭Multiline、Singleline选项。
嵌入方式修改正则表达式选项,也叫做Modifier。
RegexOption member | Inline character | Description |
None | N/A | Specifies that no options are set. |
IgnoreCase | i | 匹配过程中忽略大小写因素 |
Multiline | m | 在Multiline模式下,^和$分别匹配每一行的开始和结束位置;否则将分别匹配整个文本串的开始和结束位置 |
ExplicitCapture | n | 匹配过程中不捕获任何匿名分组,相当于在表达式中对所有匿名分组使用(?:) |
Compiled | N/A | 将正则表达式预编译到assembly中,提高匹配性能 |
Singleline | s | 也就是DOTALL模式的开关,打开Sigleline开关,.将匹配任何一个字符,否则.只匹配换行符以外的字符 |
IgnorePatternWhitespace | x |
忽略表达式中没有转义的空白字符(\s),并开启单个未#符号的注释方式 这个选项开启后,表达式中从未转义的#符号开始,到这一行的结束位置都作为注释对待 表达式中另外一种注释方式为(#your comments),只有括号里面的部分才是注释内容 |
RightToLeft | N/A |
对输入字符集的默认扫描方向为从左至右,该选项将扫描方向修改为从右至左
该选项只是改变了对输入字符集的扫描方向,它并不会改变表达式对子文本串的匹配方向,lookahead、 lookbehind的方向也不会改变,lookahead仍然向右搜索,lookbehind仍然向左搜索 |
ECMAScript | N/A | ... |
CultureInvariant | N/A | ... |
NFA, DFA
正则表达式引擎的两种类型,NFA: Nondeterministic Finite Automata, DFA: Deterministic Finite Automaton。
NFA基于正则表达式去匹配文本(文本作为有穷字母表Σ),而DFA是基于文本去匹配正则表达式。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。把多吃的字符吐出部分重新匹配的过程叫做backtracking(回溯)。
.Net中可以使用(?>patterns),禁止对这个子表达式进行回溯,即对输入字符backtracking过程中,一旦遇到这个子表达式已经匹配的字符,就停止backtracking。下面示例演示了这个效果:
Regular Expression | Input String | Result |
(\w)(\1*)(a) | aaa ffffff 999999a999 |
1. aaa 2. 999999a |
(\w)(?>\1*)(a) | aaa ffffff 999999a999 | 只有999999a 因为在对aaa的匹配过程中,最后一个a被\1*匹配上,但又不允许回溯,所以在读取aaa后面的那个空格字符后,发现跟子正则式(a)不匹配 |
NFA、DFA主要对比:
1. DFA对文本串只扫描一次,速度快(时间与有穷字母表Σ的大小成线性关系),但特性少;NFA需反复扫描文本串,速度慢,但特性多。目前主要正则表达式引擎基本都是NFA,例如Perl、Java、.Net、Python、Td、Emacs,DFA的引擎有awk、egrep、lex。
2. NFA最左子正则式优先匹配,DFA是最长左子正则式优先匹配。
3. 只有NFA支持lazy、backtracking、backreference,NFA缺省使用greedy模式,NFA可能陷入递归陷阱导致性能极差。DFA只包含有穷状态,匹配过程中无法捕获子表达式(分组)的匹配结果,因此也无法支持backreference。
有另一种NFA引擎,叫做POSIX NFA引擎。传统NFA在backtracking时,只要当前位置上的最左子正则式匹配成功就停止;而POSIX NFA会继续尝试backtracking,以试图像DFA一样找到最长左子正则式。因此POSIX NFA速度更慢。
Engine | Regular Expression | Input String | Result |
NFA | perl|perlman | perlman book | perl |
NFA | perlman|perl | perlman book | perlman |
DFA | perl|perlman | perlman book | perlman |
详细的NFA、DFA定义、算法,参考编译原理。
附加说明
1. 正则表达式在发展过程中,形成了很多版本的引擎,最基本的为grep,为了使grep具备更多特性而扩展出egrep, 目前使用的大多数正则引擎都是backtracking型的NFA。不同的正则表达式引擎之间,实现上多少也都有些差别,并且开发商还可能作出特有的扩展、语法形式等。 因此,这就意味着并不是同一个正则表达式就会适用于所有的环境,例如.Net中的正则表达式,就不一定能在Java、Python、Unix中工作,这在网上查找正则表达式资源时需要注意。
2. 使用传统NFA,书写正则表达式需要特别注意性能问题,否则很容易导致死循环、性能极差等情况。
对正则表达式依赖性比较强的系统(大量使用正则做搜索匹配),最好完全掌握NFA->DFA算法,充分理解所使用的正则表达式引擎的原理和特性。
可以通过减少表达式中的模糊匹配、限制回溯等方法,将传统NFA的性能从多项式优化到线形关系,这完全取决于正则式的写法。
参考:
1. MSDN Regular Expression Language Elements
2. MSDN Details of Regular Expression Behavior
3. Python Library Reference Regular Expression Syntax
4. 孟岩《理解正则表达式》
5. 龙的天空 《揭开正则表达式的神秘面纱》