随风而行

^o^ 格言:相信没有做不到的事情,只有想不到的事情.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

正则表达式 Regular Expression

Posted on 2009-04-17 22:09  随风而行  阅读(222)  评论(0编辑  收藏  举报

字符的表示
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>
注意上面表格中第二个例子的表达式与\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)。

 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的性能从多项式优化到线形关系,这完全取决于正则式的写法。