本文是速通教程,仅会介绍最基础的知识。如需了解更多,请参考官方文档或其他文章。 \textcolor{red}{\text{本文是速通教程,仅会介绍最基础的知识。如需了解更多,请参考官方文档或其他文章。}} 本文是速通教程,仅会介绍最基础的知识。如需了解更多,请参考官方文档或其他文章。
什么是正则表达式?正则表达式(regular expression)是一种特殊的字符串,它能帮助你方便的检查一个字符串是否与给定的模式匹配。Python 中的 re
模块提供了正则表达式的全部功能,在接下来的几章,我们将详细介绍 re
中最为常用的功能。
import re
re.match
尝试从字符串的起始位置匹配一个模式,如果匹配成功,则会返回一个 re.Match
对象;如果匹配失败,则返回 None
。具体格式如下:
re.match(pattern, string, flags=0)
pattern
:正则表达式;string
:要匹配的字符串;flags
:修饰符,用于控制匹配模式。正则表达式既可以包含普通字符也可以包含特殊字符。如果仅包含普通字符,那就是匹配某个特定的字符串:
""" 尝试从字符串 abcde 的起始位置匹配字符串 abc """
s = re.match('abc', 'abcde')
print(s)
#
字符串 abc
在字符串 abcde
中的起始位置和终止位置分别为 0
和 2
,根据左闭右开原则,span
为 (0, 3)
。
如果要获得匹配的结果,则可使用 group
方法:
print(s.group())
# abc
注意,以下这样的匹配会失败,因为 match
是从字符串的起始位置进行匹配的:
s = re.match('bcd', 'abcde')
print(s)
# None
常用特殊字符(special characters)列在下表中:
特殊字符 | 作用 |
---|---|
. |
匹配除了换行符 \n 以外的任意单个字符 |
^ |
匹配起始位置 |
$ |
匹配终止位置(换行符之前) |
* |
表示 * 前的一个字符可以出现 0 次或任意多次 |
+ |
表示 + 前的一个字符可以出现 1 次或任意多次 |
? |
表示 ? 前的一个字符可以出现 0 次或 1 次 |
{m} |
表示 {m} 前的一个字符出现 m 次 |
{m,} |
表示 {m,} 前的一个字符可以出现 m 次及以上 |
{,n} |
表示 {,n} 前的一个字符最多出现 n 次 |
{m,n} |
表示 {m,n} 前的一个字符出现 m 次到 n 次 |
[] |
匹配 [] 中列举出的字符 |
() |
匹配 () 内的表达式,表示一个分组 |
| |
或 |
为简便起见,我们定义一个 match
函数用来更为直观地展示匹配结果:
def match(pattern, list_of_strings):
for string in list_of_strings:
if re.match(pattern, string):
print('匹配成功!结果为:', res.group())
else:
print('匹配失败!')
.
:
match('.', ['a', 'ab', 'abc'])
# 匹配成功!结果为: a
# 匹配成功!结果为: a
# 匹配成功!结果为: a
因为我们是从头开始匹配单个字符的,所以结果均为 a
。
^
、$
:
match('^ab', ['ab', 'abc', 'adc', 'bac'])
# 匹配成功!结果为: ab
# 匹配成功!结果为: ab
# 匹配失败!
# 匹配失败!
match('cd$', ['cd', 'acd', 'adc', 'cdcd'])
# 匹配成功!结果为: cd
# 匹配失败!
# 匹配失败!
# 匹配失败!
看似是匹配以 cd
为结尾的字符串,但实际上别忘了 match
是从字符串的起始位置开始匹配的,因此上述语句实际上就是匹配字符串 cd
。
*
、+
、?
:
match('a*', ['aa', 'aba', 'baa', 'aaaa'])
# 匹配成功!结果为: aa
# 匹配成功!结果为: a
# 匹配成功!结果为:
# 匹配成功!结果为: aaaa
match('a+', ['aa', 'aba', 'baa'])
# 匹配成功!结果为: aa
# 匹配成功!结果为: a
# 匹配失败!
match('a?', ['aa', 'ab', 'ba'])
# 匹配成功!结果为: a
# 匹配成功!结果为: a
# 匹配成功!结果为:
注意,a*
代表 a
可以出现 0 次或任意多次,因此 baa
去匹配会得到空字符串。
{m}
、{m,}
、{,n}
、{m,n}
(注意没有空格):
match('a{3}', ['abaa', 'aaab', 'baaa'])
# 匹配失败!
# 匹配成功!结果为: aaa
# 匹配失败!
match('a{3,}', ['aaab', 'aaaab', 'baaa'])
# 匹配成功!结果为: aaa
# 匹配成功!结果为: aaaa
# 匹配失败!
match('a{,3}', ['aaab', 'aaaab', 'baaa'])
# 匹配成功!结果为: aaa
# 匹配成功!结果为: aaa
# 匹配成功!结果为:
match('a{3,5}', ['a' * i for i in range(2, 7)])
# 匹配失败!
# 匹配成功!结果为: aaa
# 匹配成功!结果为: aaaa
# 匹配成功!结果为: aaaaa
# 匹配成功!结果为: aaaaa
[]
:
match('[123]', [str(i) for i in range(1, 5)])
# 匹配成功!结果为: 1
# 匹配成功!结果为: 2
# 匹配成功!结果为: 3
# 匹配失败!
注意,我们可以将 [123]
简写为 [1-3]
,这意味着若要匹配单个数字,则可以采用 [0-9]
这样的正则表达式:
match('[0-9]', ['a', 'A', '1', '3', '_'])
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为: 1
# 匹配成功!结果为: 3
# 匹配失败!
更进一步,如果我们要想匹配区间 [ 1 , 35 ] [1, 35] [1,35] 内的所有整数,该如何做呢?很自然的一个想法是使用 [1-35]
,但仔细观察下面的例子:
match('[1-35]', ['1', '2', '3', '4'])
# 匹配成功!结果为: 1
# 匹配成功!结果为: 2
# 匹配成功!结果为: 3
# 匹配失败!
会发现数字 4 4 4 匹配失败了。这是因为 -
只能连接相邻的两个数字,所以 [1-35]
实际上代表数字 1
、2
、3
和 5
。也就是说,除了这四个数字以外的数全部都会匹配失败。
我们分三种情形考虑:十位数为 3 3 3,十位数为 1 1 1 或 2 2 2,只有个位数(需要使用或运算 |
)
pattern = '3[0-5]|[12][0-9]|[1-9]'
该正则表达式的确能够全部正确匹配 [ 1 , 35 ] [1,35] [1,35] 内的所有整数,但是:
match('3[0-5]|[12][0-9]|[1-9]', ['36', '350'])
# 匹配成功!结果为: 3
# 匹配成功!结果为: 35
我们会发现区间之外的数也能够匹配成功。因此需要使用 $
来防止误判,正确做法是:
pattern = '(3[0-5]|[12][0-9]|[1-9])$'
其中 ()
的作用之后会提及(这里可以粗略地理解成视为一个整体)。
除此之外,我们还可以判断给定的字符是否是字母,相应的正则表达式为 [a-zA-Z]
:
match('[a-zA-Z]', ['-', 'a', '9', 'G', '.'])
# 匹配失败!
# 匹配成功!结果为: a
# 匹配失败!
# 匹配成功!结果为: G
# 匹配失败!
如果我们想要匹配非数字字符,则需要使用 ^
,它表示取补集:
match('[^0-9]', ['-', 'a', '3', 'M', '9', '_'])
# 匹配成功!结果为: -
# 匹配成功!结果为: a
# 匹配失败!
# 匹配成功!结果为: M
# 匹配失败!
# 匹配成功!结果为: _
()
:
""" 匹配多个 ab """
match('(ab)+', ['ac', 'abc', 'abbc', 'abababac', 'adc'])
# 匹配失败!
# 匹配成功!结果为: ab
# 匹配成功!结果为: ab
# 匹配成功!结果为: ababab
# 匹配失败!
注意 ab+
这样的正则表达式是无效的,它代表只有一个字符 a
和一个及以上的字符 b
。因此我们必须用 ()
将其括起来视为一个整体,也称作一个分组。
以 \
开头并仅连一个字符的称为特殊序列(special sequences),常用特殊序列列在下表中:
特殊序列 | 作用 |
---|---|
\d |
等价于 [0-9] ,即所有数字(巧记:digit) |
\D |
等价于 [^\d] ,即所有非数字 |
\s |
空格字符(巧记:space) |
\S |
等价于 [^\s] ,即所有非空格字符 |
\w |
等价于 [a-zA-Z0-9_] ,即所有单词字符,包括字母、数字和下划线(巧记:word) |
\W |
等价于 [^\w] ,即所有非单词字符 |
""" 示例一 """
match('\d', ['1', 'a', '_', '-'])
# 匹配成功!结果为: 1
# 匹配失败!
# 匹配失败!
# 匹配失败!
match('\D', ['1', 'a', '_', '-'])
# 匹配失败!
# 匹配成功!结果为: a
# 匹配成功!结果为: _
# 匹配成功!结果为: -
""" 示例二 """
match('\s', ['1', 'a', '_', ' '])
# 匹配失败!
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为:
match('\S', ['1', 'a', '_', ' '])
# 匹配成功!结果为: 1
# 匹配成功!结果为: a
# 匹配成功!结果为: _
# 匹配失败!
""" 示例三 """
match('\w', ['1', 'a', '_', ' ', ']'])
# 匹配成功!结果为: 1
# 匹配成功!结果为: a
# 匹配成功!结果为: _
# 匹配失败!
# 匹配失败!
match('\W', ['1', 'a', '_', ' ', ']'])
# 匹配失败!
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为:
# 匹配成功!结果为: ]
接下来我们通过一些例子来进一步巩固之前所学的概念。
十六进制的颜色值的格式通常为 #XXXXXX
,其中 X
的取值可以为数字,也可以为 A-F
中的任意字符(假设这里不考虑小写情形)。
regex = '#[A-F0-9]{6}$'
colors = ['#00', '#FFFFFF', '#FFAAFF', '#00HH00', '#AABBCC', '#000000', '#FFFFFFFF']
match(regex, colors)
# 匹配失败!
# 匹配成功!结果为: #FFFFFF
# 匹配成功!结果为: #FFAAFF
# 匹配失败!
# 匹配成功!结果为: #AABBCC
# 匹配成功!结果为: #000000
# 匹配失败!
我们不考虑前缀0的情形,例如对于数字 8
、35
,诸如 08
、008
、035
这样的形式是排除在外的。
只需分别考虑三位数、两位数、一位数的情形:
regex = '(100|[1-9]\d|[1-9])$'
numbers = ['0', '5', '05', '005', '12', '012', '89', '100', '101']
match(regex, numbers)
# 匹配失败!
# 匹配成功!结果为: 5
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为: 12
# 匹配失败!
# 匹配成功!结果为: 89
# 匹配成功!结果为: 100
# 匹配失败!
这里我们自创一个邮箱,假设域名为 sky.com
。在创建新用户时,要求用户名只能由数字、字母及下划线组成,且不能以下划线开头,邮箱名长度在6-18位。我们该如何用正则表达式来判断用户输入的邮箱是否符合规范呢?
regex = '[a-zA-Z0-9][\w]{5,17}@sky\.com$'
emails = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]
match(regex, emails)
# 匹配失败!
# 匹配失败!
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为: [email protected]
# 匹配失败!
# 匹配失败!
如果 sky
邮箱允许用户开通vip,开通后邮箱域名变为了 vip.sky.com
,且普通用户和vip用户均属于 sky
邮箱用户。该情形下的正则表达式需要改写为:
regex = '[a-zA-Z0-9][\w]{5,17}@(vip\.)?sky\.com$'
emails = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]
match(regex, emails)
# 匹配成功!结果为: [email protected]
# 匹配成功!结果为: [email protected]
# 匹配失败!
# 匹配失败!
IPV4的格式通常为 X.X.X.X
,其中 X
的范围为 0-255。这里依然不考虑前缀0的情形。
regex = '((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$'
ipv4s = [
'0.0.0.0',
'0.0.255.0',
'255.0.0',
'127.0.0.1.0',
'256.0.0.123',
'255.255.255.255',
'012.08.0.0',
]
match(regex, ipv4s)
# 匹配成功!结果为: 0.0.0.0
# 匹配成功!结果为: 0.0.255.0
# 匹配失败!
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为: 255.255.255.255
# 匹配失败!
修饰符即 re.match
函数中的 flags
参数,常用的修饰符列在下表中:
修饰符 | 作用 |
---|---|
re.I |
匹配时忽略大小写 |
re.S |
使得 . 能够匹配任意单个字符(包括换行符 \n ) |
s = re.match('a', 'A', flags=re.I)
print(s.group())
# A
s = re.match('.', '\n', flags=re.S)
print(s)
#
re.match
是从字符串的起始位置开始匹配,即只要匹配成功,则无需管字符串的剩余位置。而 re.fullmatch
则是匹配整个字符串。
格式如下:
re.fullmatch(pattern, string, flags=0)
例如:
print(re.match('\d', '3a'))
#
print(re.fullmatch('\d', '3a'))
# None
re.search
从左到右扫描整个字符串并返回第一个成功的匹配。如果匹配失败,则返回 None
。
格式如下:
re.search(pattern, string, flags=0)
例如:
print(re.search('ab', 'abcd'))
#
print(re.search('cd', 'abcd'))
#
re.findall
是在给定的字符串中找到所有匹配正则表达式的子串,并以列表的形式返回,格式如下:
re.findall(pattern, string, flags=0)
例如:
res = re.findall('\d+', 'ab 13 cd- 274 .]')
print(res)
# ['13', '274']
res = re.findall('(\w+):(\d+)', 'Xiaoming:16, Xiaohong:14')
print(res)
# [('Xiaoming', '16'), ('Xiaohong', '14')]
re.sub
用于将匹配到的子串替换为另一个子串,执行时从左向右进行替换。格式如下:
re.sub(pattern, repl, string, count=0, flags=0)
repl
代表替换后的字符串,count
是最大替换次数,0表示替换所有。
例如:
res = re.sub(' ', '', '1 2 3 4 5')
print(res)
# 12345
res = re.sub(' ', '', '1 2 3 4 5', count=2)
print(res)
# 123 4 5
re.split
将根据匹配切割字符串(从左向右),并返回一个列表。格式如下:
re.split(pattern, string, maxsplit=0, flags=0)
count
是最大切割次数,0表示切割所有位置。
例如:
res = re.split(' ', '1 2 3 4 5')
print(res)
# ['1', '2', '3', '4', '5']
res = re.split(' ', '1 2 3 4 5', maxsplit=2)
print(res)
# ['1', '2', '3 4 5']
所有的量词:*
、+
、?
、{m}
、{m,}
、{,n}
、{m,n}
默认采取贪婪匹配的原则,即在匹配成功的情况下尽可能多地匹配。
以 *
为例,显然 \d*
是匹配任意长度的数字:
match('\d*', ['1234abc'])
# 匹配成功!结果为: 1234
根据贪婪原则,最终匹配结果一定是 1234
。
有些时候,我们不想尽可能多地匹配,而是尽可能少地匹配。这时候我们可以在量词后面加上 ?
,它表示非贪婪匹配:
match('\d*?', ['1234abc'])
# 匹配成功!结果为:
在非贪婪模式下,\d
应该出现0次(尽可能少匹配),于是最终返回空字符。
我们再来看一下其他量词的情况:
match('\d+?', ['1234abc'])
# 匹配成功!结果为: 1
match('\d??', ['1234abc'])
# 匹配成功!结果为:
match('\d{2,}?', ['1234abc'])
# 匹配成功!结果为: 12
match('\d{2,5}?', ['1234abc'])
# 匹配成功!结果为: 12
match('\d{,5}?', ['1234abc'])
# 匹配成功!结果为:
regex101 可用于练习正则表达式。