正则表达式
在处理字符串时,我们经常会遇到「判断某个字符串是否符合某种规则」的需求:比如检查邮箱格式、手机号格式、提取网页里的日期、把一段混乱的日志切成几段……这些任务如果用 if/else 加字符串方法硬写,代码会又长又容易出错。
正则表达式就是专门用来描述这种「字符串规则」的工具。它本质上是一门小语言:你写一条规则,然后用 Python 的 re 模块去检查字符串是否符合规则。规则对了,匹配、查找、替换、切分都能一句话搞定。
1. 正则表达式基础
正则表达式由普通字符和特殊字符组成。普通字符直接匹配自身,特殊字符则匹配某一类字符。
1.1 常见字符类
\d:匹配一个数字(0-9)。\w:匹配一个字母、数字或下划线(等价于[0-9a-zA-Z_])。\s:匹配一个空白字符(空格、Tab、换行等)。.:匹配任意一个字符(除了换行符)。
来看几个例子:
'00\d' # 匹配 007,不匹配 00A
'\d\d\d' # 匹配 010
'\w\w\d' # 匹配 py3
'py.' # 匹配 pyc、pyo、py! 等1.2 量词
如果要匹配「若干个」字符,就要用到量词:
| 符号 | 含义 |
|---|---|
* | 匹配 0 个或多个 |
+ | 匹配 1 个或多个 |
? | 匹配 0 个或 1 个 |
{n} | 匹配恰好 n 个 |
{n,m} | 匹配 n 到 m 个 |
比如 \d{3}\s+\d{3,8} 这条规则:
\d{3}:连续 3 个数字,比如010;\s+:至少一个空白字符;\d{3,8}:3 到 8 个数字。
它可以匹配 010 12345、010 1234567 这种带区号的电话号码。
如果要匹配 010-12345,因为 - 是特殊字符,需要转义:
'\d{3}\-\d{3,8}' # 匹配 010-12345在 Python 字符串里,正则表达式建议加
r前缀,比如r'\d{3}-\d{3,8}',这样反斜杠不会被 Python 提前转义,写起来更直观。
2. 范围、位置与转义
2.1 字符范围 []
[] 表示一个字符范围,匹配方括号里的任意一个字符。
[0-9a-zA-Z_] # 匹配一个数字、字母或下划线
[a-z]+ # 匹配一个或多个小写字母
[0-9]{2,4} # 匹配 2 到 4 个数字比如判断 Python 变量名可以用:
r'^[a-zA-Z_][0-9a-zA-Z_]*$'第一个字符必须是字母或下划线,后面跟任意个字母、数字或下划线。
2.2 分支 |
A|B 表示匹配 A 或者 B。
r'(P|p)ython' # 匹配 Python 或 python2.3 行首和行尾
^表示字符串的开头。$表示字符串的结尾。
r'^\d' # 必须以数字开头
r'\d$' # 必须以数字结尾
r'^py$' # 整行只能有 py 两个字符3. re 模块常用方法
Python 里把正则表达式功能都集中在 re 模块里。最常用的方法有这几个。
3.1 判断字符串是否匹配
re.match(pattern, string) 从字符串开头开始匹配,匹配成功返回一个 Match 对象,失败返回 None。
import re
m = re.match(r'^\d{3}-\d{3,8}$', '010-12345')
print(m) # <re.Match object; span=(0, 9), match='010-12345'>
m = re.match(r'^\d{3}-\d{3,8}$', '010 12345')
print(m) # None通常我们会这样写:
if re.match(r'^\d{3}-\d{3,8}$', '010-12345'):
print('ok')
else:
print('failed')3.2 在字符串中搜索
re.search(pattern, string) 会扫描整个字符串,返回第一个匹配到的结果。
import re
m = re.search(r'\d+', 'abc123def456')
print(m.group()) # 1233.3 找出所有匹配
re.findall(pattern, string) 返回一个列表,包含所有匹配到的字符串。
import re
print(re.findall(r'\d+', 'abc123def456')) # ['123', '456']
print(re.findall(r'\b\w+\b', 'Hello, Python!')) # ['Hello', 'Python']3.4 切分字符串
re.split(pattern, string) 按正则切分字符串,比字符串自带的 split 更灵活。
import re
# 按一个或多个空格切分
print(re.split(r'\s+', 'a b c'))
# ['a', 'b', 'c']
# 按空格、逗号或分号切分
print(re.split(r'[\s,;]+', 'a,b;; c d'))
# ['a', 'b', 'c', 'd']3.5 替换字符串
re.sub(pattern, replacement, string) 会把匹配到的部分替换成新字符串。
import re
# 把所有数字换成 X
print(re.sub(r'\d+', 'X', 'abc123def456'))
# abcXdefX
# 去掉多余空格
print(re.sub(r'\s+', ' ', 'a b c d'))
# a b c d4. 分组提取
用 () 括号可以把正则表达式的一部分括起来,形成一个「分组」。匹配之后可以单独提取每个分组的内容。
import re
m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
print(m.group(0)) # 010-12345,整个匹配结果
print(m.group(1)) # 010,第一个分组
print(m.group(2)) # 12345,第二个分组
print(m.groups()) # ('010', '12345')group(0) 永远表示整条正则匹配到的字符串,group(1)、group(2) 分别对应第 1、第 2 个分组。
来个更实际的例子:把时间字符串 19:05:30 拆成时、分、秒。
import re
t = '19:05:30'
regex = r'^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$'
m = re.match(regex, t)
print(m.groups()) # ('19', '05', '30')正则擅长做「格式」检查,但像「日期是否合法」这种逻辑判断,比如
2 月 30 号,光靠正则很难写全,建议正则负责格式,程序负责业务校验。
5. 贪婪与非贪婪匹配
正则默认是贪婪的:在能满足整个规则的前提下,它会尽可能多地匹配。
import re
m = re.match(r'^(\d+)(0*)$', '102300')
print(m.groups()) # ('102300', '')这里的 \d+ 太贪了,把后面的 0 也吃了,导致 0* 只能匹配空字符串。
想让 \d+ 少匹配一点,可以在量词后面加 ?,变成非贪婪模式:
m = re.match(r'^(\d+?)(0*)$', '102300')
print(m.groups()) # ('1023', '00')
?的含义有点多:在普通字符后表示 0 或 1 次,在量词后面表示「非贪婪」。
6. 编译正则
如果同一个正则表达式要反复用很多次,可以先用 re.compile() 编译成对象,这样能省掉每次编译的开销。
import re
# 编译一次
tel_pattern = re.compile(r'^(\d{3})-(\d{3,8})$')
# 多次使用
m1 = tel_pattern.match('010-12345')
print(m1.groups()) # ('010', '12345')
m2 = tel_pattern.match('010-8086')
print(m2.groups()) # ('010', '8086')编译后得到的是一个正则对象,调用 match、search、findall、split、sub 时不需要再传正则字符串了。
7. 小结
相关信息
正则表达式是处理字符串的超级利器:用 \d、\w、. 匹配字符,用 *、+、?、{n}、{n,m} 控制数量,用 []、|、^、$ 控制范围和位置,用 () 分组提取内容。Python 通过 re 模块提供 match、search、findall、split、sub 等方法,还提供了 re.compile 来复用编译结果。正则很强大,但也别指望它做所有事,复杂逻辑配合代码判断会更靠谱。