之前我们学了位置断言,通过位置断言,我们可以判断字符边界(\b),非字符边界(\B,)字符串或者行的开头(^)和结尾($),以及仅字符串的开头(\A)和结尾(\Z)。位置断言的特点是不占用字符,只是作为判断当前的字符是不是符合条件,如果你对位置断言已经忘记了,建议你重新看之间的章节。
我们发现位置断言很固定,但是如果我们想判断个子串前后是不是符合我们指定的某个字符,但不想占用它,这时候位置断言显得不够用了,这就引出了我们今天所学的另外一种断言,前后相断言。
8.前后相断言
有时候我们发现有的词起的文邹邹的,不常用,所以看上去有点懵,这是很多翻译人员的缺点,不接地气。如果我把这个写成前后位置判断,估计大家就很明白了。就是判断字符的前后位置是什么字符。前后相断言包括:正向先行断言(?=...),负向先行断言(?!...),正向后行断言(?<=...),负向后行断言(?<!...),原子组(?>...)。前面咋一看很像绕口令,其实大家可以这么记,判断肯定结果的是正向,判断否定结果的是负向,先判断的是先行,后判断的是后行。接下来我们来细说这几个断言。
8.1正向先行断言((?=...))
这个断言是判断后面跟的是什么,断言本身在正则表达式中是从左到右顺序执行的,所以叫先行。我们看一下下面的代码:
import re a = "中文\nab中\nDEF\nDE\nhij " b = re.findall(r"ab(?=中)", a) c = re.findall(r'D(?=EF)', a) print("b = {}, c = {}".format(b, c)) # 输出结果:b = ['ab'], c = ['D']b中通过(?=中)来指定ab后面必须跟‘中’,但是断言本身不匹配‘中’字,所以结果是ab。c中我们指定D后面必须是EF,可以看到结果是D,这说明断言可以跟多个字符。
8.2负向先行断言((?!...))
这个断言来判断后面跟的不是什么,刚好和正向先行断言相反,我们直接看代码:
import re a = "中文\nab中\nDEF\nDE\nhij " b = re.findall(r"ab(?!中)", a) c = re.findall(r'D(?!EF)', a) print("b = {}, c = {}".format(b, c)) # 输出结果:b = [], c = ['D']这个代码只是将正向先行改成了负向先行,b中因为没有符合ab后后面不是‘中’的子串,所以结果是空。c的结果还是D,那是因为D后面不跟EF,还有D后面跟E\n这样的子串满足条件,所以结果还是D。
8.3正向后行断言((?<=))
这个是用来判断字符前面是否满足条件,这也是为什么叫后行,因为需要先找符满足条件的后面的字符,然后再来断言字符前面是否满足条件,这在正则表达式中不是顺序执行的,所以是后行断言。我们看一下代码:
import re a = "中文\nab中\nDEF\nDE\nhij " b = re.findall(r"(?<=D)EF", a) c = re.findall(r'(?<=ab)中', a) print("b = {}, c = {}".format(b, c)) # 输出结果:b = ['EF'], c = ['中']b指定EF之前必须是D,字符串中DEF满足条件,因为断言不占用字符,所以结果是EF。c中指定中前面必须是ab,所以ab中满足要求,所以结果是中。
8.4负向后行断言((?<!))
负向后行断言和正向后行断言刚好相反,表示字符前面不能是指定字符。看一下下面的代码:
import re a = "中文\nab中\nDEF\nDE\nhij " b = re.findall(r"(?<!D)EF", a) c = re.findall(r'(?<!ab)中', a) print("b = {}, c = {}".format(b, c)) # 输出结果:b = [], c = ['中']b指定EF前面不能是D,字符串中没有符合条件的子串,所以结果为空。c中指定中的前面不能是ab,这个满足条件的是字符串的第一个字符“中”,因为是字符串开始的第一个字符,所以前面不是ab,所以满足条件,结果是中。
8.5原子组((?>...))
原子组在python3.11的时候加进来了,目的是为了防止回溯,从而产生性能问题。一般情况下我们不会用到原子组,除非你非常的确定你想要的结果。我们先看代码再解释:
import re a = "aaaaaaaaaaaaaaaaaaaaab" b = re.findall(r"a+ab", a) c = re.findall(r'(?>a+)ab', a) print("b = {}, c = {}".format(b, c)) # 输出结果:b = ['aaaaaaaaaaaaaaaaaaaaab'], c = []还记得我们之前讲+这样的用法时,默认是贪婪算法,尽可能多的匹配字符。所以我们可以看到b匹配了整个字符串,在这个过程中肯定产生了回溯,因为a+会吃掉所有的a,但是发现后面a不匹配,必然会让前面的a+少吃一个a,来满足匹配。但是c的结果为空,因为通过原子组,a+吃完所有的a后,出了原子组,匹配的结果就锁死了,然后后面的a找不到匹配,原子组阻止回溯,所以结果是空。这也是为什么要慎用原子组,除非你特别肯定,要不然匹配的结果可能会和你想象的不同。
为了测试一下回溯产生的效率问题,我们写一个比较极端的例子:
import re import time a = "a"*20+"b" startb = time.time() b = re.search(r"(a+)+$", a) durationb = time.time() - startb startc = time.time() c = re.search(r'(?>a+)+$', a) durationc = time.time() - startc print("b = {}, durationb = {}, c = {}, durationc = {}". format(b, durationb, c, durationc)) # 输出结果:b = None, durationb = 0.13580703735351562, # c = None, durationc = 0.00013303756713867188我们测试了嵌套的+的效果,b不使用原子组,c使用原子组,可以看到用时相差1000倍,从正则的效果来看,两个正则的结果都为空,恰恰是这种为空的场景,非原子组会不停的回溯重试,从而浪费更多的时间。
上面的情况是在无法匹配情况下导致非原子组不停的回溯从而浪费时间。但是如果我们将$替换成b,让可以匹配到结果,我们就会发现python本身的性能优化很强大,两者的耗时结果是差不多的。看下面的代码:
import re import time a = "a"*20+"b" startb = time.time() b = re.search(r"(a+)+b", a) durationb = time.time() - startb startc = time.time() c = re.search(r'(?>a+)+b', a) durationc = time.time() - startc print("b.group() = {}, durationb = {}, c.group() = {}, durationc = {}". format(b.group(), durationb, c.group(), durationc)) # 输出结果:b.group() = aaaaaaaaaaaaaaaaaaaab, # durationb = 0.00024390220642089844, # c.group() = aaaaaaaaaaaaaaaaaaaab, # durationc = 0.00013589859008789062可以看到这两者消耗时间是差不多的。
总的来讲,原子组在某些场景下可能更省时间,但是会有可能匹配不到你想要的结果,除非你有特别的理由,否则还是尽量少使用。
到这里我们这一节就讲完了,这次讲的前后相断言比之前的位置断言更灵活,通过它们我们可以写出条件更复杂的正则表达式,提到条件,我们之前谈分组的时候,讲到限制逻辑运算的作用范围,只是简单的讲了一下逻辑或|,下一章节我们会专门讲一下这个“或”的场景,正则表达式中没有逻辑与和非,但是我们写出的正则表达式中各个元素之间的顺序关系其实就是与的关系,而负向先后行断言就是非的关系。所以实际上我们已经接触了正则表达式的所有逻辑运算。
如果你认真学到这里,正则表达式基本上也算小有所成了,只剩下很小的一部分,我们之前也接触过,但是没有详细说,后见章节会继续来说,如果你有任何疑问,欢迎私信我。