正则表达式
是什么?
正则表达式就是用于描述查找字符串符合的某些复杂规则的工具。换句话说,正则表达式就是记录文本规则的代码。是用来进行文本匹配的工具。不同的环境下正则表达式的一些细节是不相同的
基本概念
元字符(metacharacter)
- \b:代表着单词的开头或结尾,也就是单词的分界处,它并不匹配单词分隔字符中的任何一个,它只匹配一个位置。\b匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w
- .:匹配除了换行符以外的任意字符
- *:同样是元字符,不过它代表是数量——它指定*前边的内容可以连续重复使用任意次以使整个表达式得到匹配;.*连在一起就意味着任意数量的不包含换行的字符
- \d:匹配一位数字(0~9)
- \s:匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等
- \w:匹配字母或数字或下划线或汉字等
- +:和*类似的元字符,不同的是*匹配重复任意次(可能是0次),而+则匹配重复1次或更多次
- ^:匹配字符串的开始
- $:匹配字符串的结束
注: 和忽略大小写的选项类似,有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项,^和$的意义就变成了匹配行的开始处和结束处。
字符转义
使用\来取消这些字符的特殊意义,如:\.、\*、\\等
重复
正则表达式中所有的限定符(指定数量的代码,例如*,{5,12}等):
- * 重复零次或更多次
- + 重复一次或更多次
- ? 重复零次或一次
- {n} 重复n次
- {n,} 重复n次或更多次
- {n,m} 重复n到m次
字符类
如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办?
很简单,你只需要在方括号里列出它们就行了,像[aeiou]就匹配任何一个英文元音字母,[.?!]匹配标点符号(.或?或!)。
也可以指定范围,如:
[0-9]代表的含意与\d就是完全一致的:一位数字;
同理[a-z0-9A-Z_]也完全等同于\w(如果只考虑英文的话)。
分支条件
指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用|把不同的规则分隔开,如:
- 0\d{2}-\d{8}|0\d{3}-\d{7}
- (0\d{2})[- ]?\d{8}|0\d{2}[- ]?\d{8}
使用分枝条件时,要注意各个条件的顺序。,如\d{5}-\d{4}|\d{5},如果你把它改成\d{5}|\d{5}-\d{4}的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。
分组
用小括号来指定子表达式(也叫做分组)
反义
有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义:
- \W 匹配任意不是字母,数字,下划线,汉字的字符
- \S 匹配任意不是空白符的字符
- \D 匹配任意非数字的字符
- \B 匹配不是单词开头或结束的位置
- [^x] 匹配除了x以外的任意字符
- [^aeiou] 匹配除了aeiou这几个字母以外的任意字符
简单例子
- \bhi\b.*\bLucy\b
- 0\d{2}-\d{8}
- \ba\w*\b
- \d+
- \b\w{6}\b
- ^\d{5,12}$
- (?0\d{2}[) -]?\d{8}
- (\d{1,3}.){3}\d{1,3}
常用分组语法
-
捕获:
- (exp):匹配exp,并捕获文本到自动命名的组里
- (?<name>exp):匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp)
- (?:exp):匹配exp,不捕获匹配的文本,也不给此分组分配组号
-
零宽断言:
- (?=exp):匹配exp前面的位置
- (?<=exp):匹配exp后面的位置
- (?!exp):匹配后面跟的不是exp的位置
- (?<!exp):匹配前面不是exp的位置
-
注释:
- (?#comment):这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读
后向引用
使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。
后向引用用于重复搜索前面某个分组匹配的文本。例如,\1代表分组1匹配的文本。例如:\b(\w+)\b\s+\1\b
你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?<Word>\w+)(或者把尖括号换成'也行:(?'Word'\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容,你可以使用\k<Word>,所以上一个例子也可以写成这样:\b(?<Word>\w+)\b\s+\k<Word>\b。
注:
- 分组0对应整个正则表达式
- 实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号
- 你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权.
零宽断言
用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。
- (?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I'm singing while you're dancing.时,它会匹配sing和danc。
- (?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找reading a book时,它匹配ading。
例如:((?<=\d)\d{3})+\b用它对1234567890进行查找时结果是234567890。
负向零宽断言
\b\w*q[^u]\w*\b匹配包含后面不是字母u的字母q的单词。但你会发现,如果q出现在单词的结尾的话,像Iraq,Benq,这个表达式就会出错。这是因为[^u]总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u]将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\w*\b将会匹配下一个单词,于是\b\w*q[^u]\w*\b就能匹配整个Iraq fighting。负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\b\w*q(?!u)\w*\b。
我们可以用(?<!exp),零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp:(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。
一个更复杂的例子:(?<=<(\w+)>).(?=</\1>)*匹配不包含属性的简单HTML标签内里的内容
注释
(?#comment),要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。例如:
(?<= # 断言要匹配的文本的前缀 <(\w+)> # 查找尖括号括起来的内容 # (即HTML/XML标签) ) # 前缀结束 .* # 匹配任意文本 (?= # 断言要匹配的文本的后缀 <\/\1> # 查找尖括号括起来的内容 # 查找尖括号括起来的内容 ) # 后缀结束
其他
贪婪与懒惰
- 贪婪匹配:当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符,如:a.*b匹配aabab得到aabab
- 懒惰匹配:也就是匹配尽可能少的字符,a.*?b应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。
注:为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。
懒惰限定符
- *? 重复任意次,但尽可能少重复
- +? 重复1次或更多次,但尽可能少重复
- ?? 重复0次或1次,但尽可能少重复
- {n,m}? 重复n到m次,但尽可能少重复
- {n,}? 重复n次以上,但尽可能少重复
处理选项
如忽略大小写,处理多行等,这些选项能用来改变处理正则表达式的方式。常见的有:
- IgnoreCase(忽略大小写):匹配时不区分大小写。
- Multiline(多行模式):更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
- Singleline(单行模式):更改.的含义,使它与每一个字符匹配(包括换行符\n)。(注:单行模式其实名叫 dotAll,意为点可以匹配所有字符)
- IgnorePatternWhitespace(忽略空白):忽略表达式中的非转义空白并启用由#标记的注释。
- ExplicitCapture(显式捕获):仅捕获已被显式命名的组。
平衡组/递归匹配
如何把xx <aa <bbb> <bbb> aa> yy这样的字符串里,最长的配对的尖括号内的内容捕获出来?
用到以下的语法构造:
- (?'group'): 把捕获的内容命名为group,并压入堆栈(Stack)
- (?'-group'): 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
- (?(group)yes|no): 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
- (?!): 零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败
我们需要做的是每碰到了左括号,就在压入一个"Open",每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就证明左括号比右括号多,那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。
< #最外层的左括号
[^<>]* #它后面非括号的内容
(
(
(?'Open'<) #左括号,压入"Open"
[^<>]* #左括号后面的内容
)+
(
(?'-Open'>) #右括号,弹出一个"Open"
[^<>]* #右括号后面的内容
)+
)*
(?(Open)(?!)) #最外层的右括号前检查
#若还有未弹出的"Open"
#则匹配失败
> #最外层的右括号
正则表达式元素一览
也可查看关于正则表达式语言元素的MSDN在线文档。
- \a: 报警字符(打印它的效果是电脑嘀一声)
- \b: 通常是单词分界位置,但如果在字符类里使用代表退格
- \t: 制表符,Tab
- \r: 回车
- \v: 竖向制表符
- \f: 换页符
- \n: 换行符
- \e: Escape
- \0nn: ASCII代码中八进制代码为nn的字符
- \xnn: ASCII代码中十六进制代码为nn的字符
- \unnnn: Unicode代码中十六进制代码为nnnn的字符
- \cN: ASCII控制字符。比如\cC代表Ctrl+C
- \A: 字符串开头(类似^,但不受处理多行选项的影响)
- \Z: 字符串结尾或行尾(不受处理多行选项的影响)
- \z: 字符串结尾(类似$,但不受处理多行选项的影响)
- \G: 当前搜索的开头
- \p{name}: Unicode中命名为name的字符类,例如\p{IsGreek}
- (?>exp): 贪婪子表达式
- (?<x>-<y>exp): 平衡组
- (?im-nsx:exp): 在子表达式exp中改变处理选项
- (?im-nsx): 为表达式后面的部分改变处理选项
- (?(exp)yes|no): 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no
- (?(exp)yes): 同上,只是使用空表达式作为no
- (?(name)yes|no): 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no
- (?(name)yes): 同上,只是使用空表达式作为no
补充
主流的正则引擎又分为3类:一、DFA,二、传统型NFA,三、POSIX NFA。
两种引擎的工作方式完全不同,一个(NFA)以表达式为主导,一个(DFA)以文本为主导!一般而论,DFA引擎则搜索更快一些!但是NFA以表达式为主导,反而更容易操纵,因此一般程序员更偏爱NFA引擎! 两种引擎各有所长,而真正的引用则取决与你的需要以及所使用的语言
参考资料
- 正则表达式30分钟入门教程
- 精通正则表达式(第3版)
- 微软的正则表达式教程
- Regex类(微软文档)
- 专业的正则表达式教学网站(英文)
- 关于.Net下的平衡组的详细讨论(英文)
- 正在表达式(百科)
- Jeffrey Friedl 在其著作《Mastering Regular Expressions (2nd edition)》(中文版译作:精通正则表达式,已出到第三版)