资源描述
编写正则表达式小结
版本
修改
修改日期
修改人
1.0.0
初稿
2008-6-14
包能辉
目录
目录 1
前言 1
正则表达式基础 1
字符组及相关字符缩写 2
锚点及其他零长度断言 3
分组及捕获 4
使用正则表达式的例子 5
匹配IP地址 5
匹配对称括号 5
提高正则表达式匹配的效率 5
多选结构 5
非捕获型括号 6
使用固化分组和占有优先量 6
尽量使用\d \w 等元字符 6
非贪婪模式与贪然模式 6
注意事项 8
前言
本文的内容适用于PCRE库,以及由COM组在PCRE基础上封装的public/spreg库。对于使用的PCRE库的php preg 库也有指导意义。
PCRE库是给C/C++语言使用的,在使用常量字符串的时候要注意存在 \ 转义的问题。下面如无特殊说明在使用字符串常量使用 ‘\’ 的时候需要写成”\\”的形式。
正则表达式基础
这里列出了PCRE库支持的正则表达式语法。
字符组及相关字符缩写
1) *: 匹配前一个字符(组)0次或多次
2) +: 匹配前一个字符(组)1次以上
3) ?: 对前一个字符做0次或1次匹配.但是它在与其他正则表达式语法结合的还有其他的特殊意义,后面会详细介绍
4) {n},{n,}, {n, m} :用来指定匹配的次数, {n}:n 次, {n,}: n次以上, {n,m} , 在n次到m次之间
5) . 点号: 默认情况下是匹配除换行符外的所有字符,但使用DOLLAR_ENDONLY选项后,也可以匹配换行符
6) [xyz] : 匹配字符集合。匹配[] 中所包含的任意一个字符。[ATK]可匹配字符 A,T 或K
7) [a-z] : 字符范围 , 可以匹配相应得范围。[\x80-\xFF]可以匹配ASCII码中0x80到0xFF范围中的字符串。
8) [^xyz]: 不匹配 [^]中的字符。 也可以使用于指定范围的情况。[^A]不能匹配A。注意如果 ^ 不出现在[]中的开头,则会把^认为是普遍字符’^’, 这里可以不需要 使用’\\^’
9) \cx 匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
10) \d 匹配一个数字字符。等价于 [0-9]。
11) \D 匹配一个非数字字符。等价于 [^0-9]。
12) \f 匹配一个换页符。等价于 \x0c 和 \cL。
13) \n 匹配一个换行符。等价于 \x0a 和 \cJ。
14) \r 匹配一个回车符。等价于 \x0d 和 \cM。
15) \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
16) \S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
17) \t 匹配一个制表符。等价于 \x09 和 \cI。
18) \v 匹配一个垂直制表符。等价于 \x0b 和 \cK。
19) \w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
20) \W 匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'。
21) \xn ,\x{n}匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,'\x41' 匹配 "A"。'\x041' 则等价于 '\x04' & "1"。正则表达式中可以使用 ASCII 编码。注意:在C/C++中的字符串常量里写成\xn 或 \\xn都可以的,因为 \xn 在C/C++里会被直接转义为相应的字符ASCII, 不过不推荐这样写,因为这样写不小心会出现 ] \ 等正则表达式中专用的字符,造成混乱,见后面的注意事项。
锚点及其他零长度断言
注意:这里列出的情况都不会捕获任何结果,只是作为判断匹配使用。
1) \b : 匹配一个单词边界,也就是指单词和空格间的位置。例如, rd\b 可以匹配word 中的 rd,但不捕获后面的分隔符
2) \B 匹配非单词边界。wo\B 能匹配 word 中的 wo, 但不捕获wo后面的r 。
3) ^: 匹配输入字符串的起始位置,如果打开了多行匹配的开关,也可以匹配一个换行符(\r,\n)后面的位置
4) $: 匹配输入字符串的终止位置,如果打开了多行匹配的开关,可以匹配换行符(\r,\n)前面的位置。
5) \A: 与^类似,但它无论是否打开多行匹配的开关都表示整个目标串的头部
6) \z: 与$类似, 但它无论是否打开多行匹配的开关都表示整个目标串的尾部
7) \Z: 与$意义相同
8) (?:pattern) : 匹配但不捕获结果。许多时候是与 “| “ 联合使用
9) (?=pattern) : 顺序环视,在捕获(?=)中的结果的时候只会匹配相应的位置,但在最终结果里不会表现出来,而(?=)不匹配的时候就会认为整个表达式不能匹配。如 AA(?=BB) 匹配 AABBCC, 在psreg_match_t中的第一位置表示的是 AA , 而不会是AABB, (?=AABB)AA, 匹配的结果是AA, (?=AABB)并不会占据结果,只做在这个位置上是能够匹配成功的标准。
10) (?!pattern) : 否定顺序环视,与顺序环视类似,不过是在(?=)中不能匹配的情况下才认为结果为真。如 AA(?=[0-9]+) 匹配 AABB 可以得到 AA,而不是得到不能匹配的结果。
11) (?<=pattern) : 逆序环视,与顺序环视现相类似,也是只匹配但不捕获。只是方向不同不是以(?<=)为起点而是以它为终点的字符串进行判断。如 (?<=AA)BB, 匹配 AABBCC可以得到BB, AA(?<=AABB) 可以得到 AA。
12) (?<!=pattern) : 否定逆序环视, 与否定顺序环视类似。 如 (?<!=[0-9]+)BB匹配AABBCC可以得到BB, 而 (?<!=AA)BB匹配的结果却是不匹配。
分组及捕获
1) (pattern) : 匹配并捕获对于的子串,在获取psreg_match_t类型从下标1开始的位置后面都是由()包含的结果。
2) \num 返向引用,其中 num 是一个正整数。对()捕获的匹配进行再次引用。例如,(.)\1 匹配两个连续的相同字符。
3) | : 支持分组功能, x|y|z, 匹配x或y,或 z , “AAA|BBB” 可以匹配AAA或BBB
4) *?、+?、??、{n,m}? 忽略优先量词, 在正常情况下正则表达式的匹配遵循贪婪匹配的原则,在使用忽略优先量词的情况,匹配的结果是非贪婪,只有满足条件的下限。
5) *+、++、?+、{n,m}+ 占有优先量词, 它们类似于前面提到固化分组的模式,在匹配的过程中不会“交还”中间的结果,就功能上而言可以使用固化分组进行模拟,不过使用占有优先量词的一个好处是在PCRE中对它的实现做了一些优化。
6) (?>pattern) : 固化分组, 比如 用 A(?>[0-9]+)9 匹配 A789 得到结果会是不匹配,因为按照正则匹配的匹配过程,A([0-9]+)会得到A789, 在最后一个9的时候,([0-9]+)匹配的结果会把最后一个9“交还”出来进行匹配,但固化分组后就不会有“交还”操作。这个在一些条件下能够提高正则表达式的匹配效率。
7) (?P<name> pattern) : 命名捕获。 对于()捕获的结果,需要在捕获结果的数组里用下标进行访问。命名捕获可以支持使用name:value的方式获取捕获结果,不受捕获()的顺序影响。在PCRE库中可以使用pcre_get_named_substring的接口获取捕获的结果。
8) (?R) (?num) (?P>name) : 支持递归。(?R) 表示 “在此处递归应用整个表达式”,而 (?num)表示 “在此处递归应用num对应编号的()序列” 。 (?P>name)则是应用命名捕获的结果。
9) 它们的使用可以参考后面的例子。
10) (?if|then|else) : 条件判断。If 部分的测试如果为真(被匹配)尝试使用then 否则使用else(else部分可以不出现)。如:(?(?<=A)([A-Z]+)|([0-9]+)) 匹配 “B9029ACDE” 可以得到9029,他检查开头是不是A,如果不是选择else分支匹配数字,否则匹配字母
使用正则表达式的例子
匹配IP地址
([01]?\d\d?|2[0-4]\d|25[0-5])\. ([01]?\d\d?|2[0-4]\d|25[0-5])\. ([01]?\d\d?|2[0-4]\d|25[0-5])\. ([01]?\d\d?|2[0-4]\d|25[0-5])
但是这样匹配是有问题的,对于1.2.3.345匹配的结果会是1.2.3.34,所以在头尾还需要加上相应的分割符。对于在C/C++中使用,用 (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}) 匹配后再在代码中判断可以用代码进行验证, 同时可以很方便避免0.0.0.0 这样的IP.
匹配对称括号
有时我们需要用正则表达式来分析括号配对情况。比如,使用表达式 "\( [^)]* \)" 可以匹配一对小括号。但是如果括号 内还嵌有一层括号的话 ,如 "( ( ) )",则这种写法将不能够匹配正确,得到的结果是 "( ( )" 。可以通过使用"\( ([^()] | \( [^()]* \))* \)"去匹配2层的括号,如果是不确定的层数就不能采用这种方法了。为此我们可以使用(?R)递归表示的方法来解决这个问题。
\((?:[^()]|(?R))*\)
可以实现对对称括号的匹配。这里有2个多选分支,第一个多选分支[^()]匹配除括号以外的任意字符,注意这里使用(?:)不会对这里的结果进行捕获.第二个分支(?R)是匹配的关键,它将自身递归再进行匹配。
提高正则表达式匹配的效率
多选结构
对于多选结构(u|v|w|x|y|z) 而言在每次匹配的时候都需要对多选结构中的每个元素进行判断,而对于 [uvwxyz]由于只是被认为是一个元素,相对来说效率会比多选结构的结果要高一些。
在(that|the|this)中,如果将开头的th提取出来,变成th(at|e|is)也是有助于性能的提高。
对于PCRE库而言,多选结构的匹配结果, 匹配的结果是会有一定的顺序的,一般是按照出现的顺序进行匹配,比如 (tha|that) 匹配thate会匹配到tha, 而 (that|tha)会匹配到that。在使用多选结构匹配的时候可以把最可能出现的匹配放在前面。
非捕获型括号
有些情况下并不需要引用括号中的文本,那么就可以考虑使用(?:)非捕获性括号比如th(at|e|is),对于后面的括号中的多选结构我们是不需要进行捕获的,写成th(?:at|e|is)的形式,不仅减少了捕获所需要的消耗也减少了需要记录的状态数
使用固化分组和占有优先量
正则表达式在匹配的时候, 当出现匹配失败的时候,会进行一定程度的回溯,已匹配的部分正则表达式会“交还”部分结果。但如果对于许多情况下,很多回溯的意义并不大。比如
用([^:]+):(. *)去匹配一个配置文件,当遇到:不能匹配的时候,其实没有必要“交回”部分匹配结果去判断是否是’:’, 这里写成([^:]++):(. *)或者(?>[^:]+):(. *),则可以不进行“交回”操作,继续进行匹配,由于没有进行不必要的回溯可以使性能有效的提高。
但是要注意这两种方法使用不当会造成匹配结果的不正确,匹配 ABCD0987ABDE,我们希望得到分组捕获可以得到ABDE, 但如果把正则式写成(.++)([A-Z]+),结果就会是找不到匹配串,因为.++会把最后的ABDE都匹配掉,而不会“交还”ABDE进行匹配判断。
尽量使用\d \w 等元字符
在多数情况下正则表达式在匹配的时候会对\d \w等元字符进行一定程度的优化,效果会比直接使用[0-9]等要好一些。
非贪婪模式与贪然模式
(此段文章来至
正则匹配在默认情况下是使用贪婪模式,当使用UNGREEDY或者使用匹配优先的量词的时候会使用非贪婪模式,返回最短符合要求的匹配结果比如 “.*:”与”.*?:”前者匹配到最后一个”:”,后者匹配到第一个 “:”。直观上感觉非贪模式效果会比贪然模式要好。事实上并不是这样,在有些情况下还会出现效率陷阱。
效率陷阱的产生:
对非贪婪匹配的描述中说到:“如果少匹配就会导致整个表达式匹配失败的时候,与贪婪模式类似,非贪婪模式会最小限度的再匹配一些,以使整个表达式匹配成功。”
具体的匹配过程是这样的:
· "非贪婪部分" 先匹配最少次数,然后尝试匹配 "右侧的表达式"。
· 如果右侧的表达式匹配成功,则整个表达式匹配结束。如果右侧表达式匹配失败,则 "非贪婪部分" 将增加匹配一次,然后再尝试匹配 "右侧的表达式"。
· 如果右侧的表达式又匹配失败,则 "非贪婪部分" 将再增加匹配一次。再尝试匹配 "右侧的表达式"。
· 依此类推,最后得到的结果是 "非贪婪部分" 以尽可能少的匹配次数,使整个表达式匹配成功。或者最终仍然匹配失败。
当一个表达式中有多个非贪婪匹配,以表达式 "d(\w+?)d(\w+?)z" 为例,对于第一个括号中的 "\w+?" 来说,右边的 "d(\w+?)z" 属于它的 "右侧的表达式",对于第二个括号中的 "\w+?" 来说,右边的 "z" 属于它的 "右侧的表达式"。
当 "z" 匹配失败时,第二个 "\w+?" 会 "增加匹配一次",再尝试匹配 "z"。如果第二个 "\w+?" 无论怎样 "增加匹配次数",直至整篇文本结束,"z" 都不能匹配,那么表示 "d(\w+?)z" 匹配失败,也就是说第一个 "\w+?" 的 "右侧" 匹配失败。此时,第一个 "\w+?" 会增加匹配一次,然后再进行 "d(\w+?)z" 的匹配。循环前面所讲的过程,直至第一个 "\w+?" 无论怎么 "增加匹配次数",后边的 "d(\w+?)z" 都不能匹配时,整个表达式才宣告匹配失败。
其实,为了使整个表达式匹配成功,贪婪匹配也会适当的“让出”已经匹配的字符。因此贪婪匹配也有类似的情况。当一个表达式中有较多的未知匹配次数的表达式 时,为了让整个表达式匹配成功,各个贪婪或非贪婪的表达式都要进行尝试减少或增加匹配次数,由此容易形成一个大循环的尝试,造成了很长的匹配时间。本文之 所以称之为“陷阱”,因为这种效率问题往往不易察觉。
举例:"d(\w+?)d(\w+?)d(\w+?)z" 匹配 "ddddddddddd..." 时,将花费较长一段时间才能判断出匹配失败 。
效率陷阱的避免:
避免效率陷阱的原则是:避免“多重循环”的“尝试匹配”。并不是说非贪婪匹配就是不好的,只是在运用非贪婪匹配的时候,需要注意避免过多“循环尝试”的问题。
情况一:对于只有一个非贪婪或者贪婪匹配的表达式来说,不存在效率陷阱。也就是说,要匹配类似 " 内容 " 这样的文本,表达式 "([^<]|<(?!/td>))*" 和 "((?!).)*" 和 ".*?" 的效率是完全相同的。
情况二:如果一个表达式中有多个未知匹配次数的表达式,应防止进行不必要的尝试匹配。
比如,对表达式 "<script language='(.*?)'>(.*?)</script>" 来说, 如果前面部分表达式在遇到 "<script language='vbscript'>" 时匹配成功后,而后边的 "(.*?)</script>" 却匹配失败,将导致第一个 ".*?" 增加匹配次数再尝试。而对于表达式真正目的,让第一个 ".*?" 增加匹配成“vbscript'>”是不对的,因此这种尝试是不必要的尝试。
因此,对依靠边界来识别的表达式,不要让未知匹配次数的部分跨过它的边界。前面的表达式中,第一个 ".*?" 应该改写成 "[^']*"。后边那个 ".*?" 的右边再没有未知匹配次数的表达式,因此这个非贪婪匹配没有效率陷阱。于是,这个匹配脚本块的表达式,应该写成:"<script language='([^']*)'>(.*?)</script>" 更好。
注意事项
1. 使用*匹配的时候需要注意是否真的是需要使用*, 因为*是会匹配到空串,无论是从性能还是结果上都会产生问题,比如使用 [0-9]* 匹配 A1234BBBBC , 本意可能是为了匹配1234, 可实际上会得到第一个匹配结果是空串,这是因为[0-9]*可以匹配空串造成的,使用[0-9]+就可以直接匹配到1234。 如果调用的是spreg中的spreg_search_all会导致对每个位置都去进行匹配空串,对性能的影响很大。实际上多数情况下都是可以使用+代替的。
2. 当正则表达式使用GBK中文进行正则匹配的时候,很可能会在双字节的中文字符的第二个字符的地方出现 [ ] { } |等正则表达式特有的语法,由于PCRE正则表达式库不会去关注多字节问题(除非打开UTF8模式并使用UTF8编码)。会造成在编译正则表达式时出错或者出现错误的结果,比如 正则表达式 “(珅)” 匹配 “珅” ,会发现结果是错误得到的是\xAB,其实是“珅”的前半个汉字的ASCII字符,主要原因就是”珅”的第二个字符是 “|”,与正则表达式中表示分组的符号一样。遇到需要在正则表达式中写GBK中文的时候(特别是对于一些特殊的不常见汉字)最好使用正则表达式中16进制表示字符的方法 \xn的形式。比如(珅) 写成 (\xAB\x7C),这里是几个特殊符号对应的ASCII码,他们都是在GBK或GB18030编码中可能出现的。
字符
十进制
16进制
:
58
3A
<
60
3C
=
61
3D
>
62
3E
?
63
3F
[
91
5B
/
92
5C
]
93
5D
^
94
5E
-
95
5F
{
123
7B
|
124
7C
}
125
7E
展开阅读全文