行的开始:^
^abc 以a开头,接下来是b 再接下来是c的文本 (只有用在表达式的开头时才表示行的开始)
行的结束:$
abc$ 以c结尾前面是b 再前面是a的文本
字符组:[]
[abc] 表示 a 或 b 或 c
连接字符: -
用于字符组内,表示一个范围 [1-5] 等同于 [12345]
^ 用在组内第一个字符时表示非
^[^1-6] 表示不以1-6的数字开头的文本
任意符:.
. 匹配任意字符
小括号:()
界定范围,其实还有个重要的作用,捕捉文本分组
多选分支:|
a(b|d)c 匹配abc或adc
多选分支和字符组都有或的意思,
但是字符组只是匹配字符组中包含字符的其中之一,
但是多选分支可能本身就是个正则表达式 比如a(a[bd]c|1[0-5]3)b
注意事项 ^abc|123: 和 ^(abc|123): 是不同的,
前面表示匹配 abc或123:开始的文本
后面表示匹配 abc:或123:开始的文本 因此为了明确语境需要配合()来完成
单词分界符:\< 和 \> (只有在少部分支持的系统中才能使用)
\<ab 以ab开头的单词 比如able
和行开始符表示的意思不同 ^ob.*表示的是以ob开始的文本,
比如'obc my good'
\<ob.* 匹配的是以ob开头的单词开头的文本,
因为obc不是个单词,所以不能匹配
如果换成 'object my good' 就可以匹配,因为object是英文单词
问号:?
goo?d ?表示可选元素不作为匹配的必然条件 示例可匹配god,good 配合括号可作出更多的操作 go(od)?则可以匹配go,good
除了?外还有两个量词 + 和 * ,和一个组合量词{}
?表示可选,即匹配一次或0次
+ 表示至少一次
* 表示任意次 0次或多次
{} 表示范围
abc? 匹配ab abc
abc+ 匹配abcc abccccc...
abc* 匹配ab abccccc...
捕获命名 (?P<name>...)
Python支持为捕获的内容进行重命名
(?P<myName>good) 表示捕获 good,可以通过obj.group(1) 来获取good 但是也可以通过自定义名称 obj.group("myName") 来获取good
大括号 {}
大括号代表区间个数 [a-z]{8} 表示 连续的八个字母 [a-z]{3,8} 连续的 3 到 8 个字母
反向引用
()有捕捉文本的作用,利用这个特性可做一些反向引用的操作,比如查找重复单词即连续出现两次相同的单词 my name is is xie,这里is是重复的如果用正则表达式找出所有的这种重复单词要怎么做,首先要匹配的是单词,所以要用到单词分界符 \<is\s+is\> 这样只可以匹配 is is 为了更通用 \<([a-zA-Z]+)\s+\1\> 这里有两个关键点1.小括号,2.\1 因为括号内的内容是会被捕获的,所以我们可以取出其捕获的内容 取出方法便是用\加一个数字 \1 表示取出第一个括号捕获的内容 因此该表达式表示 先匹配一个单词并捕获,在至少一个空格后再取出其捕获的内容进行匹配,这样前后两个匹配就是相同的\<\>又保障了这两个是单词,所以满足需求。类似的有(0-9)(a-z)\1\2 用于匹配连续两次相同的数字加字母2a2a \1 取第一个()捕获的内容 \2取第二个()捕获的内容
如果不是在表达式中而是在表达式匹配完成时取得这些分组中的值,
不同语言有不同的方式,
Perl中用 $1 $2 去取分组\1 和 \2 的内容,
Python中用group(1) group(2) 比如:
Perl中:以下表达式用来匹配前两个数字相同后面跟多个字母的字符串 类似99abc , 22kkk
if($inputStr =~ m/^([0-9])\1([a-zA-Z]*)$/) {
$value = $1; //$1 用来获取第一个括号内捕获的内容
$string = $2; //$2 用来获取第二个括号内捕获的内容
print "value = $value, string = $string";
}
当()嵌套时其对应的序号按照左边半括号(的顺序,
比如:(abc(123(.CF)))
abc(123(.CF)) 在分组\1内 123(.CF)在分组\2内 .CF在分组\3内
这里有个()的反例,如果只想界定范围而不捕捉内容怎么做,答案是:(?:) 这是一个组合元字符 里面的 ? 和之前的 可选? 没有任何关系
把上面的例子改为 (abc(?:123(.CF))) 则分组顺序变成了 abc(123(.CF)) 在分组\1内 由于第二个( 放弃了捕捉内容因此123(.CF)不会被捕捉,也不占用分组,所以.CF 变成了在分组\2内 这个元字符在这个例子里也许并看不出有什么实际意义,但是在一些实际的开发场景里非常有用,举个例子:
你在开发中写了这么一个正则 ([0-9]+)\s*(K?m) 用来捕获距离,\1 捕获数值 \2 捕获单位,而且此时程序已经写完了,这时需求扩展了,需要能匹配小数,所以你把正则改为了 ([0-9]+(.[0-9])?)\s*(K?m) 这里 (.[0-9])?的括号原本只是用来给?界定边界的 但是其依然有捕捉作用,如果你这么写 因为插入了一个分组 原本用来捕捉单位的分组则变成了\3 因为此时程序已经写完了,内部都是按照\2判断的,所以要想程序正常就必须把之前里面对\2的判断改为\3 这当然是可行的,但是还有一种更优雅的方法就是让不必捕捉的括号放弃捕捉,正则表达式改为([0-9]+(?:.[0-9])?)\s*(K?m) 这里只是多了个?: 由于第二个(内放弃了捕捉,不占用分组,所以原来的分组顺序没有任何改变就能完成这个功能的扩展
反斜杠 \ 转义
前面有多次出现过反斜杠的地方 比如 \< \> \1 \2 这些都被成为组合元字符 他们组合在一起有特殊意义 其实反斜杠还有个非常重要的作用 转义,比如我们想要匹配一个 \< 的字符串要怎么做,因为\<本身代表的是单词开头 所以要把元字符当成一个普通字符进行匹配就需要用到转义 在元字符前加一个\ 所以可以用\\< 匹配\<
正则表达式一些简记字符(Python支持)
\a 警报符
\f 进止符
\b 单词分界符(当出现在字符组内时为退格符[\b])
\v 垂直制表符
\t 水平制表符
\n 换行符
\r 回车符
\s 任何'空白'字符(空格,制表符,进止符)*很常用
\S 除了 \s 之外的所有字符
\w [0-9a-zA-Z] (经测试在Python3中 \w 表示[0-9a-zA-Z_]) 除了数字和字母外还会包括下划线
\W 除了 \w 之外的所有字符即[^0-9a-zA-Z]
\d [0-9]
\D [^0-9]
环视
环视的中心思想是用来找出一个位置而不是字符,所以其只用来校验不用来捕获,也不占用匹配字符
肯定环视:
顺序环视 (?=...) 从左往右校验 当遇到 满足 条件的字段时,那么该字段的左边位置便是匹配到的位置
逆序环视 (?<=...) 从右往左校验 当遇到 满足 条件的字段时,那么该字段的右边位置便是匹配到的位置
比如 (?<=goo)(?=d) 这个表达式用来找到一个 在goo右边
在 d 左边的一个位置,如果这个位置存在则为true否则为false
(?=good)(goo) 这个表达式的作用是是找到字符串good左边的一个位置并捕获
这个位置 后的goo 即只匹配good 单词中的goo 不会匹配 goof中的goo
(ood)(?<=good) 和上面一样,ood后面的位置必须是good的右边,
因此只匹配good中的ood 不会匹配bood中的ood
(?=good)(ood) 该表达式永远为false
因为 good 左边位置的下一个字符是 g 而不是 o,
因此永远为false
同理:(goo)(?<=good) 也永远为false
因为(goo)的右边紧邻的是goo的右边
而环视要求是good的右边,这两个条件矛盾,永不成立
否定环视:
否定顺序环视 (?!...) 从左往右校验 当遇到 不满足 条件的字段时,那么该字段的左边位置便是匹配到的位置
否定逆序环视 (?<!...) 从右往左校验 当遇到 不满足 条件的字段时,那么该字段的右边位置便是匹配到的位置
同样的例子:
(?!good)(goo) 先找到不是good字段的左边的一个位置
并捕获 这个位置 后的goo 即不匹配good中的goo 可以匹配 goof 中的goo
否定逆序同理
关于环视的主意点:
在Python中逆序环视必须是有明确长度的,
顺序环视则没有限制 比如 (?<=[a-z]+) 是不合法的,
因为[a-z]+的长度是不固定的 但在顺序中可以 (?=[a-z]+) 是合法的
所以应当特别主意逆序中只能使用(?<=[a-z]) (?<=good)这种有明确长度的表示
判断条件 (?... ...|...) 相当于 (?if then | else)
?后面紧跟判断条件,条件成立则匹配 | 左边 否则匹配其右边
(?(?:good)[A-Z]|[a-z]) good后面应该是一个大写字母 不是good则后面应该是个小写字母
匹配优先量词
?,*,+,{}
匹配优先的匹配过程是先满足前面匹配条件,
并且尽可能多的匹配:
比如 .*: 去匹配abc:abc: 匹配到的内容不是 abc: 而是abc:abc:
原因就是.* 是匹配优先的,
会直接匹配所有字符即 abc:abc: 当匹配进行到':'时.*已经把所有字符匹配完了,
但为了满足整个表达式,所以.*匹配的内容要进行回溯,回溯是后入先出的,即倒序的,
因此从第二个:开始,当回溯到第二个:时正好满足表达式
忽略优先量词
*?,??,+?,{}?
忽略优先的匹配过程是:
先跳过略优先的匹配进行下面的匹配,
如果下面的匹配不符合就回溯到忽略匹配,
比如把上面的例子改为 .*?: 去匹配 abc:abc:
此时其匹配的结果便是 abc: 而不是 abc:abc:
原因是 *?是忽略优先的,因此在该匹配过程中会跳过.的匹配 先匹配 ':'
第一个a不能满足 : 就回溯到a并用.去匹配,
因为.可以匹配a所以接着用:进行下面的匹配b 因为b也满足不了:
所以回溯到b并用忽略条件去匹配 依次类推,
当匹配到第一个:时此时正则表达式已满足,匹配完成
占有优先量词
*+,?+,++,{}+
占有优先和匹配优先一样会先尽可能多的满足前面的匹配条件,
而且一旦内容被匹配就不会再交出来,即不允许匹配到的内容进行回溯
还是上面的例子 .*+: 去匹配 abc:abc: 此时匹配会失败
原因是 .*+ 会直接匹配到所有的内容 abc:abc:
当匹配到表达式的 : 时因为已经没有内容了,
正常情况下需要对前面匹配到的内容进行回溯,
但前面的内容是占有优先的,不允许回溯,因此:无法匹配到内容,所以匹配失败
固化分组
(?>...)
固化分组的作用和占有优先相同,即防止匹配的内容回溯
比如 (?>.*): 匹配 abc:abc: 同样会失败,原因和占有优先相同
回溯的补充:
关于回溯,其实回溯能够实现的原因是表达式引擎在匹配过程中的每一步都会保留一个备用状态,当后面的匹配失败时就会跳转到上一个备用状态即完成回溯
占有优先和固化分组能够避免回溯的原因是,占有优先和固化分组的条件都不会保留备用状态,因此无法回溯
固化分组和占有优先的意义在于可以优化表达式,避免非必要的回溯,比如 .*+: 去匹配 abcdefg 当程序匹配到g时表达式立即给出失败,因为:无法匹配,又无法回溯,
如果改为匹配优先 .*: 当匹配到g时此时表达式开始匹配: 但是内容已经被.* 全部匹配了,所以要进行回溯到g此时仍不匹配继续回溯到f依次一直回溯到a发现表达式确实无法满足,此时才会报告失败
匹配优化
因为DFA是文本引导型,优化表达式并无作用。而NFA则是表达式引导型,不同的表达式效率差别会非常大,但由于POSIX NFA效率低下和用到的很少,所以以下优化没有特殊说明则只针对传统NFA引擎。
对于多选结构来说一般把更加通用的匹配条件放到前面,会减少回溯次数
例如用(^"|")* 来匹配 abcdefghigk"lmn 就比 ("|^")* 要快 因为对于传统NFA来说 多选结构只有第一个条件匹配失败才会回溯去匹配下一个条件 对于第一个表达式(^")* 第一个条件可以一直匹配到k 直到第一个"才会匹配失败,此时回溯用(")来匹配 接下来继续用(^")匹配到最后 因此第一个表达式只进行了一次回溯。而第二个表达式("|^")* 需要先用(")* 来匹配,因为一直到"之前都不满足因此a-k每次匹配都要回溯后用(^")* 来匹配 这样整个表达式匹配完成需要14次回溯
对于上面的例子还可以进一步优化,(^"|")* 虽然可以减少回溯次数,但仍需要15次迭代即匹配的次数,为了减少迭代可以改写成(^"+|") 这样 由于+是匹配优先的 所以(^"+)会一次性匹配 a-k,然后回溯一次之后再一次性匹配 l-n 这样整个表达式 只进行了一次回溯 和 两次迭代
这里要注意只有在传统NFA引擎下才可以用(^"+|")*来优化,
对于POSIX NFA来说这会引起超线性匹配,
实际上对于任何量词的嵌套POSIX NFA 都会产生超线性匹配
比如([a-z]+)* 来匹配 abcdefghigklmn
因为POSIX NFA 下表达式并不会在第一次匹配成功后就停止,
而是会遍历所有的可能 这样由于+和*都是不定长量词,
因此表达式会尝试(a)bcdefghigklmn 也会尝试 (ab)cdefghigklmn
也会尝试 a(bc)defghigklmn 依次类推,
这个匹配数量是幂增长的 当字符串长度超过40时将是一个天文数字对于
对于相似单词的匹配多选分支的范围尽量要小
比如匹配this 或 that 表达式 th(is|at) 要比 (this|that) 好
单字符多选分支可采用字符集来替代 对于指定字符可采用简记符来代替 即 \d > [0-9] > (0|1|2|3|4|5|6|7|8|9)
另一种优化
对于正则引擎,每次匹配都需要编译表达式进行格式差错校验,校验完成才会调动传动引擎进行匹配,对于不含变量的表达式这样重复校验完全是没必要的,可以进行预编译并缓存,python 对预编译的支持是通过 compile()来实现的
未完待续...
上面内容均来自《正则表达式》一书,在接下来的学习中本人任然会把一些心得和总结在该文章中进行分享,希望对大家有些许的帮助