在之前我们介绍正则表达式的时候,就介绍了单词边界\b,这个就属于位置断言。位置断言的特点是它不占用字符,只是判断当前的字符是否符合断言的要求,如果断言之后还有需要匹配的正则表达式字符,那么当前断言字符的位置可以根据情况会继续用来匹配或者跳过后续的规则。这是位置断言的特点,它不占用字符,我们在正则表达式-预定义字符类 中的3.4小节,也说明了这个问题,有兴趣的同学可以参照一下。
5.位置断言
位置断言的意思就是判断当前的字符是否符合需求,但是断言本身并不占用字符。这个被断言的字符可以用于后续匹配。位置断言有:字符串或者行的开头(^),字符串或者行末尾($),单词边界(\b),非单词边界(\B),仅字符串开头(\A),仅字符串结尾(\Z)。接下来我们来一一介绍。
5.1字符串或者行开头(^)
这里面有两层含义,一个字符串的开头也就是一个字符串位置为0的字符;另一个是行开头,行开头可以是字符串的开头,但是如果这个字符串包含了多行,那么第二行的开头就不是字符串的的开头了。但是如果要真正的实现行开头也匹配,就需要再加一个多行匹配的语法(?m),这个用法我们还没有讲过,我们可以这样认为,如果只是字符串最前面跟^,那么只能匹配位置为0的字符,只有加了(?m),才会将行字符串的开头扩展为每一行的开头。我们看一下下面的例子:
python
import re
a = "a2bca1234bc\na1bc"
a1 ="1a2bca1234bc\na1bc"
b = re.findall(r'a[0-9]{1,4}bc', a)
c = re.findall(r'^a[0-9]{1,4}bc', a)
d = re.findall(r'(?m)^a[0-9]{1,4}bc', a)
e = re.findall(r'^a[0-9]{1,4}bc', a1)
print("a= {}, a1 = {}, b = {}, c = {}, d = {}, e = {}".
format(a, a1, b, c, d, e))
# 输出结果:a= a2bca1234bc
# a1bc, a1 = 1a2bca1234bc
# a1bc, b = ['a2bc', 'a1234bc', 'a1bc'],
# c = ['a2bc'], d = ['a2bc', 'a1bc'], e = []
上面代码中,字符串a有两行,我们看b的正则表达式,没有加^,匹配a开头,然后后面跟1-4个数字,以bc结尾的子串。按照这个标注满足条件的有三个子串,分别为第一行的a2bc,a1234bc以及第二行的a1bc。
对于c,加了^,表示必须是字符串的开头,所以只匹配的第一行的最开始的子串a2bc。从这个例子可以看出,如果只是加^,那么只能匹配从位置0开始符合条件的子串。
d中我们正在表达式的开始位置加了(?m),强调一下,必须带着两个括号才能代表多行,如果不带(),正则表达式会把?当作是匹配0个或者1个字符。可以看到加了(?m)之后,第二行开始的位置的子串a1bc被匹配到了。
e匹配的结果为空,因为符合条件的字符串没有在位置0开始。
5.2字符串或者行结尾($)
这个和字符串开头^很像,只是这个子串必须是字符串或者行的结尾位置。同样,$默认只是字符串的结尾,如果也要匹配行的结尾,那么就得加上多行(?m)的标志在正则表达式的开头。看一下下面的代码:
python
import re
a = "a2bca1234bc\na1bc"
a1 ="a2bca1234bc1\na1bc1"
b = re.findall(r'a[0-9]{1,4}bc', a)
c = re.findall(r'a[0-9]{1,4}bc$', a)
d = re.findall(r'(?m)a[0-9]{1,4}bc$', a)
e = re.findall(r'(?m)a[0-9]{1,4}bc$', a1)
f = re.findall(r'(?m)^a[0-9]{1,4}bc$', a)
g = re.findall(r'^a[0-9]{1,4}bc$', a)
print("a= {}, a1 = {}, b = {}, c = {}, d = {}, e = {}, \
f = {}, g = {}".format(a, a1, b, c, d, e, f, g))
# 输出结果:a= a2bca1234bc
# a1bc, a1 = a2bca1234bc1
# a1bc1, b = ['a2bc', 'a1234bc', 'a1bc'], c = ['a1bc'],
# d = ['a1234bc', 'a1bc'], e = [], f = ['a1bc'], g = []
b和5.1的结果是一样的,这里不再赘述。
c我们将$加在了正则的末尾,表示字符串的结尾,所以匹配到了第二行的结尾的字符串子串a1bc。
d因为在正则的开始位置加了(?m),表示支持多行模式,正则最后跟$,表示匹配的子串需在行结尾的位置,此时我们匹配到了第一行和第二行结尾的子串:a1234bc和a1bc。
e因为a1的子串结尾的位置有个1,导致符合条件的子串都不是行结尾,所以匹配为空。
f开始的位置为(?m)表示多行匹配,然后跟着^表示匹配行开头,最后的位置是$表示匹配行结尾,其实就是匹配整行,符合这个条件的只有第二行,所以匹配了a1bc
g的正则表达式开头没有加(?m),所以只是匹配字符串的开头和结尾,也就是匹配整个字符串,这个字符串不符合正则表达式,所以匹配为空。
5.3单词边界(\b)和非单词边界(\B)
单词边界意思是当前的字符不是字母数字或者下划线,非单词边界表示当前字符是字母,数字或者下划线。单词边界我们在之前已经谈论过不少了,现在我们来看看几个例子:
python
import re
a = "a2bca1234bc\na1bc"
a1 ="a2bca1234bc1\na1bc1"
b = re.findall(r'\ba[0-9]{1,4}bc', a)
b1 = re.findall(r'\n\ba[0-9]{1,4}bc', a)
b2 = re.findall(r'\b\na[0-9]{1,4}bc', a)
c = re.findall(r'\Ba[0-9]{1,4}bc', a)
c1 = re.findall(r'c\Ba[0-9]{1,4}bc', a)
c2 = re.findall(r'\Bca[0-9]{1,4}bc', a)
d = re.findall(r'\ba[0-9]{1,4}bc\b', a)
e = re.findall(r'a[0-9]{1,4}bc\B', a)
f = re.findall(r'\Ba[0-9]{1,4}bc\B', a)
g = re.findall(r'\Ba[0-9]{1,4}bc\B', a1)
print("a= {}, a1 = {}, b = {}, b1 = {}, b2 = {}, c = {}, c1 = {}, \
c2 = {}, d = {}, e = {}, f = {}, g = {}".
format(a, a1, b, b1, b2, c, c1, c2, d, e, f, g))
# 输出结果:a= a2bca1234bc
# a1bc, a1 = a2bca1234bc1
# a1bc1, b = ['a2bc', 'a1bc'], b1 = ['\na1bc'], b2 = ['\na1bc'],
# c = ['a1234bc'], c1 = ['ca1234bc'], c2 = ['ca1234bc'],
# d = ['a1bc'], e = ['a2bc'], f = [], g = ['a1234bc']
虽然之前的章节已经讲了很多关于单词边界的例子,但是这次我仍然想多强调一下,可以把这个看成是位置断言的普遍共性,理解如何应用非占位字符的特性,它远比你想象的要灵活一些。
b在正则表达式的前面加了\b,表示该字符串子串的前面应该是非单词字符,也就是说不能是字母,数字或者下划线,用字符类表示应该是\^a-zA-Z0-9_,字符串的开头和结尾我们可以理解是单词的边界,所以字符串开头的a2bc符合条件,然后就是\n的后面a1bc也符合条件,因为a1bc前面是\n,符合单词边界的条件。 b1和b2很有意思,这两个正则表达式的区别只是\n和\b的顺序,但是结果是一样的。b1表示第一个字符是\n,然后是字符边界\b,随后跟的是a,说明a的前面需要是字符边界,因为\b不参与匹配,所以a的前面是\n,符合条件,我们得到了\na1bc这样的子串。b2正则第一个\b,表示字符边界,\n满足条件,随后是匹配\n,因为\b不占用字符匹配,所以\n匹配到了\n后面的字符a继续匹配后续的,所以得到了一样的结果。所以我们可以总结,如果\b规定了单词边界,后面的字符可以匹配这个边界(b2),也可以不匹配这个边界(b,b2),b2有点特殊,它是前面的字符已经匹配了这个边界,后面的字符其实并没有匹配,所以可以算作不匹配。
c的情况和b恰恰相反,这个是非单词边界,所以c并没有匹配字符串开始位置的a2bc,而是匹配了随后的a1234bc,因为a1bc前面是\n是单词边界,所以也没有匹配。c1和c2和b1,b2的道理是一样的,这里不再赘述。
d要求匹配的字符串前后都是单词边界,满足条件的只有a1bc,这个字符串子串的前面是\n,后面是空,满足单词边界的条件。
e要求匹配的字符串的末尾是非单词边界,满足条件的只有字符串开始位置的a2bc。
f要求匹配的字符串的首尾都是非单词边界,因为a2bc前面没有字符,不满足条件,a1234bc后面是\n,不满足条件,a1bc的前面是\n,后面是空,也不满足条件,所以结果是空。
g和f的条件是一样的,但是我们用的是字符串a1,a1中a1234bc前面是字符c,后面是字符1,所以满足条件,结果是a1234bc。
5.4仅字符串开头(\A),仅字符串结尾(\Z)
这个可以看作是^和$的不加多行的版本,只是匹配字符串开头或者结尾的子串。我们看下代码:
python
import re
a = "a2bca1234bc\na1bc"
b = re.findall(r'\Aa[0-9]{1,4}bc', a)
b1 = re.findall(r'^a[0-9]{1,4}bc', a)
b2 = re.findall(r'(?m)^a[0-9]{1,4}bc', a)
c = re.findall(r'a[0-9]{1,4}bc\Z', a)
c1 = re.findall(r'a[0-9]{1,4}bc$', a)
c2 = re.findall(r'(?m)a[0-9]{1,4}bc$', a)
d = re.findall(r'\Aa[0-9]{1,4}bc\Z', a)
d1 = re.findall(r'^a[0-9]{1,4}bc$', a)
d2 = re.findall(r'(?m)^a[0-9]{1,4}bc$', a)
d3 = re.findall(r'(?m)\Aa[0-9]{1,4}bc\Z', a)
#e = re.findall(r'a[0-9]{1,4}bc\z', a) re.error: bad escape \z at position 13
print("a= {}, b = {}, b1 = {}, b2 = {}, c = {}, c1 = {}, \
c2 = {}, d = {}, d1 = {}, d2 = {}, d3 = {}".
format(a, b, b1, b2, c, c1, c2, d, d1, d2, d3))
# 输出结果:a= a2bca1234bc
# a1bc, b = ['a2bc'], b1 = ['a2bc'], b2 = ['a2bc', 'a1bc'],
# = ['a1bc'], c1 = ['a1bc'], c2 = ['a1234bc', 'a1bc'],
# d = [], d1 = [], d2 = ['a1bc'], d3 = []
b中\A规定后面的匹配在字符串的开头,所以匹配了a2bc,b1的效果和b是一样的,b2因为加入了多行标记(?m),所以匹配为每行的开头,得到了两个结果。
c中在正则的末尾加上了\Z,表示在字符串末尾匹配,所以匹配了a1bc,c1的效果和c是一样的,c2加入了多行标记,所以变成在每行的末尾匹配,所以匹配出两个结果。
d,的效果是匹配整个字符串,所以结果是空。d1和d的效果是一样的,d2加入了多行匹配,表示匹配每行的开始和结尾,刚好第二行满足需求,所以结果是a1bc,d3是测试(?m)在\A和\Z中是否有效,发现是无效的。
e是测试小z,发现小z在python的正则模块中是不认的,所以我们以后用大Z就行。
讲到这里我们的位置断言就讲完了,我们记住一点,位置断言只判断当前位置,但是不匹配任何字符。位置断言后面跟的字符,既可以匹配配置断言的字符,又可以匹配位置断言那个字符后的字符,根据实际情况匹配。下一章,我们来讲分组,分组的作用实在是太多了,它可以重复多个字符,还可以应用逻辑,提取内容,反向引用等等,真的是很强大的工具。