完整讲解 · 5 段教学·配 4 道练习题·预计 35 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
正则表达式(Regular Expression,简称 regex) 是一种用「特殊字符序列」描述字符串模式的语言。
它能回答这一类问题:
Python 自 1.5 版本起就内置了 re 模块,提供 Perl 风格的正则功能。
先看一个反面例子。要判断字符串里是否包含某段固定文字,根本不需要正则:
a = '两点水|twowater|liangdianshui|草根程序员'
print('两点水' in a) # True
print(a.index('两点水') > -1) # True这种纯「子串查找」用 in 就够了。
正则真正发力的地方是——你不知道具体的字符是什么,但知道它们的形状。比如:
「找出字符串里所有的连续小写字母」
import re
a = '两点水|twowater|liangdianshui|草根程序员'
print(re.findall('[a-z]+', a))
# ['twowater', 'liangdianshui'][a-z]+ 就是一条「规则」:
[a-z] 代表「任何一个小写字母」+ 代表「连续出现一次以上」合起来就是「一段小写字母」。re.findall 把字符串里所有匹配的片段都列出来。
re.match / re.search / re.findall:三个最常用的函数(...):从匹配里提取片段re.sub:基于规则做替换学完你就有能力写出「找手机号」「抓邮箱」「清洗多余空格」这类常用工具了。
正则之所以「叫正则」,是因为它有一套自己的小语法。这里只学最常用的两类:字符集 / 元字符 和 量词。
[...]:「或」的关系方括号 [...] 里写一组字符,表示匹配其中任意一个。
import re
a = 'uav,ubv,ucv,uwv,uzv,uov'
# 取 u 和 v 中间是 a / b / c 的
print(re.findall('u[abc]v', a))
# ['uav', 'ubv', 'ucv']
# 连续字母可以用 - 缩写
print(re.findall('u[a-c]v', a))
# ['uav', 'ubv', 'ucv']
# 取反:u 和 v 中间不是 a / b / c 的
print(re.findall('u[^abc]v', a))
# ['uwv', 'uzv', 'uov']要点:
[abc] 是 a 或 b 或 c[a-z] 是连续范围:所有小写字母[^abc] 在开头加 ^ 表示取反下面这几个是最常用的预定义字符集,建议记下来:
| 写法 | 等价 | 含义 |
|---|---|---|
\d | [0-9] | 任何数字 |
\D | [^0-9] | 非数字 |
\w | [A-Za-z0-9_] | 单词字符(字母 / 数字 / 下划线) |
\W | [^A-Za-z0-9_] | 非单词字符 |
\s | [ \t\n\r\f\v] | 空白字符 |
\S | 非空白字符 | |
. | 任意一个字符(除换行外) |
import re
a = 'hi 123-abc'
print(re.findall(r'\d', a)) # ['1', '2', '3']
print(re.findall(r'\w', a)) # ['h', 'i', '1', '2', '3', 'a', 'b', 'c']注意:正则字符串里反斜杠
\容易和 Python 自身的转义冲突——养成习惯,正则全部用r'...'原始字符串。
光匹配「一个」字符不够用。我们要表达重复几次,就要用量词:
| 写法 | 含义 |
|---|---|
? | 0 次或 1 次 |
+ | 1 次或多次 |
* | 0 次或多次 |
{n} | 恰好 n 次 |
{n,m} | n 到 m 次 |
{n,} | 至少 n 次 |
例子——找出 4 到 7 个字母组成的英文单词:
import re
a = 'java*&39android##@@python'
print(re.findall('[a-z]{4,7}', a))
# ['java', 'android', 'python']默认量词是贪婪的——能多吃就多吃:
print(re.findall('[a-z]{4,7}', 'android'))
# ['android'] ← 一次吃满 7 个在量词后面加 ? 变成非贪婪——能少吃就少吃:
print(re.findall('[a-z]{4,7}?', 'android'))
# ['andr'] ← 只吃 4 个就回头| 贪婪 | 非贪婪 | 描述 |
|---|---|---|
? | ?? | 0 次或 1 次 |
+ | +? | 1 次或多次 |
* | *? | 0 次或多次 |
{n,m} | {n,m}? | n 到 m 次 |
经验:写正则时默认是贪婪——这往往不是你想要的。要小心,必要时加
?。
re.match / re.search / re.findallre 模块里最常用的三个函数。它们都接收两个参数:正则规则 和 要查的字符串,但查找方式不同。
re.match:只看开头re.match(pattern, string)re.match 只在字符串的最开头尝试匹配——不在开头就直接返回 None。
import re
m1 = re.match(r'\d+', '123abc456')
print(m1) # <re.Match object; span=(0, 3), match='123'>
print(m1.group()) # '123'
m2 = re.match(r'\d+', 'abc123')
print(m2) # None ← 开头不是数字,直接 None适合做格式校验:「这个字符串是不是以某种规则开头」。
re.search:扫描整个字符串re.search(pattern, string)re.search 会扫整个字符串,找到第一个匹配就返回。
import re
m = re.search(r'\d+', 'abc123def456')
print(m.group()) # '123' ← 找到第一个就停re.match 和 re.search 返回的都是 Match 对象(或 None)。常用方法:
| 方法 | 含义 |
|---|---|
m.group() / m.group(0) | 整个匹配 |
m.group(1) | 第 1 个分组(下节讲) |
m.start() / m.end() | 匹配位置 |
m.span() | (start, end) 元组 |
re.findall:找出全部re.findall(pattern, string)返回一个列表,包含所有不重叠的匹配。
import re
print(re.findall(r'\d+', 'abc123def456ghi789'))
# ['123', '456', '789']findall 通常是日常使用最多的——一次拿到所有结果,不用循环。
| 函数 | 找几个 | 不匹配时返回 |
|---|---|---|
re.match | 最多 1 个,只看开头 | None |
re.search | 最多 1 个,扫整个 | None |
re.findall | 全部 | [] |
import re
s = 'abc123def456'
print(re.match(r'\d+', s)) # None(开头不是数字)
print(re.search(r'\d+', s)) # <Match '123'>
print(re.findall(r'\d+', s)) # ['123', '456']选择建议:要校验/解析格式用
match;要在长文本里找东西用search或findall。
(...):从匹配里提取片段到目前为止,我们只能判断匹配 / 不匹配,或者拿到完整的匹配字符串。
但实际场景里,我们常常想要**「把匹配的某一部分单独抽出来」**,比如:
从
[email protected]中分别拿到alice和example.com
这就要用 分组(group)——在正则里用一对括号 (...) 把想抽取的部分包起来。
import re
m = re.match(r'(\w+)@(\w+\.\w+)', '[email protected]')
print(m.group(0)) # [email protected] ← 完整匹配
print(m.group(1)) # alice ← 第 1 个分组
print(m.group(2)) # example.com ← 第 2 个分组
print(m.groups()) # ('alice', 'example.com')要点:
group(0) 是整个匹配(不算分组)groups() 一次性把所有分组返回成元组re.findall 与分组的化学反应如果正则有分组,re.findall 返回的就不再是完整匹配——而是分组的内容:
import re
a = '<img src="https://a.com/1.jpg"><img src="https://b.com/2.png">'
# 没分组:返回完整匹配
print(re.findall(r'<img src="\S+">', a))
# ['<img src="https://a.com/1.jpg">', '<img src="https://b.com/2.png">']
# 有 1 个分组:返回该分组内容的列表
print(re.findall(r'<img src="(\S+)">', a))
# ['https://a.com/1.jpg', 'https://b.com/2.png']
# 多个分组:返回元组的列表
print(re.findall(r'<(\w+) src="(\S+)">', a))
# [('img', 'https://a.com/1.jpg'), ('img', 'https://b.com/2.png')]这是个常见坑:当你给一个原本好用的 findall 加括号后,结果突然「变了形状」——这正是分组生效的标志。
在分组里用 (?P<name>...) 给它起名,之后用 group('name') 取:
import re
m = re.match(r'(?P<user>\w+)@(?P<host>\S+)', '[email protected]')
print(m.group('user')) # alice
print(m.group('host')) # example.com适合正则比较长、分组比较多的场合,让代码更易读。
(?:...)有时候你只想用括号做语法分组(比如配合 | 选择),但不需要它捕获——加 ?::
import re
# 普通分组:会被 findall 抓出来
print(re.findall(r'(cat|dog)s?', 'cats and dogs'))
# ['cat', 'dog']
# 不捕获分组:不进 group
print(re.findall(r'(?:cat|dog)s?', 'cats and dogs'))
# ['cats', 'dogs']re.sub:基于规则替换字符串自带的 s.replace(old, new) 只能替换固定子串。如果想「按规则」替换——比如「把所有数字替换成 *」——就要用 re.sub。
re.sub(pattern, repl, string, count=0)| 参数 | 含义 |
|---|---|
pattern | 正则规则 |
repl | 用来替换的字符串(或函数,见下) |
string | 原字符串 |
count | 最多替换几次(0 表示全部) |
import re
a = 'Python*Android*Java-888'
# 把 * 替换成 &
print(re.sub(r'\*', '&', a))
# Python&Android&Java-888
# 只替换第一个
print(re.sub(r'\*', '&', a, count=1))
# Python&Android*Java-888写正则最经典的小工具——把多个连续空白替换成一个空格:
import re
s = 'hello world python is cool'
print(re.sub(r'\s+', ' ', s))
# hello world python is cool\s+ 匹配「一个或多个空白字符」。整段被替换成单个空格——多个变一个。
repl 可以是一个函数——它接收 Match 对象,返回一个字符串作为替换值。
import re
a = 'Python*Android*Java-888'
def convert(m):
g = m.group()
if g == '*':
return '&'
elif g == '-':
return '|'
print(re.sub(r'[\*\-]', convert, a))
# Python&Android&Java|888或者用 lambda 简写:
import re
# 把所有数字翻倍
print(re.sub(r'\d+', lambda m: str(int(m.group()) * 2), 'a1 b2 c3'))
# a2 b4 c6这是 re.sub 比 str.replace 强大的关键——你能对每一个匹配,按它的内容动态决定替换值。
学完这一章,你已经掌握了:
[abc]、\d / \w / \s、.? / + / * / {n,m},以及贪婪 / 非贪婪re.match / re.search / re.findall 三个查找函数(...) 提取片段,命名分组 (?P<name>...),不捕获分组 (?:...)re.sub 基于规则替换,可传函数下面去做练习,把这些工具用起来。
正则是非常强的工具——通常用来解决字符串内置函数解决不了的问题。爬虫、数据清洗、日志分析里几乎离不开它。
读完了?动手练一遍才算真会。
去闯关模式练习 →