Python正则表达式

发布时间 2023-04-09 16:48:02作者: Circle_Wang

  本章将介绍Python中正则表达式,本文将会基于Python的标准库re模块讲解正则表达式。

1、正则表达式的基本使用

 1.1、re.search(正则表达式,待匹配文本)

  我们可以使用re.search查询待匹配文本中是否存在可以匹配上的字符串,直接上例子。

import re

match = re.search(r'python', "我爱python")
print(match)
print(type(match))

  我们使用.search方法查询字符串中是否有‘python’这个匹配得上。该方法会返回一个re.Match对象(re模块中定义的类),直接打印这个match将会看到最近一个匹配上的字符串所在位置区间。比如上述代码打结果是:

   span=(2,8)表示匹配上的位置是在原字符串的2到7位置[2, 8),我们可以直接使用索引 str[2:8] 从原来的字符串中提取出匹配上的字符串。需要注意的是re.search方法只会匹配上最近的一个,如果位置8之后仍然有可以匹配的内容,也不会进行匹配了。如果从头到尾都没有可以匹配的对象,那么re.search方法将会返回None对象。

  re.Match对象有许多方法供我们使用,最常使用的就是.group()方法,它将返回包含匹配文本的字符串。

  • Match.start()方法:提供原始字符串中匹配开始的位置
  • Match.end()方法:提供原始字符串中匹配结束的位置
import re

match = re.search(r'python', "我爱python, python")
print(match.group())
print(match.start())
print(match.end())

  以上代码的返回结果是:

 1.2、re.findall / re.finditer(正则表达式,待匹配文本)

  上面提到re.search的的一个限制是它仅仅返回最近的一个匹配,如果一句话中我们想得到所有的匹配结果,我们需要使用re.findall或者re.finditer。不过需要注意的是re.findall返回的是一个列表,其中元素只是匹配上的字符串(并不是re.Match对象)。re.finditer返回的是一个生成器,我们可以通过for循环这个生成器中的元素,值得注意的是与re.findall不同,这些元素都是re.Match对象。

import re

match1 = re.findall(r'python', "我爱python, python")
match2 = re.finditer(r'python', "我爱python, python")
print(match1)
print(match2)

for i in match2:
    print(i)

  运行结果如下:

   可以看到for循环中打印出了每个元素对象都是re.Match。

2、正则表达式使用规则

  上一节我们了解了如何使用正则表达式(仅仅使用了匹配纯文本的方式),本节将介绍正则表达式的如何使用指定的匹配文本模式完成匹配。

 2.1、[] 字符组

  通过[]我们可以指定,一个字符可以与可能出现的字符进行匹配。比如我认为big和pig我都希望匹配得上,那么我们就可以使用[]的方式,用于表示一个字符组。

import re

match = re.findall(r'[pb]ig', "我很喜欢big的pig")
print(match)

  我们采用findall的方式,查看匹配结果,上述代码结果如下。

   我们成功匹配上了”big“和”pig“。需要注意的是[]中每一个字符之间没有空格,并且只表示一个字符,如果字符中有特殊符号,那我们需要使用\进行转义。

  一些常见的字符组辉很大,比如我想把所有数字都放到一个字符组中,那我需要使用[1234567890],这显然很繁琐,因此Python将一些很常见的字符组进行了简化,比如:

  • [0-9]: 表示匹配任何数字字符
  • [a-z]:表示匹配任意小写字符
  • [A-Z]: 匹配任意大写字符

  当然我们也可以使用一部分,比如[0-2]表示,当前匹配位置为0,1,2这三个数字都可以。而且我们还可以组合,比如我们想匹配1-9和a-c,那我们可以写成[1-9a-c]。

  在某些情况下,我们并不想匹配字符组里的字符,比如我希望我当前匹配位置不要出现3-9之间的任何数字(换句话说只要不是3,4,5,6,7,8,9这些字符就可以),我们可以使用^取反符号,写成[^3-9]。看一个小例子:

import re

match = re.findall(r'[^2-3]ig', "我很喜欢pig的3ig")
print(match)

  我们这里使用取反,表示该位置不要出现2,3这两个数字,匹配结果如下:

 2.2、\d,\s,\b,\w

  我们还有一些快捷的匹配方式,比如\d表示匹配数字字符,意味着这个位置可以式任意的0-9的数字,类似的还有以下:

  • \d:表示匹配数字字符
  • \w: 表示匹配任何单词字符(空格、标点符号这些不算是单词字符),不规定语言(单词字符可以是汉字,也可以是英文字母的一个字符)
  • \s: 表示匹配空白字符(空格,tab,换行)
  • \b: 表示匹配开始或者结尾

  上述快捷方式中,需要提一下的是\b,其表示匹配开始或者结尾,而开始或者结尾的标志一般是空格或者换行,看例子:

import re

match1 = re.findall(r'\bpig\b', "pig")
match2 = re.findall(r'\bpig\b', "dpig")
match3 = re.findall(r'\bpig\b', "d pig")
print(match1)
print(match2)
print(match3)

  我们这里使用同一个正则表达式,但是使用的是不同的字符串,最后匹配的结果如下:

   可以看出第二个匹配没有匹配上,那是因为"dpig"中虽然包含字符串”pig“,并不是以p为开始,以g为结尾的,而match3中在d与pig之间加了一个空格,则可以完成匹配。其实这种方式称为词边界字符快捷方式。

 2.3、任意字符: .

  如果当前的匹配位置,可以匹配任意字符,那么我们可以使用 . 来进行占位,表示当前位置可以匹配任意字符(但必须是有一个字符)。

import re

match1 = re.findall(r'pyth.n', "pythkn")
match2 = re.findall(r'py.h.n', "python")
match3 = re.findall(r'py...n', "py thn")
match4 = re.findall(r'python.', "python")
print(match1)
print(match2)
print(match3)
print(match4)

  运行结果如下:

  

  我们需要注意的是match3和match4。match3里 . 实际匹配的是空格。match4并没有匹配结果,是因为 . 必须有一个字符与之匹配,而原始字符串中当匹配完n后不再具有字符,因此.并没有字符与之匹配,因此match4没有匹配结果。

 2.4、可选字符:?

  前面提到的匹配模式都是1:1匹配,即每一个位置都有需要进行匹配。但很多时候我们也需要同时匹配出”color“和”colour“,前文提到的方式便没法解决。我们可以在一个正则表达式中使用?表示其前面的一个字符(字符组)是可选的(允许出现0次与1次)。看例子:

match1 = re.findall(r'colou?r', "colour")
match2 = re.findall(r'colou?r', "color")
match3 = re.findall(r'colo[upa]?r', "coloar")
match4 = re.findall(r'colo[upa]?r', "color")
print(match1)
print(match2)
print(match3)
print(match4)

  运行结果如下:

  我们看到所有的正则表达式都匹配到了结果。对于match1和match2来说,由于正则表达式中u字符后面嗲有?,则表示u这个字符可以出现1次,也可以不出现,因此colour和color都是满足条件的。对于match3和match4来说,[upa]这个字符组可以出现一次也可以不出现,因此coloar和color也是符合条件的。

 2.5、重复:{}、*

  有时候我们希望某个字符(或者字符组)重复多次(比如我希望匹配pooooog,这里的o重复了多次),针对这个问题我们当然可以在正则表达式中多写几次,但re模块提供了更简单的方式即使用{N}。这里的N表示重复的次数。比如我poooooog可以使用r”po{6}g“匹配得到。

  {}除了使用N来单独表示重复次数,也可以使用{N,M}的方式,表示重复区间,即重复的次数在N到M之间(这里包含有N,M边界的),看例子:

match1 = re.findall(r'po{1,4}g', "pg")
match2 = re.findall(r'po{1,4}g', "pog")
match3 = re.findall(r'po{,4}g', "pg")
match4 = re.findall(r'po{1,4}g', "pooog")
print(match1)
print(match2)
print(match3)
print(match4)

  我们需要注意的match3,这里我们省略了重复的下界,则表示的是{0,4}最低可以不出现(类似的我们可以省略上界,比如{1,},表示字符可以出现"无穷次")。上面代码的结果如下:

   由于match1中要求o必须出现一次,但是”pg“中p后面并没有o因此匹配结果为空。

  其实我们发现2.4节中讲到的?其实就是{0,1}的一个简写。还有一个常用的缩写是*,表示{0,},也就是前一个字符可以出现0次或者无限多次

3、分组

  第二节中我们基本介绍了正则表达式的一些常见匹配模式,但是实际上一些复杂的正则表达式还需要我们掌握分组这个概念。正则表达式提供了一个机制,可以将表达式进行分组,当使用分组时,除了获得整个匹配以外,还可以在匹配中选择单独的一个组。我们在正则表达式中使用 () 来表示分组,我们直接看例子:

import re

match = re.search(r'(\d{3})-(\d{6})', "023-820820 / circle_wang")
print(match.group())
print(match.groups())

  这里有一个新的方法Match.groups(),前面我们提到re.Match对象中的group()方法会返回匹配的结果,这里的groups()方法将会返回分组的结果(以元组的形式返回,元组的每一个元素就是分组得匹配到的结果)。比如这里我们在正则表达式中有两个组,一个是\d{3}表示匹配3个数字,还有一个是\d{4}表示匹配4个数字,使用match.groups()我们将得到这两个匹配的结果。上述代码完整结果如下:

   可以看到match.group()返回了完整的匹配内容,而match.groups()则将分组匹配的结果依次放入到了元组中返回。

 3.1、分组索引

  我们可以单独的获取某个分组的内容(除了先获得所有分组结果再进行索引),我们可以使用match.group(index)的方式获得某个分组结果。这里有两个注意的点,第一是我们使用的是group(index),而不是groups(index)。第二点是,分组的index是从1开始的,并不是从0开始索引,比如上面例子中我们想获得‘023’这个分组内容,我们可以使用下述代码:

import re

match = re.search(r'(\d{3})-(\d{6})', "023-820820 / circle_wang")
print(match.group(1))

  结果如下:

   其实match.group(0)表示的就是整个匹配结果,虽然我们前文一直使用的是match.group(),省略了默认的0,但实际上我们依然可以使用match.group(0)来得到整个正则表达式的匹配结果。

 3.2、命名分组

  除了按位置编号的分组以外,我们还可以使用命名分组机制。语法是在 ( 后紧跟?P<分组名字>,采用这种方式我们就可以使用 match.group(分组名字)得到对应分组的匹配结果,比如我们把上面例子中的两个分组分别进行命名:

match1 = re.search(r'(?P<区号>\d{3})-(?P<电话号>\d{6})', "023-820820 / circle_wang")
print(match1.group("区号"))
print(match1.group("电话号"))

  运行的结果是:

 

   在维护角度上来说,命名分组非常重要。如果在后续代码中你需要引用某个分组的结果,但如果使用位置进行索引,那么当你的正则表达式发生更改时,你需要保证你的位置索引仍然是你所期望的分组。使用命名分组的方式,可以保证只要分组名没有改变那你索引得到的结果不会因为新增加分组而发生改变。

  对于分组索引,我们还可以使用match.groupdict()得到一个字典,这个字典的键为分组名,值为分组匹配的结果。需要注意的是match.groupdict()返回的字典中只包括采用命名分组的结果,如果有的分组没有命名,那么将不会在字典中(这部分没有命名的分组依然可以使用match.groups()查看)。

 3.3、引用分组

  我们在正则表达式中可能会引用到前面表达式中的分组结果,这一部分在实际应用中比较少,所以此处贴一个关键词,如果需要可以自行百度。

4、先行断言(?=)、(?!)

  正则表达式中存在一种机制,能够基于之后的内容是否存在而决定接收或者拒绝一个匹配,并不需要接下来的内容作为匹配的一部分(简单来说,就是如果发现了某个字符出现或者不出现,那么我可以直接结束匹配)。直接看例子:

import re

match1 = re.search(r'in(?!b)ally', "finally")
match2 = re.search(r'in(?=b)ally', "finally")
match3 = re.search(r'n(?=a)', "na")
print(match1)
print(match2)
print(match3)

  (?!b)表示当前位置(n的面)是否不等于b,如果不等于b,则继续匹配,如果等于b则直接停止匹配,返回None结果。而(?=b)则表示当前位置是否等于b,如果等于b则继续匹配,如果不等于b则停止匹配,直接返回None结果。需要注意的是这个判断是否等于b并不在会阻止后续匹配当前位置,比如以match1的匹配为例,当Python解释器匹配到in后,将会判断原字符串中n后面是否是b,如果不是b那么可以继续匹配,由于此处finally的n后是a,因此满足条件,可以继续匹配。此时正则表达式中我们将会比较正则表达式中的a是否等于finally中的a,虽然刚才我们判断过finally中的n后是否为b,但不会影响正则表达式后续的匹配。也就是说如果断言判断为True,那么我们完全可以将这个断言括号部分去掉,再来看整个表达式。因此上述代码结果为:

 

 5、编译正则表达式

  有时候我们需要将一个复杂的正则表达式进行编译,以方便我们随时随地的可以重复使用。re.compile(正则表达式)可以将一个一句正则表达式变成一个re.Pattern对象,我们可以使用对象的.search()方法对字符串进行匹配。

import re

regex = re.compile(r'(?P<区号>\d{3})-(?P<电话号>\d{6})')
match1 = regex.search("023-820820 / circle_wang")
print(match1.group())
print(match1.groups())

  我们将正则表达式: (?P<区号>\d{3})-(?P<电话号>\d{6}) 保存为一个re.Pattern对象,我们可以使用这个对象,在任何地方对字符串进行匹配从而得到re.Match对象。上面代码结果如下:

 

   可以得到我们预期的结果。

  这种将正则表达式编译成对象的方式非常方便我们对复杂正则表达式的运用,我们也可以通过这种方式将re.Pattern对象进行跨程序的使用。