小白学 Python
AI 编程指南GitHub
© 2026 小白学 Python · 基于 walter201230/Python 教程
课程目录关于本站联系方式隐私政策GitHub

正则表达式

完整讲解 · 5 段教学·配 4 道练习题·预计 35 分钟

本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。

教学 01 / 05

第一节:正则表达式

正则表达式(Regular Expression,简称 regex) 是一种用「特殊字符序列」描述字符串模式的语言。

它能回答这一类问题:

  • 这个字符串里有没有手机号?
  • 这段文字里所有的邮箱抓出来
  • 把多余的空白替换成一个空格
  • 这段输入是不是合法的身份证号?

Python 自 1.5 版本起就内置了 re 模块,提供 Perl 风格的正则功能。

用 Python 字符串能解决的,就别用正则

先看一个反面例子。要判断字符串里是否包含某段固定文字,根本不需要正则:

python到闯关页运行这段 →
a = '两点水|twowater|liangdianshui|草根程序员'

print('两点水' in a)   # True
print(a.index('两点水') > -1)  # True

这种纯「子串查找」用 in 就够了。

正则的强项:「带规则」的查找

正则真正发力的地方是——你不知道具体的字符是什么,但知道它们的形状。比如:

「找出字符串里所有的连续小写字母」

python到闯关页运行这段 →
import re

a = '两点水|twowater|liangdianshui|草根程序员'
print(re.findall('[a-z]+', a))
# ['twowater', 'liangdianshui']

[a-z]+ 就是一条「规则」:

  • [a-z] 代表「任何一个小写字母」
  • + 代表「连续出现一次以上」

合起来就是「一段小写字母」。re.findall 把字符串里所有匹配的片段都列出来。

本节学习路线

  1. 元字符 + 量词:写出第一条正则规则
  2. re.match / re.search / re.findall:三个最常用的函数
  3. 分组 (...):从匹配里提取片段
  4. re.sub:基于规则做替换

学完你就有能力写出「找手机号」「抓邮箱」「清洗多余空格」这类常用工具了。

教学 02 / 05

二、元字符与量词

正则之所以「叫正则」,是因为它有一套自己的小语法。这里只学最常用的两类:字符集 / 元字符 和 量词。

1、字符集 [...]:「或」的关系

方括号 [...] 里写一组字符,表示匹配其中任意一个。

python到闯关页运行这段 →
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] 在开头加 ^ 表示取反

2、概括字符集(最常用)

下面这几个是最常用的预定义字符集,建议记下来:

写法等价含义
\d[0-9]任何数字
\D[^0-9]非数字
\w[A-Za-z0-9_]单词字符(字母 / 数字 / 下划线)
\W[^A-Za-z0-9_]非单词字符
\s[ \t\n\r\f\v]空白字符
\S非空白字符
.任意一个字符(除换行外)
python到闯关页运行这段 →
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'...' 原始字符串。

3、量词

光匹配「一个」字符不够用。我们要表达重复几次,就要用量词:

写法含义
?0 次或 1 次
+1 次或多次
*0 次或多次
{n}恰好 n 次
{n,m}n 到 m 次
{n,}至少 n 次

例子——找出 4 到 7 个字母组成的英文单词:

python到闯关页运行这段 →
import re

a = 'java*&39android##@@python'
print(re.findall('[a-z]{4,7}', a))
# ['java', 'android', 'python']

4、贪婪 vs 非贪婪

默认量词是贪婪的——能多吃就多吃:

python到闯关页运行这段 →
print(re.findall('[a-z]{4,7}', 'android'))
# ['android']  ← 一次吃满 7 个

在量词后面加 ? 变成非贪婪——能少吃就少吃:

python到闯关页运行这段 →
print(re.findall('[a-z]{4,7}?', 'android'))
# ['andr']     ← 只吃 4 个就回头
贪婪非贪婪描述
???0 次或 1 次
++?1 次或多次
**?0 次或多次
{n,m}{n,m}?n 到 m 次

经验:写正则时默认是贪婪——这往往不是你想要的。要小心,必要时加 ?。

练习 1 / 4用 re.findall 抓出全部数字去闯关页做这题 →
教学 03 / 05

三、re.match / re.search / re.findall

re 模块里最常用的三个函数。它们都接收两个参数:正则规则 和 要查的字符串,但查找方式不同。

1、re.match:只看开头

python到闯关页运行这段 →
re.match(pattern, string)

re.match 只在字符串的最开头尝试匹配——不在开头就直接返回 None。

python到闯关页运行这段 →
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

适合做格式校验:「这个字符串是不是以某种规则开头」。

2、re.search:扫描整个字符串

python到闯关页运行这段 →
re.search(pattern, string)

re.search 会扫整个字符串,找到第一个匹配就返回。

python到闯关页运行这段 →
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) 元组

3、re.findall:找出全部

python到闯关页运行这段 →
re.findall(pattern, string)

返回一个列表,包含所有不重叠的匹配。

python到闯关页运行这段 →
import re

print(re.findall(r'\d+', 'abc123def456ghi789'))
# ['123', '456', '789']

findall 通常是日常使用最多的——一次拿到所有结果,不用循环。

4、三者对比

函数找几个不匹配时返回
re.match最多 1 个,只看开头None
re.search最多 1 个,扫整个None
re.findall全部[]
python到闯关页运行这段 →
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。

练习 2 / 4用 re.match 校验手机号去闯关页做这题 →
教学 04 / 05

四、分组 (...):从匹配里提取片段

到目前为止,我们只能判断匹配 / 不匹配,或者拿到完整的匹配字符串。

但实际场景里,我们常常想要**「把匹配的某一部分单独抽出来」**,比如:

从 [email protected] 中分别拿到 alice 和 example.com

这就要用 分组(group)——在正则里用一对括号 (...) 把想抽取的部分包起来。

1、基本用法

python到闯关页运行这段 →
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')

要点:

  • 分组从 1 开始编号——左括号出现的顺序就是编号顺序
  • group(0) 是整个匹配(不算分组)
  • groups() 一次性把所有分组返回成元组

2、re.findall 与分组的化学反应

如果正则有分组,re.findall 返回的就不再是完整匹配——而是分组的内容:

python到闯关页运行这段 →
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 加括号后,结果突然「变了形状」——这正是分组生效的标志。

3、命名分组(高级用法)

在分组里用 (?P<name>...) 给它起名,之后用 group('name') 取:

python到闯关页运行这段 →
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

适合正则比较长、分组比较多的场合,让代码更易读。

4、不捕获分组 (?:...)

有时候你只想用括号做语法分组(比如配合 | 选择),但不需要它捕获——加 ?::

python到闯关页运行这段 →
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']
练习 3 / 4用分组提取邮箱的用户名和域名去闯关页做这题 →
教学 05 / 05

五、re.sub:基于规则替换

字符串自带的 s.replace(old, new) 只能替换固定子串。如果想「按规则」替换——比如「把所有数字替换成 *」——就要用 re.sub。

1、基本用法

python到闯关页运行这段 →
re.sub(pattern, repl, string, count=0)
参数含义
pattern正则规则
repl用来替换的字符串(或函数,见下)
string原字符串
count最多替换几次(0 表示全部)
python到闯关页运行这段 →
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

2、最实用的例子:清洗多余空白

写正则最经典的小工具——把多个连续空白替换成一个空格:

python到闯关页运行这段 →
import re

s = 'hello   world  python  is   cool'
print(re.sub(r'\s+', ' ', s))
# hello world python is cool

\s+ 匹配「一个或多个空白字符」。整段被替换成单个空格——多个变一个。

3、用 lambda 做复杂替换

repl 可以是一个函数——它接收 Match 对象,返回一个字符串作为替换值。

python到闯关页运行这段 →
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 简写:

python到闯关页运行这段 →
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 基于规则替换,可传函数

下面去做练习,把这些工具用起来。

正则是非常强的工具——通常用来解决字符串内置函数解决不了的问题。爬虫、数据清洗、日志分析里几乎离不开它。

练习 4 / 4用 re.sub 把多空格压成单空格去闯关页做这题 →

读完了?动手练一遍才算真会。

去闯关模式练习 →