史上最易懂的Python正则表达式教程

本文是速通教程,仅会介绍最基础的知识。如需了解更多,请参考官方文档或其他文章。 \textcolor{red}{\text{本文是速通教程,仅会介绍最基础的知识。如需了解更多,请参考官方文档或其他文章。}} 本文是速通教程,仅会介绍最基础的知识。如需了解更多,请参考官方文档或其他文章。

目录

  • 前言
  • 一、re.match
    • 1.1 常用特殊字符
    • 1.2 常用特殊序列
    • 1.3 一些例子
      • 1.3.1 十六进制RGB颜色值匹配
      • 1.3.2 1-100之间的所有整数匹配
      • 1.3.3 邮箱格式匹配
      • 1.3.4 IPV4地址匹配
    • 1.4 修饰符
  • 二、其他函数
    • 2.1 re.fullmatch
    • 2.2 re.search
    • 2.3 re.findall
    • 2.4 re.sub
    • 2.5 re.split
  • 三、贪婪匹配与非贪婪匹配
  • 四、推荐网站

前言

什么是正则表达式?正则表达式(regular expression)是一种特殊的字符串,它能帮助你方便的检查一个字符串是否与给定的模式匹配。Python 中的 re 模块提供了正则表达式的全部功能,在接下来的几章,我们将详细介绍 re 中最为常用的功能。

import re

一、re.match

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 中的起始位置和终止位置分别为 02,根据左闭右开原则,span(0, 3)

如果要获得匹配的结果,则可使用 group 方法:

print(s.group())
# abc

注意,以下这样的匹配会失败,因为 match 是从字符串的起始位置进行匹配的:

s = re.match('bcd', 'abcde')
print(s)
# None

1.1 常用特殊字符

常用特殊字符(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] 实际上代表数字 1235。也就是说,除了这四个数字以外的数全部都会匹配失败。

我们分三种情形考虑:十位数为 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。因此我们必须用 () 将其括起来视为一个整体,也称作一个分组。

1.2 常用特殊序列

\ 开头并仅连一个字符的称为特殊序列(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', '_', ' ', ']'])
# 匹配失败!
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为:  
# 匹配成功!结果为: ]

1.3 一些例子

接下来我们通过一些例子来进一步巩固之前所学的概念。

1.3.1 十六进制RGB颜色值匹配

十六进制的颜色值的格式通常为 #XXXXXX,其中 X 的取值可以为数字,也可以为 A-F 中的任意字符(假设这里不考虑小写情形)。

regex = '#[A-F0-9]{6}$'
colors = ['#00', '#FFFFFF', '#FFAAFF', '#00HH00', '#AABBCC', '#000000', '#FFFFFFFF']
match(regex, colors)
# 匹配失败!
# 匹配成功!结果为: #FFFFFF
# 匹配成功!结果为: #FFAAFF
# 匹配失败!
# 匹配成功!结果为: #AABBCC
# 匹配成功!结果为: #000000
# 匹配失败!

1.3.2 1-100之间的所有整数匹配

我们不考虑前缀0的情形,例如对于数字 835,诸如 08008035 这样的形式是排除在外的。

只需分别考虑三位数、两位数、一位数的情形:

regex = '(100|[1-9]\d|[1-9])$'
numbers = ['0', '5', '05', '005', '12', '012', '89', '100', '101']
match(regex, numbers)
# 匹配失败!
# 匹配成功!结果为: 5
# 匹配失败!
# 匹配失败!
# 匹配成功!结果为: 12
# 匹配失败!
# 匹配成功!结果为: 89
# 匹配成功!结果为: 100
# 匹配失败!

1.3.3 邮箱格式匹配

这里我们自创一个邮箱,假设域名为 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]
# 匹配失败!
# 匹配失败!

1.3.4 IPV4地址匹配

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
# 匹配失败!

1.4 修饰符

修饰符即 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)
# 

二、其他函数

2.1 re.fullmatch

re.match 是从字符串的起始位置开始匹配,即只要匹配成功,则无需管字符串的剩余位置。而 re.fullmatch 则是匹配整个字符串。

格式如下:

re.fullmatch(pattern, string, flags=0)

例如:

print(re.match('\d', '3a'))
# 
print(re.fullmatch('\d', '3a'))
# None

2.2 re.search

re.search 从左到右扫描整个字符串并返回第一个成功的匹配。如果匹配失败,则返回 None

格式如下:

re.search(pattern, string, flags=0)

例如:

print(re.search('ab', 'abcd'))
# 
print(re.search('cd', 'abcd'))
# 

2.3 re.findall

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')]

2.4 re.sub

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

2.5 re.split

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 可用于练习正则表达式。

你可能感兴趣的:(Python,正则表达式,perl,开发语言)