Python::re 模块 -- 在Python中使用正则表达式

时间:2022-08-09 12:42:32

前言

这篇文章,并不是对正则表达式的介绍,而是对Python中如何结合re模块使用正则表达式的介绍。文章的侧重点是如何使用re模块在Python语言中使用正则表达式,对于Python表达式的语法和详细的介绍,可以参考别的文章,这篇文章只是给出一些常用的正则表达式语法,以方便对re模块的使用进行讲解。

对正则表达式的介绍,可以参看这两篇文章:

正则表达式30分钟入门教程

正则表达式之道

注意:实验环境为 Python 3.4.3

正则表达式简介

正则表达式,又称正规表示式、正规表示法、正规表达式、规则表达式、常规表示法(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。

许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的。正则表达式通常缩写成“regex”,单数有regexp、regex,复数有regexps、regexes、regexen。

最初的正则表达式出现于理论计算机科学的自动控制理论和形式化语言理论中。在这些领域中有对计算(自动控制)的模型和对形式化语言描述与分类的研究。 1940年,Warren McCulloch与Walter Pitts将神经系统中的神经元描述成小而简单的自动控制元。 1950年代,数学家斯蒂芬·科尔·克莱尼利用称之为“正则集合”的数学符号来描述此模型。肯·汤普逊将此符号系统引入编辑器QED,然后是Unix上的编辑器ed,并最终引入grep。自此,正则表达式被广泛地使用于各种Unix或者类似Unix的工具,例如Perl。

Python提供了对正则表达式的支持,它内嵌在Python中,通过Python的re模块提供。

re模块提供了类似于Perl的正则表达式语法。

通过使用正则表达式,我们可以制定需要匹配的字符串的特定格式,然后从需要处理的字符串中提取我们感兴趣的字符串。Python中的re模块也提供了像sub()subn()split()这些方法来实现通过正则表达式来灵活地进行文本的替换和分割。

在Python中,正则表达式会被编译成一系列的字节码,然后由通过C编写的正则表达式引擎进行执行。

初尝正则表达式

下面,我们先看看正则表达是什么,了解下正则表达式是怎么处理字符串问题的。如果你以前没有接触过正则表达式,是正则表达式小白,那么可以初略的熟悉下这节内容,心里有个大概的了解。在接下来的几节中,我们将学习到正则表达式的一些知识,等学习完了有关正则的知识,可以再回过头来看看这些例子,加深下理解。

下面,我们简单的看看在Python中使用正则表达式的例子:

简答的字符串匹配操作


>>> import re
>>> p = re.compile(r'ab*')
>>> m = p.search('abbbba')
>>> m.group()
'abbbb'

>>> import re
>>> p = re.compile(r'(ab)*')
>>> m = p.search('abababa')
>>> m.group(0)
'ababab'

找出字符串中的数字


>>> import re
>>> p = re.compile(r'\d+')
>>> m = p.search('this year is 2015')
>>> m.group()
'2015'

找出字符串中的字符


>>> import re
>>> p = re.compile(r'\w+')
>>> m = p.search('this year is 2015')
>>> m.group()
'this'

匹配邮箱地址


>>> p = re.compile(r'[\w\d]+@\w+\.com')
>>> m = p.search('mymail@mail.com')
>>> m.group()
'mymail@mail.com'

匹配IP地址


>>> p = re.compile(r'(\d{1,3}\.){3}\d{1,3}')
>>> m = p.search('ip address is : 192.168.1.1 not 192.1.1')
>>> m.group()
'192.168.1.1'

正则表达式中的元字符

大多数的字符在进行正则表达式匹配的时候,会简单的进行一对一的匹配,比如,普通的字符串test将会精确地匹配到test。但是,则正则表达式中,有一些字符具有特殊的用于,它们在匹配的时候不会精确匹配到对应的字符。这些具有特殊用于的字符,在正则表达式中被称为元字符

元字符不会匹配自身,相反,单个的元字符可以匹配不同内容和长度的字符串,或者影响正则表达式的执行。正是由于元字符的存在,才使得正则表达式如此灵活和强大。

在正则表达式中,元字符由一些这些字符组成,这些字符在使用的时候不再表示它们原来的作用,如果要使用它们原来的意思,则需要使用反斜杠转义:


. ^ $ * + ? { } [ ] \ | ( )

下面,我们将会正则表达式中的这些元字符进行解释,但是在这之前,我们需要先介绍下在Python中使用反斜杠进行转义时碰到的问题。

转义

在Python的字符串中,一些特殊的转义字符具有特殊的意义。比如,在字符串中的'\n'表示的是换行,'\b'表示的是回退键(Backspace),等等。详细的转义字符表可以查看这里。我们可以看到,对于转义字符,我们需要在前面加上一个反斜杠来表示。那么,如果我们需要在字符串中使用一个反斜杠,而不是把这个反斜杠用于转义的时候,我们需要使用两个反斜杠来表示'\\'

所以,如果我们希望在字符串中包含'\n',但是我们不希望进行转义,那么可以在前面的反斜杠之前添加上一个反斜杠来进行跳脱(escape)。所以'\\n'就变成了单纯的'\n'字符了,而不是一个换行符。反斜杠的作用就是用来对转义字符进行跳脱。除了使用反斜杠来进行转义字符的跳脱,也可以用Python的raw字符串来达到同样的效果,python的raw字符串可以在字符串前面加上一个字符r来表示:r'raw string',这个字符串就是一个raw字符串。在raw字符串中,所有的转义都将失去作用,所以r'\n'将表示两个字符'\''n',而不是一个换行符。

前面我们讲到了Python中的转义字符,那转义字符对正则表达式有什么影响呢?

我们知道,正则表达式实际上就是一个字符串,和别的字符串的区别就是它们可以被正则表达式引擎解析和执行。所以,为了使正则表达式可以正常的工作,我们需要保证传递给正则表达式引擎的正则表达式字符串的语法是正确的,并且是我们期望的样子。但是,由于Python中的转义字符的存在,并且,在正则表达式的元字符中也包含了反斜杠,所以我们需要正确地处理正则表达式中的反斜杠。

如果我们需要在正则表达式中使用反斜杠本来的意思,而不是作为元字符使用,那么我们需要传递给正则表达式引擎的就是一个'\\'表示的字符串。其中第一个反斜杠用于对后面的反斜杠进行跳脱,使得后面的反斜杠失去元字符的作用。现在,我们需要两个反斜杠'\\'传递给正则表达式引擎,那么,我们需要用'\\\\'这样的Python字符串来表示。为什么呢?因为,我们是在Python中使用正则表达式,并且正则表达式是使用Python中的字符串表示的,那么考虑到Python中转义字符的问题,我们如果需要一个反斜杠,那么需要在字符串中这样表示'\\',如果需要两个,那么就要这样表示'\\\\'。这样,我们的Python字符串就会产生两个反斜杠组成的字符串了。

而对于正则表达式的一些特殊的元字符,比如'\d',如果用Python的字符串表示,则需要表示成'\\d'来保证传递给正则表达式引擎的是'\d'


>>> print('\\')
\
>>> print('\\\\')
\\
>>> print('\\d')
\d

如果正则表达式中包含了很多的反斜杠,这样会导致正则表达式变的复杂和难以理解,所以,我们一般都用Python的raw字符串来表示一个正则表达式。因为在raw字符串中,反斜杠将不具有特殊用途,所以r'\\'表示'\\'r'\d'表示'\d'


>>> print(r'\\')
\\
>>> print(r'\d')
\d

我们在编写正则表达式的时候,都应该使用Python的raw字符串来表示。

元字符

介绍完了反斜杠的问题,下面,我们介绍正则表达式中的元字符,是它们使得正则表达式如此灵活和强大的。

注意:这里只是介绍正则表达式中一些常用的元字符,详细的介绍可以参考Python标准库中的re模块

元字符[]用于表示一个字符类,一个字符类表示了一个希望被匹配的字符组成的集合。字符类中的字符可以是以单个的字符出现,也可以是以字符区间的形式出现,字符区间是由两个字符组成,中间由一个连字符'-'分隔。如:[abc]可以匹配a、b、c中的任何一个字符,同样,也可以用[a-c]来达到同样的效果,[a-c]表示匹配a到c之间的任何一个字母。如果需要匹配任何一个小写字母,则可以这样写[a-z]

在元字符[]中的字符,将会失去特殊的作用,也就是说,如果在[]中包含了元字符,则这些元字符将不再具有特使作用,而只是表示字符自身。这可以用来代替反斜杠来跳脱元字符:

[$]可以和\$ 达到相同的效果。

如果需要匹配不再字符类中的字符,则可以对字符类进行取反,使用'^'符号来进行取反。将'^'符号放在字符类中的字符集合中的第一个,使得可以匹配不在这个字符集合中的字符。比如:[^a-z]表示匹配任何不是小写字母的字符。


>>> p = re.compile(r'[ab]')
>>> m = p.search('ababaabaacdefg')
>>> m.group()
'a'

元字符\w可以匹配字母和数字。如果正则表达式是由字节表示的,则\w相当于是字符类[a-zA-Z0-9_]。如果正则表达式是一个字符串,则\w将会匹配所有在Unicode database中标记为文字(letters)的字符,Unicode database由模块unicodedata模块提供。在编译正则表达式的时候,可以通过指定 re.ASCII 选项来限制\w匹配的范围。


>>> p = re.compile(r'\w')
>>> m = p.search('ababaabaacdefg')
>>> m.group()
'a'

元字符\d将会匹配数字,对于Unicode表示的正则表达式(str类型),将会匹配Unicode表示的十进制数字,包括[0-9],以及其他的数字字符。如果 re.ASCII 选项在编译的时候被指定,则只会匹配[0-9](由于 re.ASCII 选项会影响整个正则表达式,所以在需要匹配0到9组成的数字的时候,可以使用[0-9])。对于8位字节(bytes类型)表示的正则表达式,则相当于是[0-9]


>>> p = re.compile(r'\d')
>>> m = p.search('123')
>>> m.group()
'1'

元字符\D匹配任何不是Unicode数字的字符,这个元字符和\d的相反。如果指定了 re.ASCII 选项,则相当于是 [^0-9]


>>> p = re.compile(r'\D')
>>> m = p.search('123abc')
>>> m.group()
'a'

元字符\s,在Unicode表示(str类型)的正则表达式中,用于匹配Unicode空白字符,包括[ \t\n\r\f\v],如果 re.ASCII 选项被指定,则只匹配[ \t\b\r\f\v]。对于8位字节(bytes)表示的正则表达式,则和[ \t\n\r\f\v]是等价的。


>>> p = re.compile(r'\s')
>>> m = p.search('hello world')
>>> m.group()
' '

元字符\S,匹配不是Unicode空白字符的任何字符。和\s相反。如果 re.ASCII 选项被指定,则和 [^ \t\n\r\f\v]等价。


>>> p = re.compile(r'\S')
>>> m = p.search('hello world')
>>> m.group()
'h'

元字符\W匹配不是Unicode中的word字符的任何字符。和\w相反。如果指定了 re.ASCII 选项,则和 [^a-zA-Z0-9_]等价。


>>> p = re.compile(r'\W')
>>> m = p.search('hello-world')
>>> m.group()
'-'

元字符 .可以匹配除了换行符外的任何一个字符。但是,如果在编译的时候指定了 re.DOTALL 选项,则可以匹配任何一个字符,包括换行符。


>>> p = re.compile(r'.')
>>> m = p.search('hello')
>>> m.group()
'h'

元字符|表示操作。如果A和B都是正则表达式,则A|B表示会匹配A和B中的任何一个。由于|的优先级具有很低的优先级,所以当进行如下hello|world模式串进行匹配的时候,helloworld之间的任何一个匹配了,则这个正则表达式就匹配成功了,而不是作用于ow。如果需要在正则表达式中使用|作为普通的字符,则可以使用反斜杠进行转义\|,或者使用字符类[|]


>>> p = re.compile(r'\d+|\w+')
>>> m = p.search('a1b2c3d4')
>>> m.group()
'a1b2c3d4'

元字符^匹配一行的开头。除非指定了re.MULTILINE选项,否则会匹配字符串的开头,如果re.MULTILINE选项被指定,则会匹配字符串中换行符后面的位置。

如果需要在模式串中去掉元字符的特殊意义,则可以使用反斜杠来转义\^,或者使用字符类[^]


>>> p = re.compile(r'^hello')
>>> p.search('hello world')
<_sre.SRE_Match object; span=(0, 5), match='hello'>
>>> print(p.search('a word hello'))
None

元字符$匹配一行的结尾,一行的结尾的定义是:一个字符串的结尾,当指定了re.MULTILNE选项后,会匹配换行符之前的位置。

如果需要去掉元字符的特殊意义,则可以使用反斜杠进行转义\^,或者使用字符类[$]


>>> p = re.compile(r'd$')
>>> p.search('hello world')
<_sre.SRE_Match object; span=(10, 11), match='d'>
>>> print(p.search('hello'))
None >>> p = re.compile(r'\w+d$', re.M)
>>> p.search('word\n Word').group()
'word'

元字符\A\Z分别匹配字符串的开头和结尾,不会受到re.MULTILINE选项的影响。当没有指定re.MULTILINE选项的情况下,^相对于是\A,而$相当于是\Z。(为什么要使用AZ作为匹配字符串开头和结尾的元字符,可能是考虑到A是英文字符的第一个字母,而Z是英文字符的最后一个字母的缘故,这样相对容易记忆)。

元字符\b匹配单词的边界,这是一个零宽断言,匹配单词的边界(开头或结尾)。单词的定义是:一系列字母数字组成的序列,所以单词可以被空白符或者非字母数字分隔。


>>> p = re.compile(r'\bhello\b')
>>> p.search('hello world').group()
'hello'
>>> print(p.search('helloworld'))
None

元字符\B匹配不是单词边界的位置,这个元字符也是一个零宽断言,和元字符\b的意思相反。

重复匹配

正则表达式除了可以通过元字符匹配任意的字符以外,还具有重复的能力,可以指定匹配的次数。

元字符 *匹配重复前一个字符或分组0次或多次,如果需要匹配'*'字符,则需要进行转义'\*'。由于Python的re模块中的正则表达式引擎是采用C编写的,所以重复次数的最大值被int类型的最大值限制,重复次数最大为20亿次。

Python中正则表达式引擎的重复匹配是贪婪的,匹配引擎会尽可能多的进行匹配,直到不能匹配为止,然后匹配引擎会进行回溯,尝试更小的重复次数进行匹配,直到匹配上。所以对于字符串aaaaaaba*将会匹配aaaaaa,而不是匹配a


>>> p = re.compile(r'ca*t')
>>> m = p.search('caaat')
>>> m.group()
'caaat'

元字符+,用于重复前一个字符或分组一次或多次。元字符*+是有区别的,+要求前一个字符出现至少一次,而*并不要求前一个字符一定要出现。所以对于正则表达式ca*t,可以匹配ct,但是正则表达式ca+t将不能匹配ct

元字符?,用于重复前一个字符或分组一次或0次。正则表达式home-?brew可以匹配home-brewhomebrew

元字符{m, n},可以匹配前一个字符或分组m ~ n次。正则表达式a/{1,3}b将会匹配a/ba//ba///b。如果省略了m,则表示匹配0 ~ n次,如果省略了n,则表示至少匹配m次。因此,{0,}*是等价的,{0,1}?是等价的,{1,}+是等价的。

使用正则表达式

现在,我们已经对正则表达式有了一些了解,也知道了正则表达式中元字符的作用。下面,我们开始结合Python的re模块来学习使用这些正则表达式。

Python的re模块提供了一套接口来使用正则表达式引擎,通过re模块提供的接口,可以将正则表达式编程成正则表达式对象,然后进行需要的匹配操作。

编译正则表达式

正则表达式可以被编译成模式对象,然后调用模式对象的方法来进行字符串匹配操作,比如搜索、替换或者分割。

通过re模块的compile()函数可以将一个正则表达式编译成一个模式对象


re.compile(pattern, flags=0)

>>> import re
>>> p = re.compile(r'a*')
>>> p
re.compile('a*')

传递给re.compile()函数一个可选的flags参数,可以控制编译后的对象在匹配的时候的行为。

flags 参数的值可以是以下的值:

re.A | re.ASCII

\w\W\b\B\d\D\s\S产生影响,编译后的模式对象在进行匹配的时候,只会匹配ASCII字符,而不是Unicode字符。

这个选项只对Unicode模式串有效,对字节模式串无效。

re.I | re.IGNORECASE

在匹配的时候忽略大小写,在字符类和字符字面值进行匹配的时候会忽略大小写。进行大小写转换的时候,不会考虑当前的locale信息,除非指定了re.LOCALE选项。

  >>> p = re.compile(r'[a-z]+', re.I)
>>> m = p.search('abcdABCD')
>>> m.group()
abcdABCD
re.M | re.MULTILINE

默认,元字符^会匹配字符串的开始处,元字符$会匹配字符串的结束位置和字符串后面紧跟的换行符之前(如果存在这个换行符)。如果指定了这个选项,则^将会匹配字符串的开头和每一行的开始处,紧跟在每一个换行符后面的位置。类似的,$会匹配字符串的最后和每一行的最后,在接下来的换行符的前面的位置。

  >>> p = re.compile(r'(^hello$)\s(^hello$)\s(^hello$)\s')
>>> m = p.search('hello\nhello\nhello\n')
>>> print(m)
None >>> p = re.compile(r'(^hello$)\s(^hello$)\s(^hello$)\s', re.M)
>>> m = p.search('\nhello\nhello\nhello\n')
>>> m.groups()
('hello', 'hello', 'hello')
re.S | re.DOTALL

使得.元字符可以匹配任何字符,包括换行符。

re.X | re.VERBOSE

这个选项允许编写可读性更强的正则表达式代码,并进行*的格式化操作。当这个选项被指定以后,在正则表达式之间的空格符会被忽略,除非这个空格符是在一个字符类中[ ],或者在空格前使用一个反斜杠\ 。这个选项允许对正则表达式进行缩进,使得正则表达式的代码更加格式化,更加清晰。并且可以在正则表达式的代码中使用注释,这些注释会被正则表达式引擎在处理的时候忽略。注释以'#' 字符开头。所以如果需要在正则表达式中使用'#'符号,需要在前面添加反斜杠'\#'或者将它放在[]中,[#]

  charref = re.compile(r"""
&[#] # Start of a numeric entity reference
(
0[0-7]+ # Octal form
| [0-9]+ # Decimal form
| x[0-9a-fA-F]+ # Hexadecimal form
)
; # Trailing semicolon
""", re.VERBOSE)

如果没有指定re.VERBOSE选项,则相当于:

  charref = re.compile("&#(0[0-7]+"
"|[0-9]+"
"|x[0-9a-fA-F]+);")

进行匹配

一旦我们通过re.compile()编译后产生了一个正则表达式对象,我们就可以通过这个正则表达式对象进行匹配操作了。re模块的正则表达式对象包含了一些方法,可以进行需要的匹配操作。

方法 描述
match() 从字符串开头位置开始匹配
search() 对字符串的任意位置进行匹配
findall() 返回字符串中所有匹配的子串组成的列表
finditer() 返回一个包含了所有的匹配对象的迭代器

如果没有匹配到,则match()search()方法返回None,如果匹配成功,则返回一个匹配对象。返回的匹配对象包含了匹配信息:包括匹配的子串的开始位置和结束位置等信息。


>>> p = re.compile(r'[a-z]+')
>>> print(p.match('hello123'))
<_sre.SRE_Match object; span=(0, 5), match='hello'>
>>> print(p.match('123hello'))
None
>>> print(p.search('123hello'))
<_sre.SRE_Match object; span=(3, 8), match='hello'> >>> p = re.compile(r'\d+')
>>> p.findall('a number A is 12, and a number B is 8, A + B = 20')
['12', '8', '20'] >>> i = p.finditer('a number A is 12, and a number B is 8, A + B = 20')
>>> for item in i:
print(item.group()) 12
8
20

除了使用正则表达式对象中的方法,re模块也提供了一些模块级别的同名函数来进行匹配操作。包括:match(),search(),findall()和sub()等函数。这些函数接受和正则表达式对象中的方法类似的参数,除了第一个参数是一个正则表达式字符串以外,后面的参数和正则表达式对象中的方法接受的参数是一致的。并且这些函数的返回值也是相同的。


>>> re.search(r'\d+', 'a number A is 12')
<_sre.SRE_Match object; span=(14, 16), match='12'> >>> re.findall(r'\d+', 'a number A is 12, and a number B is 8, A + B = 20')
['12', '8', '20']

实际上,这些函数在底层会对正则表达式字符串进行编译,产生一个正则表达式对象,然后调用正则表达式中同名的方法来实现。并且,这些函数会将正则表达式对象缓存起来,所以下次再次使用相同的正则表达式的时候,就可以不用再次对这个正则表达式进行编译了。

对于使用模块级别的函数还是使用正则表达式对象中的方法来进行匹配,需要依据不同的使用场景来权衡。如果是在循环中,则使用编译好的正则表达式对象会相对高效,而在循环外,由于缓存的存在,这两种方式区别不大。

我们得到匹配对象以后,就可以通过匹配对象中的方法对匹配的信息进行进一步的处理了。

匹配对象中重要的方法如下:

方法 描述
group() 返回正则表达式匹配到的字符串
start() 返回匹配的起始位置
end() 返回匹配的结束位置
span() 返回一个包含匹配的起始位置和结束位置的元组(start, end)

>>> p = re.compile(r'[a-z]+')
>>> m = p.search('123hello')
>>> m
<_sre.SRE_Match object; span=(3, 8), match='hello'>
>>> m.group()
'hello'
>>> m.start()
3
>>> m.end()
8
>>> m.span()
(3, 8)

正则表达式不是万能的

虽然正则表达式很强大,可以灵活地处理很多字符串处理得工作。但是,由于正则表达式语言相对严格和小巧,所以一些字符串处理工作,正则表达式并不能处理的很好。

在一些情况下,并不适合使用正则表达式,相反,使用Python的字符串中的方法,可能更加合适。比如,如果你需要对一个字符串进行匹配,而匹配的模式串是一个固定的字符串的话,那么,使用Python中的字符串中的一些方法,如replace()方法,会比使用正则表达式来的更加高效和简单。

因此,在我们使用Python的re模块中的正则表达式来进行字符串的处理工作之前,我们不妨考虑下,是否可以使用字符串中简单高效的方法来解决。

后续

这篇文章没有提到正则表达式中的分组(group),在re模块中也支持分组,并且添加了Python的一些特性,后续再来介绍下re模块中的分组的使用,以及一些别的特性。


正则表达式 - *

https://docs.python.org/3/howto/regex.html