写爬虫,怎么可以不会正则呢?
Python大本营?8月19日
作者 | 丹枫无迹?
来源 | 大龄码农的Python之路(ID:gl-1573)
导读:正则在各语言中的使用是有差异的,本文以 Python 3 为基础。本文主要讲述的是正则的语法,对于 re 模块不做过多描述,只会对一些特殊地方做提示。
很多人觉得正则很难,在我看来,这些人一定是没有用心。其实正则很简单,根据二八原则,我们只需要懂 20% 的内容就可以解决 80% 的问题了。我曾经有几年几乎每天都跟正则打交道,刚接手项目的时候我对正则也是一无所知,花半小时百度了一下,然后写了几个 demo,就开始正式接手了。三年多时间,我用到的正则鲜有超出我最初半小时百度到的知识的。
1、正则基础
1.1、基础语法
(1)常用元字符
(2)限定词(又叫量词)
(3)常用反义词
(4)字符族
以上便是正则的基础内容,下面来写两个例子看下:
s?='123abc你好'
re.search('\d+',?s).group()
re.search('\w+',?s).group()
结果:
123
123abc你好
是不是很简单?
1.2、修饰符
修饰符在各语言中也是有差异的。
Python 中的修饰符:
(1)re.A
修饰符?A?使?\w?只匹配 ASCII 字符,\W?匹配非 ASCII 字符。
s?='123abc你好'
re.search('\w+',?s,?re.A).group()
re.search('\W+',?s,?re.A).group()
结果:
123abc
你好
但是描述中还有?\d?和?\D,数字不都是 ASCII 字符吗?这是什么意思?别忘了,还有全角和半角!
s?='0123456789'#?全角数字
re.search('\d+',?s,?re.U).group()
结果:
0123456789
(2)re.M
多行匹配的模式其实也不常用,很少有一行行规整的数据。
s='aaa\r\nbbb\r\nccc'
re.findall('^[\s\w]*?$',?s)
re.findall('^[\s\w]*?$',?s,?re.M)
结果:
['aaa\r\nbbb\r\nccc']#?单行模式
['aaa\r','bbb\r','ccc']#?多行模式
(3)re.S
这个简单,直接看个例子。
s='aaa\r\nbbb\r\nccc'
re.findall('^.*',?s)
re.findall('^.*',?s,?re.S)
结果:
['aaa\r']
['aaa\r\nbbb\r\nccc']
(4)re.X
用法如下:
rc?=?re.compile(r"""
\d+?#?匹配数字
#?和字母
[a-zA-Z]+
"""
,?re.X)
rc.search('123abc').group()
结果:
123abc
注意,用了X修饰符后,正则中的所有空格会被忽略,包括正则里面的原本有用的空格。如果正则中有需要使用空格,只能用\s代替。
(5)(?aiLmsux)
修饰符不仅可以代码中指定,也可以在正则中指定。(?aiLmsux)?表示了以上所有的修饰符,具体用的时候需要哪个就在 ? 后面加上对应的字母,示例如下,(?a)?和?re.A?效果是一样的:
s?='123abc你好'
re.search('(?a)\w+',?s).group()
re.search('\w+',?s,?re.A).group()
结果是一样的:
123abc
123abc
1.3、贪婪与懒惰
当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。
s?='aabab'
re.search('a.*b',?s).group()#?这就是贪婪
re.search('a.*?b',?s).group()#?这就是懒惰
结果:
aabab
aab
简单来说:
所谓贪婪,就是尽可能多的匹配;
所谓懒惰,就是尽可能少的匹配。
*、+、{n,}?这些表达式属于贪婪;
*?、+?、{n,}??这些表达式就是懒惰(在贪婪的基础上加上??)。
2、正则进阶
2.1、捕获分组
注意:在其他语言或者网上的一些正则工具中,分组命名的语法是?(?<name>exp)或(?'name'exp),但在 Python 里,这样写会报错:This named group syntax is not supported in this regex dialect。Python 中正确的写法是:(?P<name>exp)
示例一:
分组可以让我们用一条正则提取出多个信息,例如:
s =?'姓名:张三;性别:男;电话:138123456789'
m?=?re.search('姓名[::](\w+).*?电话[::](\d{11})',?s)
if?m:
name?=?m.group(1)
phone?=?m.group(2)
print(f'name:{name},?phone:{phone}')
结果:
name:张三,phone:13812345678
示例二:
(?P<name>exp)?有时还是会用到的,?(?P=name)?则很少情况下会用到。我想了一个?(?P=name)?的使用示例,给大家看下效果:
s?='''
张三
30
138123456789
'''
pattern?=r'<(?P<name>.*?)>(.*?)</(?P=name)>'
It?=?re.findall(pattern,?s)
结果:
[('name',?'张三'),?('age',?'30'),?('phone',?'138123456789')]
2.2、零宽断言
注意:正则中常用的前项界定(?<=exp)和前项否定界定(?<!exp)在 Python 中可能会报错:look-behind requires fixed-width pattern,原因是 python 中前项界定的表达式必须是定长的,看如下示例:
(?<=aaa)#?正确
(?<=aaa|bbb)#?正确
(?<=aaa|bb)#?错误
(?<=\d+)#?错误
(?<=\d{3})#?正确
2.3、条件匹配
这大概是最复杂的正则表达式了。语法如下:
此语法极少用到,印象中只用过一次。
以下示例的要求是:如果以 _ 开头,则以字母结尾,否则以数字结尾。
s1?='_abcd'
s2?='abc1'
pattern?='(_)?[a-zA-Z]+(?(1)[a-zA-Z]|\d)'
re.search(pattern,?s1).group()
re.search(pattern,?s2).group()
结果:
_abcd
abc1
2.4、findall
Python 中的?re.findall?是个比较特别的方法(之所以说它特别,是跟我常用的 C# 做比较,在没看注释之前我想当然的掉坑里去了)。我们看这个方法的官方注释:
Returna?listofall?non-overlapping?matchesinthestring.
Ifoneormore?capturing?groups?are?presentinthe?pattern,return
a?listofgroups;?this?will?be?a?listoftuplesifthe?pattern
has?more?than?onegroup.
Empty?matches?are?includedinthe?result.
简单来说,就是
如果没有分组,则返回整条正则匹配结果的列表;
如果有 1 个分组,则返回分组匹配到的结果的列表;
如果有多个分组,则返回分组匹配到的结果的元组的列表。
看下面的例子:
s='aaa123bbb456ccc'
re.findall('[a-z]+\d+',?s)#?不包含分组
re.findall('[a-z]+(\d+)',?s)#?包含一个分组
re.findall('([a-z]+(\d+))',?s)#?包含多个分组
re.findall('(?:[a-z]+(\d+))',?s)#??:?不捕获分组匹配结果
结果:
['aaa123',?'bbb456']
['123',?'456']
[('aaa123',?'123'),?('bbb456',?'456')]
['123',?'456']
零宽断言中讲到 Python 中前项界定必须是定长的,这很不方便,但是配合 findall 有分组时只取分组结果的特性,就可以模拟出非定长前项界定的效果了。
结语
其实正则就像是一个数学公式,会背公式不一定会做题。但其实这公式一点也不难,至少比学校里学的数学简单多了,多练习几次也就会了。