查看原文
其他

正则表达式中零宽断言的用法

崔庆才 Python爱好者社区 2019-04-07


Python3网络爬虫精华实战视频教程

点击上图立即了解学习,限时优惠价299

作者:崔庆才,Python技术控,爬虫博文访问量已过百万。喜欢钻研,热爱生活,乐于分享。《Python3网络爬虫开发实战》书籍作者。
个人博客:静觅 | http://cuiqingcai.com


了解了正则表达式,想必一般情况下的匹配都不会出现什么问题,但是如果一些特殊情况,可能需要用到一些更高级的正则表达式匹配操作,本节我们来说明一下正则表达式的一个较常用又比较重要的知识点——零宽断言。

实例引入

首先我们来看一个例子,这里有一段问答对话:

问:我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? 答:在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 问:为什么我看到的卡号输入框显示为*符号? 答:您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。 问:看了以上几个问题,还是不能登录,怎么办? 答:您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 问:无法出现个人网上银行大众版登录界面。 答:这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。 问:我在输入账号和卡号时,总出错,该怎样输? 答:存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。 问:我的存折没有设密码,怎样在个人网上银行大众版中查询余额? 答:存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。

我们需要将这段对话中的问题和答案对提取出来,即提取出如下内容:

Q:我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?

A:在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。

Q:为什么我看到的卡号输入框显示为*符号?

A:您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。

...

如果要用 Python 实现的话,那么我们很可能自然而然想到 split() 或 findall() 方法,如果用 split() 方法,我们可能会这么写:

import re results = re.split('问:| 答:', text)
for index, result in enumerate(results[1:]):    print(('Q' if index%2 == 0 else 'A') + ': ' + result)

这里 split() 方法的第一个参数传入了 `问:| 答:` 这个正则表达式,意思是将这段话用 `问:` 或者 `答:` 分开,这个功能是正则表达式对字符串进行分割的方法,相比直接字符串的 split() 方法功能更为强大。这里其实得到的结果是一个列表,长度是一个奇数,如果我们把 results 打印出来,结果是这样的:

['', '我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?', '在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 ', '为什么我看到的卡号输入框显示为*符号?', '您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。 ', '看了以上几个问题,还是不能登录,怎么办?', '您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 ', '无法出现个人网上银行大众版登录界面。', '这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。 ', '我在输入账号和卡号时,总出错,该怎样输?', '存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。 ', '我的存折没有设密码,怎样在个人网上银行大众版中查询余额?', '存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。 ']

这是因为我们分割使用的字符本身就处于整个文本的字符,所以一上来就找到了分割的标志 `问:`,所以它左侧的结果就是空字符串了,所以最终得到的结果第一个内容就是空字符串,后续的内容便是正常的一问一答的短句。所以这里我们还需要对结果进行切片操作,去除第一个元素,然后将其遍历打印输出,最终结果如下:

Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? A: 在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 Q: 为什么我看到的卡号输入框显示为*符号? A: 您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。 Q: 看了以上几个问题,还是不能登录,怎么办? A: 您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 Q: 无法出现个人网上银行大众版登录界面。 A: 这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。 Q: 我在输入账号和卡号时,总出错,该怎样输? A: 存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。 Q: 我的存折没有设密码,怎样在个人网上银行大众版中查询余额? A: 存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。

这样确实没问题,我们可以顺利地提取出来,但是总感觉这个解法并不那么优雅,因为我们这里是将问题和答案的内容都单独切出来了,并没有将问答对一块提取,而且 split() 方法返回的结果的第一个元素还不是我们想要的结果,所以还需要进行一些切片操作来去除,所以整个写法感觉实现起来并不完美。

所以我们又想到了 findall() 方法,这时我们会这么写:

import re results = re.findall('问:(.*?) 答:(.*?)', text, re.S)
for result in results:    print('Q: ' + result[0], 'A: ' + result[1], sep='\n')

表面上看似乎是把问题答案对用正则表示出来了,而且使用了非贪婪匹配,但是很明显,在末尾我们并没有指定匹配的终点,所以整个的结果就会导致回答是完全匹配不到的,运行结果如下:

Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? A: Q: 为什么我看到的卡号输入框显示为*符号? A: Q: 看了以上几个问题,还是不能登录,怎么办? A: Q: 无法出现个人网上银行大众版登录界面。 A: Q: 我在输入账号和卡号时,总出错,该怎样输? A: Q: 我的存折没有设密码,怎样在个人网上银行大众版中查询余额? A:

好,那么我们加上匹配的终点吧,以下一个的 `问:` 作为我们正则表达式匹配的终点总可以了吧?所以我们可能会改写成这样子:

import re results = re.findall('问:(.*?) 答:(.*?)问:', text, re.S)
for result in results:    print('Q: ' + result[0], 'A: ' + result[1], sep='\n')

这样写似乎看起来是可以了,但结果却是这样的:

Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? A: 在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 Q: 看了以上几个问题,还是不能登录,怎么办? A: 您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 Q: 我在输入账号和卡号时,总出错,该怎样输? A: 存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。

结果只剩三个问题答案对了,有三个问答对被“吃”掉了,其实这是因为我们的正则表达式最后加了 问:的缘故,findall() 方法它会查找所有符合正则表达式的结果,但其中匹配的时候它内部也是有一个查找索引在扫描的。在查找第一个符合要求的结果时,由于我们是根据正则表达式结尾的 问:来作为结束标志,所以在找到第一个符合要求的结果时,我们的查找索引就已经移动到了第二个问答对开头的 问: 上面,即查找索引就已经进入到了第二个问答对的位置了,而在下一次查找符合要求的结果时,索引会继续往后移动进行扫描,所以它是从第二个问答对的 问: 后面继续扫描的,所以对于第二个问答对,实际上已经被割裂了,所以它只能查找到第三个问答对的时候才可以发现符合正则表达式的内容。因此,我们可以观察到,返回的结果只是第一、三、五三个问答对。

所以,如果我们想要用该方法找到完整的留个问答对,就需要用到零宽断言了。

解法如下:

import re results = re.findall('问:(.*?) 答:(.*?)(?=问:|\Z)', text, re.S)
for result in results:    print('Q: ' + result[0], 'A: ' + result[1], sep='\n')

运行结果如下:

Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? A: 在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 Q: 为什么我看到的卡号输入框显示为*符号? A: 您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。 Q: 看了以上几个问题,还是不能登录,怎么办? A: 您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 Q: 无法出现个人网上银行大众版登录界面。 A: 这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。 Q: 我在输入账号和卡号时,总出错,该怎样输? A: 存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。 Q: 我的存折没有设密码,怎样在个人网上银行大众版中查询余额? A: 存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。

这里我们实际上是使用了 (?=)这样的形式来构建了整个表达式,等号后面的内容是 问:或者结束符 \Z,这样其实就保证了在匹配的时候,查找索引不会继续向后移,但这也同时标志了结束标志,因此它就可以查找到完整的内容了。

零宽断言

零宽断言,顾名思义,是一种零宽度的匹配,它匹配的内容不会保存到匹配结果中,表达式的匹配内容只是代表了一个位置而已,如标明某个字符的右边界是怎样的构造。

在前面我们使用了 ?=来进行了实例讲解,这是其中一个用法,另外还有 ?<=?!?<!,下面我们来依次进行讲解说明。

  • ?=代表零宽度正预测先行断言,它断言自身出现的位置的后面可以匹配后面跟的表达式。

  • ?<=代表零宽度正回顾后发断言,它断言自身出现的位置的前面可以匹配后面跟的表达式。

  • ?!代表零宽度负预测先行断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。

  • ?<!代表零宽度负回顾后发断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。

?=

首先我们来看下 ?=的用法,它断言自身出现的位置的后面可以匹配后面跟的表达式。

比如我们这里有这样的一个字符串:

str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'

在这里我们想把我的个人邮箱这句话和个人邮箱单独摘出来,假如我们不使用零宽断言的话,我们需要给个人邮箱后面这一句加一个结束标识符或者单独匹配邮箱作为标识符,我们可能会这么写:

import re str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('我的个人邮箱是(.*?),个人博客', str) print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

在正则表达式的最后我们加了,个人博客作为匹配的结束符,然后邮箱部分用非贪婪匹配的模式进行匹配,我们看下运行结果:

整句结果:我的个人邮箱是cqc@cuiqingcai.com,个人博客
第一个匹配结果:cqc@cuiqingcai.com

我们可以看到第一个匹配结果成功得到了邮箱信息,但是我们看整句结果缺并不理想,它多匹配了我们加入的结尾标识,并没有得到正常的一句话。

这时候如果我们改用 ?=来匹配,结果就不会带有此标识符了,改写如下:

import re str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('我的个人邮箱是(.*?)(?=,个人博客)', str) print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

在这里我们将结尾标识符改成了 (?=,个人博客) ,这样就将此部分内容作为零宽度匹配,它代表后面需要跟 ,个人博客,但是它不会出现在匹配结果中。

运行结果如下:

整句结果:我的个人邮箱是cqc@cuiqingcai.com
第一个匹配结果:cqc@cuiqingcai.com

可以看到整句结果中已经没有无用的后缀字符了。

?<=

接下来我们再看下 ?<=的用法,它代表零宽度正回顾后发断言,其实就是匹配前面的标识,比如这里我们还是以上面的例子为例,匹配出个人博客这句话,代码如下:

import re str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('(?<=,)个人博客是(.*?)(?=,)', str) print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

这里我们在个人博客 前面加了一个零宽断言的逗号符号作为开头,使用的就是 ?<=,句子结尾是用的 ?=,这样前后的标识都不会匹配到了,运行结果如下:

整句结果:个人博客是cuiqingcai.com 第一个匹配结果:cuiqingcai.com

可以看到得到的整句结果也是完整的一句话。

?!

?!代表代表零宽度负预测先行断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。也是用来匹配后面的文本,但这里是取反,它指定了后面出现的内容不匹配该标识,我们在前面的例子基础上修改如下:

import re str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('我的个人邮箱是(.*?)(?!,个人公众号)(?=,个人博客)', str) print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

本来是 (?=,个人博客)的标识符,不过这里我们使用 ?!来指定了另一个标识符,个人公众号,这就代表这句话后面跟的需要是(?=,个人博客)而不是,个人公众号,运行结果如下:

整句结果:我的个人邮箱是cqc@cuiqingcai.com
第一个匹配结果:cqc@cuiqingcai.com

?<!

?<!代表零宽度负回顾后发断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。我们在前面的例子基础上加以修改:

import re str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('(?<=,)(?<!。)个人博客是(.*?)(?=,)', str) print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

这里我们写了 ?<!标识符,后面跟了一个句号,这代表前面不应该出现句号。

运行结果如下:

整句结果:个人博客是cuiqingcai.com 第一个匹配结果:cuiqingcai.com

常用用法

其实上面的示例中我们使用了 search() 方法进行了内容匹配,其实这并不常用,因为一般我们更关注的是匹配分组结果的内容,其实更多的用法是用在了 findall() 方法上,它用来匹配多个结果,也就类似于我们一开始的实例一样,这里我们还是以刚才的字符串为例,来输出一下个人邮箱、个人博客、个人公众号三个内容,代码如下:

import re str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
results = re.findall('个人(.*?)是(.*?)(?=,|\Z)', str)
for result in results:    print(result[0] + ': ' + result[1])

这里我们匹配了个人二字,然后后面跟了非贪婪匹配,然后加了一个字,最关键的是结尾标识符,这里必须要使用零宽断言才可以匹配出三个结果,这里匹配的内容是 ,|\Z,意思是匹配逗号或结束符。

运行结果如下:

邮箱: cqc@cuiqingcai.com
博客: cuiqingcai.com 公众号: 进击的Coder

这样我们就成功输出了邮箱、博客及公众号的内容了,匹配非常顺利方便。

结语

通过本节,我们应该大体可以了解了正则表达式中零宽断言的基本用法和适用场景,相信理解了零宽断言之后,我们再做正则匹配时会更加得心应手。

下图扫码或点击阅读原文

报名学习崔老师的网络爬虫课程

已经1800人加入学习

限时优惠价299!


点击“
阅读原文

”,立即加速爬虫技能修炼!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存