上一章我们讲了分组,其实在分组中可以应用局部修饰符,有局部修饰符,必然也有全局修饰符。之前其实我们已经用过了全局修饰符(?m),表示多行模式,我们结合边界修复符^和$来获取行开头和结尾的边界。本章我们讲学习更多的修饰符,包含局部和全局。
7.修饰符
修饰符是为了重新定义或者附加一些规则,以便让我们能够处理更复杂的场景。修饰符有两种形式,全局修饰符以及局部修饰符。全局修饰符对整个正则表达式有效,而局部修饰符,只对所在括号内的正则表达式有效。拿全局修饰符多行模式(?m)为例,其对应的组内局部修饰符为(?m:),只是多了一个冒号,但冒号后面需要跟组内的正则表达式。我们的全局修饰符有如下:(?i),(?m),(?s),(?a),(?u),(?x),对应的组内修饰符有:(?i:...),(?m:..),(?s:...),(?a:...),(?u:...),(?x:...),因为全局修饰符和组内修饰符含义都是一样的,只是作用范围不同,所以我们会放到一起来讲。同时还有一些相反作用的修饰符,例如(?-i:)和(?i:)含义是相反的,我们会在讲的时候一并说到。
全局修饰符需要放在正则的最开头,当然python3.11之后可以放在任意位置,但是为了避免混乱和歧义,我都建议只放在正则表达式的开头,接下来我们一个一个的讲解这些修饰符的含义。
7.1忽略大小写与解除忽略((?i),(?i:...),(?-i:...))
(?i:...)这个修饰符表示忽略大小写,默认情况下是大小写敏感的。如果想取消忽略,则用(?-i:...),这个(?-i:...)的使用有两种情况,一个是在全局应用了忽略大小写的修饰,然后在局部取消忽略,另一个是在分组中使用了忽略大小写,但是在分组内的分组中取消忽略,听上去有点绕,其实就是括号嵌套括号的意思,例如:(?i:...(?-i...)),我们用具体的例子来看,就会一目了然。
python
import re
a = "abcDEFABCDEFabcdefABCdef"
b = re.search(r"(?i:ABC(def))", a)
b1 = re.search(r"(?i:ABC(?-i:def))", a)
c = re.search(r"(?i)ABC(def)", a)
c1 = re.search(r"(?i)ABC(?-i:def)", a)
d = re.search(r"(?i:ABC)def", a)
d1 = re.search(r"(?i)ABC(?-i:def)", a)
d2 = re.search(r"(?i)ABC(def)", a)
print("b.group() = {}, b1.group = {} , c.group() = {}, \
c1.group() = {}, d.group() = {}, d1.group() = {}, d2.group() = {}".
format(b.group(), b1.group(), c.group(),
c1.group(), d.group(), d1.group(), d2.group()))
# 输出结果:b.group() = abcDEF, b1.group = abcdef ,
# c.group() = abcDEF, c1.group() = abcdef,
# d.group() = abcdef, d1.group() = abcdef, d2.group() = abcDE
b用了嵌套的分组,在第一个分组的开始处用了?i:来忽略大小写,这个设置对嵌套分组也产生了作用,所以匹配到了abcDEF,b1的通过在第二个分组中用?-i:来重新设置为不忽略大小写,所以匹配的结果是abcdef。
c和b的结果是一样的,只是用的是全局修饰符,虽然def包含在分组内,但是全局修饰符依然有效,所以整个正则匹配都是忽略大小写的。c1和b1的结果是一样的,通过在分组中设置取消忽略大小写,从而必须匹配小写的def,所以结果是abfdef。
d说明局部修饰符的作用域只能在分组内,后面的def必须匹配小写,所以得到的结果是abcdef。d1正则开始用全局修饰符(?i)来忽略大小写,但是在分组内用?-i:来取消忽略,所以同样匹配了abcdef,d2没有在分组内添加修饰符,全局修饰符让所有字母都忽略大小写,所以结果是abcDEF。
7.2 多行模式((?m),(?m...))
在之前的章节我们讲位置断言的时候,用了(?m)这样的全局多行模式,现在又有这种局部的多行模式。全局多行(?m)和局部多行(?m:...)在大部分场景下是可以互换的,基本上你不会遇到必须用局部或者全局的场景。除非需要匹配字符串开头,同时又需要匹配多行开头,这时候可能是局部多行有意义的地方,但是这种情况又不是必须的,可以用别的写法替代,而且也不会增加复杂度,所以我个人感觉很鸡肋。来看看下面的代码:
python
import re
a = "bec\nabc\nDEF\nefg\nhij"
a1 = "abc\nDEF\nefg\nhij"
b = re.search(r"(?m:^abc$)", a)
b1 = re.search(r"(?m)^abc$", a)
c = re.search(r"^abc\n(?m:^DEF$)", a)
c1 = re.search(r"^abc\n(?m:^DEF$)", a1)
c2 = re.search(r"^abc\nDEF(?=\n)", a1)
print("b.group() = {}, b1.group = {} , c = {}, \
c1.group() = {}, c2.group() = {}".
format(b.group(), b1.group(), c, c1.group( ), c2.group()))
# 输出结果:b.group() = abc, b1.group = abc ,
# c = None, c1.group() = abc
# DEF, c2.group() = abc
# DEF
b和b1分别用全局多行和局部多行来查询字符串a中符合条件的子串,得到了相同的结果。
c和c1一样的表达式,都是为了查询字符串开头是abc的子串,局部多行规定DEF必须是行开头同时也是行末尾。字符串a不符合条件,所以c得到的结果是None,字符串a1符合条件,所以得到的结果是abc\nDEF。如果用全局多行,那么字符串a和a1都能得到匹配,这不符合之前我们想要字符串开头是abc的约束。 c2是和c1同样的效果,但是并没有使用多行模式,正则最后面的(?=\n)属于前后向断言的正向先行断言,这个我们还没有讲到,这个断言的意思是DEF后面跟的是\n,这个断言并没有占用字符,所以\n不会被匹配进去,这和之前我们讲的位置断言效果是一样的,只是这个控制更加的精细,后面我们会讲这个。c2说明局部多行也不是必要的,我们可以用其他的方式替代。
7.3让点号.可以匹配换行((?s),(?s:...))
点号.可以匹配任何字符,但是无法匹配换行,为了能够让点号.匹配换成,我们需要一个修饰符,这个就是全局修饰(?s)和局部修饰(?s:...),如果你想点号在整个正则表达式都可以匹配\n,那么可以用全局修饰符,如果你只是希望在局部生效,那么可以用局部修饰符。看一下下面的代码:
python
import re
a = "bec\nabc\nDEF\nefg\nhij"
a1 = "abc\nDEF\nefg\nhij"
b = re.search(r"(?s)abc.DEF", a)
b1 = re.search(r"(?s:abc.DEF)", a)
b2 = re.search(r"s:abc.DEF", a)
c = re.search(r"(?s)abc.DEF.efg", a)
c1 = re.search(r"(?s:abd.DEF).efg", a1)
c2 = re.search(r"(?s)abc(?-s:\nDEF).efg", a1)
print("b.group() = {}, b1.group = {} , b2 = {}, c.group() = {}, \
c1 = {}, c2.group() = {}".
format(b.group(), b1.group(), b2, c.group(), c1, c2.group()))
# 输出结果:b.group() = abc
# DEF, b1.group = abc
# DEF , b2 = None, c.group() = abc
# DEF
# efg, c1 = None, c2.group() = abc
# DEF
# efg
b和b1的效果是一样的,都是可以允许用点号来代替\n,b2没有加修饰符,我们可以看到结果是空,说明默认情况下点号.没有办法匹配\n。
c通过全局修饰符,让两个点号.分别匹配了\n,得到了abc\nDEF\nefg的子串,c1使用局部修饰符,在括号外的点号.因为已经超出了局部修饰符的范围,所以点号.不能匹配\n,结果为空。c2比较特别,用了一个(?-s:),这个修饰符的含义就是在局部括号内取消全局全局修饰符的作用,所以我们用\n代替逗号.,这样我们才能匹配到结果,感兴趣的可以试试将\n换成逗号.,看看还能不能匹配到结果。
局部修饰符的相反操作都是在修饰符标志之前加一个减号,忽略大小写修饰符也是如此。像c2这种写法确实有点复杂,所以如果不是特殊的业务需求,尽量不要如此来写,这个例子只是为了说明用法,并没有太多的实际意义。
7.4ascii模式((?a),(?a:...))
在之前我们学预定义字符的时候,\w可以代表字母,数字和下划线,这个字母其实包含了其他语言的各种字符,例如中文汉字等,为了让\w只表示ascii编码的字符,我们可以用局部修饰符(?a:...)或者全局修饰符(?a),都可以限制\w只表示ascii编码的字符,这样也就是说只表示英文字母,数字,以及下划线,我们看一下下面的代码:
python
import re
a = "bec\nab中\nDEF\ne文g\nhij"
b = re.findall(r"(?a)\w+", a)
b1 = re.findall(r"(?a:\w+)", a)
b2 = re.findall(r"(?a:\w+)\w+", a)
c = re.findall(r"\w+", a)
print("b = {}, b1 = {}, b2 = {}, c = {}".
format(b, b1, b2, c))
# 输出结果:b = ['bec', 'ab', 'DEF', 'e', 'g', 'hij'],
# b1 = ['bec', 'ab', 'DEF', 'e', 'g', 'hij'],
# b2 = ['bec', 'ab中', 'DEF', 'e文g', 'hij'],
# c = ['bec', 'ab中', 'DEF', 'e文g', 'hij']
b和b1分别用全局和局部的模式来只匹配英文字母,我们可以看到结果中不包含中文,这两个效果是一样的,我们看到结果相同。b2在局部模式外有又加了一个\w+,这个预定义字符可以匹配中文,所以你看到结果中有中文。b2和c的结果是相同的,因为b2外面的\w+把局部修饰符中不能匹配的中文又匹配了进来。c只是正常的匹配所有的字母包括中文。
7.5 unicode模式((?u),(?u:...))
默认情况下,\w对应的就是unicode模式,python在3.11之后支持这个特性。他的场景是在全局使用ascii的模式下,在局部可以回到unicode模式,从而让\w在局部去匹配中文之类的字符。我们看一下下面的例子:
python
import re
a = "中文\nab中\nDEF\ne文g\nhij"
b = re.findall(r"(?a)(\w+(?u:\w+))", a)
c = re.findall(r"(?a)(\w+\w)+", a)
print("b = {}, c = {}".
format(b, c))
# 输出结果:b = ['ab中', 'DEF', 'e文g', 'hij'],
# c = ['ab', 'DEF', 'hij']
可以看到b和c的正则表达式几乎相同,唯一的区别就是b中将第二个\w+用局部unicode模式修饰了。可以看到中文没有被匹配,因为第一个\w+要求至少匹配一个acsii码的字母,数字,下划线。所以中文不符合。第二个\w+会匹配所有语言的字母包括中文,英文,所以对于b来讲,只要第一个字母是英文,后面是其他语言都可以,这里面纯英文会被匹配是因为unicode包含了ascii码所表达的所有字符。当然ascii码和unicode不是可以相提并论的概念,ascii只是编码了unicode的一小部分,后面章节我会详细的讨论这两者之间的关系,如果有兴趣可以关注我。c本身只用一个\w+就可以达到一样的效果,这里用两个完全是想和b尽量形式相近,从而有利于对比。因为用全局修饰符ascii模式,所以只能匹配英文字母,数字和下划线。
7.6详细模式((?x),(?x:...))
有时候我们写正则表达式,如果比较复杂,读起来难免费劲儿,这时候我们可能想加点格式,让阅读更容易,甚至说加点注释,这时候就需要用到局部详细模式(?x:),当然,我们已经明白肯定有去全局模式,就是去掉冒号(?x)。这个详细模式就是忽略掉正则中空格,换行和回车(回车键),制表符(tab键),同时#表示从当前位置到行的末尾都是注释(所以从#开始,后面不应该再有任何正则语法),我们来看下面的代码:
python
import re
a = "中文\nab中\nDEF\ne文g\nhij "
b = re.findall(r'''(?x)(?a)(\w+
(?u:\w+) )#这是注释''', a)
c = re.findall(r'''(?a)(?x:\w+(?u:\w+)#这里是注释
)''', a)
d = re.findall(r'''(?a)(?x:\w+(?u:\w+)\n#这里是注释
)''', a)
e = re.findall(r'''(?a)(?x:\w+(?u:\w+)\ #这里是注释
)''', a)
print("b = {}, c = {}, d = {}, e = {}".
format(b, c, d, e))
# 输出结果:b = ['ab中', 'DEF', 'e文g', 'hij'],
# c = ['ab中', 'DEF', 'e文g', 'hij'],
# d = ['ab中\n', 'DEF\n', 'e文g\n'], e = ['hij ']
b和c分别用了全局修饰和局部修饰来忽略正则中的空白,tab,以及换行,我们可以看到用了''' '''来表示多行,这个过程中你可以随意的调整格式,可以看到b和c的结果是一样的,尽管格式有那么一点不同,注意因为c是局部修饰符,所以#的注释不能出现在修饰符所在的括号外面,要不然会达不到想要的效果,但是b是全局修饰符,可以出现在正则表达式的最外层。d表明如果需要匹配换行,可以和以前一样加\n表示明确需要换行,这符合我们之前的习惯,但是e有点特别,当我们需要匹配空格的时候需要在空格前面加反斜杠\来声明不忽略这个空格,其他也一样比如\t来表示不忽略tab,我们可以看到e匹配到了后面带空格的子串。
讲到这里,我们的修饰符就讲完了。全局修饰符还可以通过参数的方式传过去,而不需要写在正则表达式中,例如可以将flags设置为re.X来取代全局修饰符(?x),这在将来将正则函数的时候会再详细说明。