在前两篇连载文章中,我们学习了re模块的match()、search()、findall()方法,以及学习了使用正则表达式中常用的元字符、限定符、选择字符、中括号来搭配这些方法来灵活处理常见的数据匹配问题。这本篇文章分钟,我们将会进一步学习正则表达式中其他符合,包括令初学者非常头疼的分组问题。
排除字符
首先我们回顾一下上一篇连载文章中最后使用的例子:
pattern='[aA-zZ]+'
message='企业名称:CDA数据科学研究院\n邮箱:19185604 61@q q.com\n地址:北京市海淀区厂洼街3号2号楼2层\n网址:ww w.cd a.c n\n\企业名称:广州就学在线科技有限公司\n邮箱:981856 661@q q.com\n地址:广州市黄埔区护林路1198号516房\n网址:ww w.cd a.c n\n'
findall = re.findall(pattern, message)
print(findall)
out:'CDA', 'qq', 'com', 'www', 'cda', 'cn', 'qq', 'com', 'www', 'cda', 'cn']
通过模式字符串中的中括号,我们可以匹配到字符串中所有的英文字符串,但是如果反过来说,要提取所有非英文的字符串,如何提取?
这时,可以使用排除字符“^”,放在方括号中,表示排除的意思,只需要将其放在模式字符串的中括号以内的第一个字符位置即可:
pattern='[^aA-zZ]+'
message='企业名称:CDA数据科学研究院\n邮箱:191856 0461@q q.com\n地址:北京市海淀区厂洼街3号2号楼2层\n网址:w ww.c da.c n\n\企业名称:广州就学在线科技有限公司\n邮箱:9818566 61@q q.com\n地址:广州市黄埔区护林路1198号516房\n网址:ww w.c da.cn\n'
findall = re.findall(pattern, message)
print(findall)
out:['企业名称:', '数据科学研究院\n邮箱:1918560461@', '.', '\n地址:北京市海淀区厂洼街3号2号楼2层\n网址:', '.', '.', '\n企业名称:广州就学在线科技有限公司\n邮箱:981856661@', '.', '\n地址:广州市黄埔区护林路1198号516房\n网址:', '.', '.', '\n']
这是我们会发现,使用排除字符字符,就连换行符以及逗号等常见的分界符都被提取出来了。
可以看到,排除字符“^”的实质是把要排除的字符当做分隔符,一一提取要排除的字符。
令人头疼的分组
上面讲解了排除字符在正则表达式的中括号所起到的作用,与中括号相比,则表达式里面的小括号,对于很多初学的同学来说简直是噩梦般的存在:无处不在但是其所起到的作用往往让人摸不着头。
这一方面是由于没有关于这方面的太多的浅显易懂的学习资料,一方面是小括号这符号在正则表达式里面可以表现出两种不同的作用方式。 首先我们看小括号第一种起作用的方式:分组。
正则表达式里面所说的分组,跟很多编程语言中的groupby操作有着天壤之便。本着由浅入深的精神,我们先举一个简单的例子如下:
pattern='(six)th'
message='sixthfourth'
search=re.search(pattern,message)
search
search.group(0)
search.group(1)
out:<re.Match object; span=(0, 5), match='sixth'>
'sixth'
'six'
我们发现,使用模式字符串'(six)th'可以匹配出字符串'sixthfourth'中的‘sixth’。
如果调用返回的re.match对象中的.group(n)方法,我们会惊奇第发现sear ch.gr oup(0)返回来的就是第一个匹配出来的对象sixth’。
search.group(1)返回来的是另外一个匹配对象“six”!。也就是说,带有括号的模式字符串会匹配两次!
第一次是忽略小括号,直接使用模式字符串'sixth'来匹配出'sixth'。第二次是把模式字符串中的小括号中的字符串‘six’,当做是子模式字符串进行第二次匹配,得到匹配结果'six'。我们可以把括号以内的字符串当做是模式字符串的“子表达式”。
我们再举个例子,比如我们想要匹配字符串'sixthseventh'中的‘six’、‘sixth’、‘seven’、‘seventh’这四个字符串,我们使用re.findall()方法看看效果:
pattern='(six|seven)th'
message='sixthseventh'
findall=re.findall(pattern,message)
findall
out:['six', 'seven']
我们会发现,我们使用了小括号,小括号内部也使用了选择字符“|”,小括号右边还有“th”,但是依然没有把sixth和seventh匹配出来。
这其实是一个初学正则表达式的时候很多同学经常踩到的一个“坑”,那就是小括号在re.search()起作用的方式和在re.findall()起作用的方式不同。
re.findall()在识别模式字符串的时候,一旦碰到小括号,就会匹配小括号里面的模式字符串,而小括号以外的模式字符串会被忽略掉。
在上面的例子当中,模式字符串中的“th”在小括号以外的地方,这就是为什么前面例子当中re.findall()没有把sixth和seventh匹配出来的原因。
那么这种情况该怎么办?既然re.findall()方法只匹配小括号以内的模式字符串,那么我们可以在前面的模式字符串最外层再嵌套一层小括号就可以了,看效果:
pattern='((six|seven)th)'
message='sixthseventh'
findall=re.findall(pattern,message)
findall
out:[('sixth', 'six'), ('seventh', 'seven')]
我们会发现,‘six’、‘sixth’、‘seven’、‘seventh’这四个字符串都匹配上了,而且'sixth', 'six'用一个元组来“打包”在一起。
运算符优先级问题
上面的例子其实也可以反映出小括号可以限制选择字符"|"其中用范围,除此以外小括号还可以限定元字符的范围。这其实反映出在正则表达式式里面,小括号的运算符优先级是高于选择字符的,那么,正则表达式的所有运算符的优先级顺序是怎么样的呢?
我们可以看下表:
那么,既然正则表达式的小括号的优先级更高,那么又有什么作用?
我们可以使用它来搭配限定符去匹配一些连续多次重复出现的字符串,且每个字符串自身内部也有相同的规律,比如每次都是连续出现的日期。
首先举一个简单的例子,比如说我们要匹配特定格式的日期,找到日期字符串规律之后,我们把模式字符串定义为以下形式:
pattern=r'[0-9]{4}年[0-9]{1,2}月[1-9]{1,2}日'
message='今天是2020年03月1日,在2019年7月17日发生的一件事,让我记忆犹新'
findall=re.findall(pattern,message)
findall
out:['2020年03月1日', '2019年7月17日']
可以看到,由于上面出现的日期,其内部都是有规律的,因此我们把日期的这些内部规律写成模式字符串后才把所有日期都匹配出来。但是如果上面例子中的message中的日期有部分出现异常,每次日期都会重复两次出现,比如:
message='今天是2020年03月1日2020年03月1日,在2019年7月17日2019年7月17日发生的一件事,让我记忆犹新'
这种就是前面所说的,内部有规律,连续出现的次数也有规律的情况。
这个时候我们可以把前面的例子中的pattern内部使用小括号“打包在一块”,这小括号内的模式字符串其实就相当于用来匹配内部规律,变成这样:
pattern=r'([0-9]{4}年[0-9]{1,2}月[1-9]{1,2}日)'
然后在小括号外面的右边添加限定符,比如说异常日期都是连续出现两次,这连续两次也是一种规律,添加限定符{2},变成这样:
pattern=r'([0-9]{4}年[0-9]{1,2}月[1-9]{1,2}日){2}'
还没完!连续出现两次这种外部规律还需要用小括号“打包”起来,正如前面一小节给大家提醒,外部连续出现的规律的通过“外部小括号”来匹配,最终pattern变成这样:
pattern=r'(([0-9]{4}年[0-9]{1,2}月[1-9]{1,2}日){2})'
我们看一下效果:
pattern=r'(([0-9]{4}年[0-9]{1,2}月[1-9]{1,2}日){2})'
message='今天是2020年03月1日2020年03月1日,在2019年7月17日2019年7月17日发生的一件事,让我记忆犹新'
findall=re.findall(pattern,message)
findall
out:[('2020年03月1日2020年03月1日', '2020年03月1日'),
('2019年7月17日2019年7月17日', '2019年7月17日')]
这样子就可以将内部有规律,外部也有连续规律的字符串都匹配出来。