首页 > 代码库 > MyEssay 之 Python正则表达式 —— 四种断言扩展的理解

MyEssay 之 Python正则表达式 —— 四种断言扩展的理解

我们经常用正则表达式来检测一个字符串中包含某个子串,要表示一个字符串中不包含单个的某字符或某些字符也很容易,用[^...]形式就可以了。但是要表示一个字符串中不包含某个子串(由字符序列构成)的时候,用[^...]这种形式就不行了,此时就需要使用到四种正则表达式的扩展匹配了,即所谓的“正向前行匹配”  (?=...)、“负向前行匹配” (?!...)、"正向后行匹配" (?<=...)  、“负向后行匹配”(?<!...)。其中的...又可以是任意的合法正则匹配字符串。类似于\b单词边界特殊字符一样,这四种断言表达式本身也是不消耗任何被匹配字符串中的字符宽度,而仅仅只是匹配一个position而已。

对于这4个断言的理解和记忆,可以借鉴<http://blog.csdn.net/rebelqsp/article/details/22115249>文中的描述,从两个方面入手:

  1. 所谓的前行(lookahead)和后行(lookbehind),其实就是向前看和向后看的意思。正则表达式引擎在执行字符串和表达式匹配时,会从头到尾(从前到后)连续扫描字符串中的字符,设想有一个扫描指针指向字符边界处并随匹配过程移动。前行断言,是当扫描指针位于某个位置时,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为前行。后行断言,引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。

    记忆方式:后行断言(?<=pattern)、(?<!pattern)中,有个像箭头一样的小于号,对于自左至右的文本方向,这个箭头是指向后的,这也比较符合我们的习惯。把小于号去掉,就是前行断言

  2. 所谓的正向(positive)和负向(negative):正向就表示匹配括号中的表达式,负向表示不匹配。

    记忆方式:不等于(!=)、逻辑非(!)都是用!号来表示,所以有!号的形式表示不匹配、负向;将!号换成=号,就表示匹配、正向。

我们特别需要注意的一点是,对于后行方式的两种断言(?<=...)和(?<!...),其中的...表达式所匹配的内容必须是定长的,这也就意味着后行断言的匹配表达式中是不能含有*、?和+符号的,这也就为后行断言的使用带来了一定的困难和麻烦。 下面以具体实例来说明这些断言的使用(以下实例中被匹配字符串和pattern字符串中的空格为了明确显示出来,使用字符?来表示):
 

line0 = ‘?#?def???func(funcName, funcParam, funcTime=360) ‘
line1 = ‘?def???func(funcName, funcParam, funcTime=360) ‘
line2 = "????obj1(param).func(‘func1‘, ‘param1‘, funcTime=150) # test"
line3 = "??obj2().funcTest(1)  # obj1(param).func(‘func1‘, ‘param1‘)"

我们希望字符串中包含对函数 func()的调用,即在被测试line中出现 "func("字符串,但是在被测line中却又不包含针对函数func的定义,即不能出现 “def func(” 字符串,并且def 和 func 之间可能包含多个空格。按照最直接的思路,为要匹配 "func(" 字符串,并且是在 "func(" 前面不出现 “def\s+”模式的字符串,所以首先考虑使用向后看的方法,即负向后行匹配方式来应用于line1,即 re.findall(r"(?<!def?)\s*func\(", line1) (def后带一个空格),预期不会得到匹配的内容,即将会得到结果为 [] 空列表,但是实际得到的却是:

>>> re.findall(r"(?<!def )\s*func\(", line1)
[‘???func(‘]

"func"前为三个空格;这是为什么呢?原因是re引擎会去尝试找到一个 "\s*func\(" 模式的字符串,并且在这个字符串前面不会出现 "def?" 字符串(def后有一个空格),包含三个前置空格的 "???func(" 正好就能满足条件,首先它能够匹配 "\s*func\(" 的模式,并且这个字符串前面的是不含空格的 "def" 字符串,而不是在负向后行匹配断言(?<!def?)中所表示的"def?"(def后包含一个空格)。

那么尝试将负向后行匹配断言中def后面的空格去掉,即修改为 re.findall("(?<!def)\s*func\(", line1),结果又如何呢?实测结果是:

>>> re.findall(r"(?<!def)\s*func\(", line1)
[‘??func(‘]

"func"前为两个空格——仔细分析会发现这是因为原因是re引擎会去尝试找到一个“\s*func\(”模式的字符串,并且在这个字符串前面不会出现“def”字符串(def后没有空格),包含2个前置空格的 "??func(" 就正好满足条件,因为包含2个空格的 "??func(" 字符串能够匹配 "\s*func\(" 的模式,并且这个字符串前面的是后接了一个空格的 "def?" 字符串,而不是在负向后行匹配断言pattern "(?<!def)" 中所表示的没有后接空格的"def"


再尝试在负向后行匹配断言中在def后面使用\s+,即修改为  re.findall("(?<!def\s+)func\(", line1),从逻辑上来说涵盖了def和func之间不定长空格数量的情况,但是因为后行方式的两种断言要求匹配表达式字符串的内容为定长,所以这样的执行结果将会是Python解释器报错  "error: look-behind requires fixed-width pattern"


——所以,对于在 def 和 func之间包含了三个空格的line1,要想用负向后行断言来实现匹配,必须使用def后包含三个空格而func前无空格的 
 re.findall("(?<!def???)func\(", line1) 或  def后无空格但是func前有三个空格 re.findall("(?<!def)\s{3}func\(", line1) 的形式,即需要准确地知道def 和 func之间究竟有多少个空格,但是因为python语法中并没有规定def 和 函数名之间的空格数,所以使用负向后行断言方式实际上是无法准确匹配到def和func之间空格数未知的字符串的。

于是我们只能考虑采取负向前行断言来实现精确匹配,即 re.findall("^(?!.*def\s+func\().*func\(", line1),执行得到的结果为空列表[],同时我们使用正向前行断言来验证我们的匹配字符串使用正确,即执行 re.findall("^(?=.*def\s+func\().*func\(", line1),得到的结果为 [‘def   func(‘] 

>>> re.findall("^(?!.*def\s+func\().*func\(", line1)
[]
>>> re.findall("^(?=.*def\s+func\().*func\(", line1)
[‘?def???func(‘]

 —— 这说明我们的负向前行断言正好精确匹配到了 def 和 func 之间存在不定长度空格数的情况。
此处再来解析一下这里的负向前行断言的含义:
"^(?!.*def\s+func\().*func\("  表示从line的起始位置开始向后搜索,不允许出现 ".*def\s+func\(" 这种模式的字符串,但又尝试在此前提下寻找能够匹配  ".*func\(" 模式的字符串,这也就正是我们所希望的过滤条件。此处的 (?!.*def\s+func\() 是不消耗任何字符串长度的

这里需要特别注意的是另外两种与 re.findall("^(?!.*def\s+func\().*func\(", line1) 很接近的匹配模式:

1、如果使用的是  re.findall("^(?!def\s+func\().*func\(", line1),执行的结果将不会是预期的空列表,而是 [‘ def???func(‘],这是因为这种写法,RE引擎将会尝试搜索是否存在起始位置开始不是 "def\s+func\(" 而是 ".*func\(" 的字符串,但是line1中的"def"前面正好有一个空格,所以RE引擎发现从开始位置处搜索到的是带一个前置空格的 "?def\s+func\(" 模式的字符串,而不是负向前表达式中没有空格的 "def\s+func\(" 模式字符串,所以会匹配成功。

2、如果使用的是  re.findall("(?!.*def\s+func\().*func\(", line1),执行的结果也不会是预期的空列表,而是 [ ‘ef???func(‘ ],这是因为如果pattern中没有了^字符,就不是要求line1从开始就必须满足匹配条件,而是line1中任意位置能够满足匹配条件都可以,所以line1中的 "ef???func(" 这个字符串就能满足匹配条件 

—— 综上所述,建议尝试正则匹配“在xxx之前不出现yyy,且 xxx 和 yyy 之间可能存在其他不定长字符串”的场景时,优先考虑使用负向前行断言; 对于能够确定xxx和yyy之间是定长的情况下,可以使用负向后行断言


再例如考虑在line3中匹配 "func(" 字符串的时候,要求在 "func(" 前不能出现#符号,即要求func函数的调用语句没有被注释掉,因为 # 和 func( 之间的字符长度完全是随机未知的,故应该使用负向想前行断言方式的 re.findall("^(?!.*#.*func\().*func\(", line3),而不是 re.findall("(?<!#).*func\(",line3)

MyEssay 之 Python正则表达式 —— 四种断言扩展的理解