W
e
l
c
o
m
e
: )

12.正则表达式 ★★★

本篇导航

正则表达式是文本处理的通用语言,也是本系列标注 ★★★ 的重难点篇。本篇路线:
§1 概述(是什么、能干什么)→ §2 分类(BRE / ERE / PCRE 三大流派)→ §3 基本语法(元字符、转义、限定符、或、边界、捕获分组、断言、贪婪与非贪婪、引擎选项,这一层是跨语言通用的)→ §4 Python 的正则(re 模块:compile / match / search / fullmatch / findall / finditer / sub / subn / split / 分组取值)。

阅读建议:§3.6 捕获分组、§3.7 断言、§3.8 贪婪与非贪婪是三大难点;§4 的方法族看似多,其实只有两条主线——单次匹配(match / search / fullmatch)与全文搜索(findall / finditer),抓住「从哪开始匹配、是否要求整体匹配、返回什么」三个问题即可。

环境说明:本篇所有实测结论基于 Python 3.13.14 + re 模块(标准库自带 re,无需安装)。文中凡涉及「Python 方言行为」处均已实跑验证并标注输出。

原文勘误(全篇文字性笔误汇总,已直接修正)

原文由 PDF/截图 OCR 转写而来,散落若干笔误。此类不改动原意的错字已直接改正,在此汇总以便对照:

位置 原文 已改为
§4.9 原文写作「finditer 在字符串中找到正则表达式所匹配的所有子串,并把它们替换为指定的内容」(描述的是 subn,函数名写错) 「subn 在字符串中找到正则表达式所匹配的所有子串,并把它们替换为指定的内容」
§4.10 原文写作「finditer 在字符串中根据指定的正则表达式对字符串进行分割」(描述的是 split,函数名写错) 「split 在字符串中根据指定的正则表达式对字符串进行分割」
§4.10 标题 分割字符串——spilt、子标题 `spilt` 方法示例 split(英文拼写错,spilt 是 spill 的过去式)
§3.8 贪婪模式指数量词默认是贪婪的 贪婪模式指限定符默认是贪婪的(术语笔误,本节标题正是「重复(限定符)」)
§3.8 在重复的符号是后面加上一个 在重复的符号后面加上一个(衍字「是」)
§4.7 每次选代返回的是 match 对象 每次迭代返回的是 match 对象(选代→迭代)
§4.11 4 处 # 不写分组好,默认为 0print(res.group()) 一类注释与代码挤在同一行 拆成两行;并修正 不写分组好→不写分组号
§3.9 / §4.1 表格含 re.L(LOCALE)却未说明限制 补实测:re.L 仅适用于 bytes 模式,str 模式会抛 ValueError(见 §3.9 勘误)

1 概述

正则表达式,Regular Expression,缩写为 regex、regexp、RE 等。

正则表达式是对字符串操作的一种逻辑公式:用事先定义好的一些特定字符、以及这些特定字符的组合,组成一个「规则字符串」,用它来表达对字符串的一种过滤逻辑(可用于检索、截取或替换)。

正则表达式是文本处理极为重要的技术,用它可以对字符串按照某种规则进行检索、替换。

1970 年代,Unix 之父 Ken Thompson 将正则表达式引入到 Unix 中文本编辑器 ed 和 grep 命令中,由此正则表达式普及开来。

1980 年后,perl 语言对 Henry Spencer 编写的库,扩展了很多新的特性。1997 年开始,Philip Hazel 开发出了 PCRE(Perl Compatible Regular Expressions),它被 PHP 和 HTTPD 等工具采用。

正则表达式应用极其广泛,shell 中处理文本的命令、各种高级编程语言都支持正则表达式。

作用:

  • 给定的字符串是否符合正则表达式的过滤逻辑(称作「匹配」)。
  • 可以通过正则表达式,从字符串中获取我们想要的特定部分。
  • 还可以对目标字符串进行替换操作。
注意

在一次字符串处理中,指针只能向前,不能回退。

补充:Python 的正则引擎

CPython 的 re 模块底层是 SRE 引擎(SRE = Standard Regular Expression,由 Fredrik Lundh 编写)。
官方资料记载:Python 1.6 引入了 SRE,用来同时支持 8 位字节串与 Unicode 字符串,取代了此前基于 PCRE 的 re 实现
(PEP 160;Python 1.6 发布说明「The new regular expression engine, SRE by Fredrik Lundh」)。
也就是说:Python 的语法是 PCRE 家族的方言,但引擎实现是自研的 SRE——这正是「同一个正则在不同语言里行为可能不同」的根源。

2 分类

  1. BRE

    基本正则表达式,grep、sed、vi 等软件支持。vim 有扩展。

  2. ERE

    扩展正则表达式,egrep(grep -E)、sed -r 等。

  3. PCRE

    几乎所有高级语言都是 PCRE 的方言或者变种。Python 从 1.6 开始使用 SRE 正则表达式引擎,可以认为是 PCRE 的子集,见模块 re。

三种流派的关系
  • BRE(Basic Regular Expression,基本正则):grep、sed、vi 默认使用,元字符需要转义才生效(如 \+、\|)。
  • ERE(Extended Regular Expression,扩展正则):egrep / grep -E、sed -r / sed -E 使用,元字符直接用(+、| 无需转义)。
  • PCRE(Perl Compatible Regular Expressions):Perl 5 的语法,功能最强,被 PHP、Python、Java、JavaScript(近似)、Nginx 等广泛采用。

记住一句话:BRE / ERE 是 shell 世界的历史包袱,PCRE 才是现代编程语言的主流。本笔记后续全部基于 PCRE/Python 方言。

3 基本语法

3.1 元字符

metacharacter

代码 说明 举例
. 匹配换行符(\n)以外的任意一个字符 .
[abc] 字符集合,只能表示一个字符位置。
匹配所包含的任意一个字符。
[abc] 匹配 "plain" 中 'a'
[^abc] 字符集合,只能表示一个字符位置。
匹配除去集合内字符的任意一个字符
[^abc] 可以匹配 "plain" 中的 'p'、'l'、'i' 或者 'n'
[a-z] 字符范围,也是个集合,表示一个字符位置
匹配所包含的任意一个字符
常用 [A-Z] [0-9]
[^a-z] 字符范围,也是个集合,表示一个字符位置
匹配除去集合内字符的任意一个字符
\b 匹配一个单词边界,也就是指单词和空格间的位置 er\b 可以匹配以 "er" 结尾的单词中的 "er"
\bab 可以匹配以 "ab" 开头的单词中的 "ab"
\B 匹配非单词边界 t\B 匹配包含 't' 的单词但是不以 't' 结尾的单词中的 't',例如 "write" 中的 't'
\Bb 不以 'b' 开头的含有 'b' 的单词,例如 "able" 中的 'b'
\d 匹配一个数字字符。等价于 [0-9]
\D 匹配一个非数字字符。等价于 [^0-9]
\s 匹配一个任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]
\S 匹配一个任何非空白字符。等价于 [^ \f\n\r\t\v]
\w 匹配一个字母、数字、下划线,包括汉字。等价于 [A-Za-z0-9_] + 所有汉字 Python 等方言中 \w 可以匹配中文
PCRE(PHP) 等方言中 \w 无法匹配中文
使用时注意看清 re 使用的规则
\W 匹配一个非字母、数字、下划线及汉字
原文勘误:\d / \s / \w 的「等价于」写法在 Python 下并不严格成立

上表沿用了通用教程的口径,把 \d \s \w 写成等价于 ASCII 字符类。这在 Python 的 str(Unicode)模式下不准确,实测如下(Python 3.13.14):

简写 原文写的等价类 实测(str 模式,默认) 要得到原文的等价类需加 re.ASCII
\d [0-9] ✅ 5;❌ 也匹配 '٥'(阿拉伯-印度数字)、'3'(全角)等 Unicode 十进制数字(Nd 类) re.ASCII 下退化为 [0-9]
\s [ \f\n\r\t\v] ✅ 这 6 个;❌ 还匹配 '\x1c'、'\x85'、'\xa0'(NBSP)等 Unicode 空白 re.ASCII 下退化为这 6 个
\w [A-Za-z0-9_] + 所有汉字 实际是 Unicode 词字符 = 全部 Unicode 字母 + 数字 + 下划线(等价 str.isalnum() or ch == '_')。反例:'е'(西里尔字母)、'Ⅲ'(罗马数字)、'²'(上标)全都匹配 \w,但它们既不在 [A-Za-z0-9_] 也不是汉字 re.ASCII 下退化为 [a-zA-Z0-9_]
\W 非字母数字下划线及汉字 默认模式下 \W 确实不匹配汉字(re.match(r'\W','小') → None),与原文一致;但 re.ASCII 下汉字不属于 \w,反而会被 \W 命中 —

官方原文(re 模块文档):\d「For Unicode (str) patterns: Matches any Unicode decimal digit (that is, any character in Unicode character category [Nd]). This includes [0-9], and also many other digit characters. Matches [0-9] if the ASCII flag is used.」
结论:原文的「等价于」结论只在 re.ASCII 下成立;默认 str 模式请按「Unicode 语义」理解。

示例:元字符的使用

在线正则

原始文本:

hello world
小龙女

正则表达式:

.

匹配结果:

h
e
l
l
o
 
w
o
r
l
d
小
龙
女
注意

以下写法,中括号中的 . 没有特殊意义,只能匹配点号 .。加 \ 转义字符也没用。

[.]

原始文本:

hello world
小龙女

正则表达式:(使用 Python 方言)

Pasted-image-20250410175943

\w

匹配结果:

h
e
l
l
o
w
o
r
l
d
小
龙
女

原始文本:

hello world
小龙女

正则表达式:(使用 Python 方言)

\w\w

匹配结果:

he
ll
wo
rl
小龙

原始文本:

hello world
123
小龙女

正则表达式:

\d

匹配结果:

1
2
3

原始文本:

hello world
123
小龙女

正则表达式:

[heo ]

匹配结果:

h
e
o
 
o

原始文本:

hello world
123 4
小龙女

正则表达式:

[heo\s]

匹配结果:

Pasted-image-20250410181850


原始文本:

hello world
123 4
小龙女

正则表达式:

lo\s

匹配结果:

lo 

原始文本:

hello world
123 4
小龙女

正则表达式:

\d\s\d

匹配结果:

3 4

原始文本:

hello world
123 4
小龙女

正则表达式:

[\d\s]

匹配结果:

Pasted-image-20250410182653


原始文本:

hello world
12 34
小龙女

正则表达式:

\w\s\w

匹配结果:

Pasted-image-20250410183159


原始文本:

hello world
12 34
小龙女

正则表达式:

\w\s\d

匹配结果:

Pasted-image-20250410183435


原始文本:

abc xyz
12 34
中文.

正则表达式:

\b\w

匹配结果:

Pasted-image-20250410210214


原始文本:

abc xyz
12 34
中文.

正则表达式:

\b\w\w

匹配结果:

Pasted-image-20250410210505


原始文本:

abc xyz
12 34
中文.

正则表达式:

\b\w\w\w

匹配结果:

Pasted-image-20250410210535


原始文本:

abc xyz
12 34
中文.

正则表达式:

\b\w\w\b

匹配结果:

Pasted-image-20250410210728

上列示例的文本级复算(真实运行结果,未删减)

截图无法检索、复制,故把「原文给了截图」的几例用 re.findall 复算,便于对照核对(Python 3.13.14):

import re

re.findall(r'[heo\s]', 'hello world\n123 4\n小龙女')
# ['h', 'e', 'o', ' ', 'o', '\n', ' ', '\n']

re.findall(r'[\d\s]', 'hello world\n123 4\n小龙女')
# [' ', '\n', '1', '2', '3', ' ', '4', '\n']

re.findall(r'\w\s\w', 'hello world\n12 34\n小龙女')
# ['o w', 'd\n1', '2 3', '4\n小']

re.findall(r'\w\s\d', 'hello world\n12 34\n小龙女')
# ['d\n1', '2 3']

re.findall(r'\b\w', 'abc xyz\n12 34\n中文.')
# ['a', 'x', '1', '3', '中']

re.findall(r'\b\w\w', 'abc xyz\n12 34\n中文.')
# ['ab', 'xy', '12', '34', '中文']

re.findall(r'\b\w\w\w', 'abc xyz\n12 34\n中文.')
# ['abc', 'xyz']

re.findall(r'\b\w\w\b', 'abc xyz\n12 34\n中文.')
# ['12', '34', '中文']

注意最后两例:\b\w\w\w 匹配不到 '中文.'(. 不是 \w,凑不满 3 个词字符),
\b\w\w\b 反而能匹配 '中文'——\b 是按 \w 与 \W 的分界定义的,汉字属于 \w,所以汉字组词与英文字母一样受 \b 约束。

3.2 转义

凡是在正则表达式中有特殊意义的符号,如果想使用它的本意,请使用 \ 转义。反斜杠自身,得使用 \\。

\r、\n 还是转义后代表回车、换行。

补充:字符类内部无需转义

在 [...] 字符类内部,^(非首位时)、.、*、+、? 等都失去特殊含义,直接就是字面量:

re.findall(r'[.]', 'a.b')       # ['.'],字符类内的 . 就是点号本身
re.findall(r'[*+?]', 'a*b+c?')  # ['*', '+', '?']

但字符类内 ]、\、^(首位)、-(非首尾)仍需转义:

re.findall(r'[\]]', 'a]b')   # [']']
re.findall(r'[a\-z]', 'a-z') # ['a', '-', 'z']

另外官方文档特别提醒:字符类内的 \b 表示退格符(\x08),不再是单词边界。

3.3 重复(限定符)

匹配手机号码需要形如 \d\d\d\d\d\d\d\d\d\d\d 这样的模式,其中 \d 重复 11 次,写法烦琐。正则表达式作为一门小型的语言,提供了对表达式某一部分进行重复处理的功能:* 就能让某个部分重复匹配多次,这类符号称为限定符。

代码 说明 举例
* 匹配前面的子表达式零次或多次 例如,zo* 能匹配以 'z' 开头的后跟任意个 'o' 的字符。
+ 匹配前面的子表达式一次或多次 例如,zo+ 能匹配以 'z' 开头的后跟至少一个 'o' 的字符。
? 匹配前面的子表达式零次或一次 do? 可以匹配 'd' 或 'do'。
{n} n 是一个非负整数。匹配确定的 n 次 o{2} 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 'o'。
{n,} n 是一个非负整数。至少匹配 n 次 o{2,} 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 'o'。
o{1,} 等价于 o+。
o{0,} 则等价于 o*。
{n,m} m 和 n 均为非负整数,其中 n <= m。
最少匹配 n 次且最多匹配 m 次
o{1,3} 将匹配 "fooooood" 中的前三个 'o'。
o{0,1} 等价于 o?。
请注意在逗号和两个数之间不能有空格。
限定符的三个易错点(实测)
  1. {n,m} 的花括号里不能有空格。原文说「不能有空格」,实测其真实后果是:o{1, 3} 不报错,而是整段被当作字面量,于是匹配不到任何东西:

    re.findall(r'o{1,3}',  'fooooood')  # ['ooo', 'ooo']  ← 7 个 o 被拆成 3 + 3 + 1(非重叠)
    re.findall(r'o{1, 3}', 'fooooood')  # []  ← 含空格后不是限定符了
  2. 量词只作用于「紧邻的前一个元素」。zo* 中的 * 只管 o,不管 z。

  3. 匹配是非重叠的。o{1,3} 对 7 个 o 得到 3+3+1 两段(余下 1 个不足 3 但满足下限 1,单独成段),而不是滑动出多个窗口。
    想要重叠匹配需要用零宽断言(见 §3.7):re.findall(r'(?=(\w\w))', 'abc') → ['ab', 'bc']。

示例:限定符的使用

原始文本:

abc xyz
12 34
中文好的.abc

正则表达式:

\w+

匹配结果:

Pasted-image-20250410211446


原始文本:

abc xyz
12 34
中文好的.abc

正则表达式:

\w*

匹配结果:

Pasted-image-20250410211736


原始文本:

abc xyz
12 34
中文好的.abc

正则表达式:

\w?

匹配结果:

Pasted-image-20250410212033

注意:* 和 ? 允许「零次」,会产生空匹配

\w* 与 \w? 都允许匹配「0 个字符」,因此结果列表里会出现空字符串 '':

re.findall(r'\w+', 'abc xyz')  # ['abc', 'xyz']
re.findall(r'\w*', 'abc xyz')  # ['abc', '', 'xyz', '']  ← 空格处匹配了空串

这是初学时最容易被截图结果「看不懂」的原因:截图里那些空行就是空匹配。


原始文本:

abc xyz m
12 34
中文好的.abc

正则表达式:

\w\w*

匹配结果:

Pasted-image-20250410212838


原始文本:

abc xyz m
12 34
中文好的.abc

正则表达式:

\w\w+

匹配结果:

Pasted-image-20250410213125


原始文本:

abc xyz m
12 34
中文好的.abc

正则表达式:

\w\w?

匹配结果:

Pasted-image-20250412155827

\w\w* / \w\w+ / \w\w? 的区别(实测)

三者都是「先吃掉 1 个词字符,再看下一个」:

s = 'abc xyz m\n12 34\n中文好的.abc'
re.findall(r'\w\w*', s)  # ['abc', 'xyz', 'm', '12', '34', '中文好的', 'abc']  ← 至少 1 个,多多益善
re.findall(r'\w\w+', s)  # ['abc', 'xyz', '12', '34', '中文好的', 'abc']        ← 至少 2 个,所以单独的 m 落空
re.findall(r'\w\w?', s)  # ['ab', 'c', 'xy', 'z', 'm', '12', '34', '中文', '好的', 'ab', 'c']  ← 至多 2 个

口诀:\w\w* 与 \w+ 等价;\w\w? 等价于「长度 1 或 2」。单写 \w 时 ? 的「0 次」才会暴露成空匹配,前面有确定性的 \w 兜底后就不会了。


原始文本:

abc xyz m
12 34
中文好的.ef

正则表达式:

\w\w*

匹配结果:

Pasted-image-20250410213814

3.4 或

代码 说明 举例
x|y 匹配 x 或者 y "wood took foot food"
使用 wood|f 可以匹配到 'wood'、'f'、'f'。
(w|f)ood 可以匹配到 'wood'、'food'。
原文勘误:上表两处转发义符号

Markdown 表格里 \| 是为了显示竖线而写的转义,真正的正则写法是 x|y(无需转义)。
复制到代码里请直接用 wood|f、(w|f)ood,写成 wood\|f 会变成「匹配字面竖线」。

示例:正则中的或

原始文本:

took food wood foot

正则表达式:

f|wood

匹配结果:

Pasted-image-20250412155227


原始文本:

took food wood foot

正则表达式:

(f|w)ood

匹配结果:

Pasted-image-20250412160215

注意

一定是要先匹配上,然后才会在结果中进行分组。有一对括号就会分一组。


原始文本:

took food wood foot

正则表达式:

(?:f|w)ood

匹配结果:括号只为了改变优先级,不进行分组

Pasted-image-20250412161753

| 的「短路」语义(实测,易错)

| 从左到右尝试,一旦某个分支整体匹配成功就立即采纳,不会再试后面的分支——即使后面的分支能匹配更长的串。
所以 f|wood 在 "wood" 上先命中 f 吗?实测不是:

re.findall(r'f|wood', 'took food wood foot')   # ['f', 'wood', 'f']
re.findall(r'wood|f', 'took food wood foot')   # ['wood', 'f', 'f']

两行结果不同,因为 | 是从左往右试:f|wood 遇到 wood 时,第 1 个分支 f 在当前位置(w)匹配失败,才轮到 wood。
写 | 时把「更具体/更长」的分支放前面,是避免踩坑的通用建议。

3.5 边界字符

代码 说明 举例
^ 字符串的开头
$ 字符串的结尾
\b 匹配一个单词边界,也就是指单词和空格间的位置
\B 匹配非单词的边界
注意

^ 与 [^m] 中的 ^ 的含义并不相同,后者 ^ 表示除了…的意思

原文勘误:Python 中 ^ / $ 不只是「字符串的开头/结尾」

上表是通用教程的简写,在 Python 中默认模式下 $ 还有一个额外位置:
官方文档原文——「$ Matches the end of the string or just before the newline at the end of the string, and in MULTILINE mode also matches before a newline.」
实测(Python 3.13.14):

bool(re.search(r'e$', 'e'))     # True
bool(re.search(r'e$', 'e\n'))   # True   ← $ 也匹配「末尾 \n 之前」
bool(re.search(r'e$', 'e\r\n')) # False  ← 末尾是 \r\n 时,$ 认的是 \n 之前,那里是 \r
bool(re.search(r'e\Z', 'e\n'))  # False  ← \Z 只认绝对末尾

因此若字符串是 \r\n 结尾(Windows 文本文件常态),判断「e 是否在结尾」应写 e\r?$ 或直接用 \Z。
另需注意 Python 的 \Z 官方定义从 3.14 起多了一个别名:\z「Matches only at the end of the string. Added in version 3.14.」而「\Z The same as \z. For compatibility with old Python versions.」

补充一个 Python 独有的锚点:\A「Matches only at the start of the string」,它不受 re.M 影响(^ 在 re.M 下会匹配每行行首,\A 始终只匹配整个串的开头)。

3.6 捕获(分组)

代码 说明 举例
(pattern) 使用小括号指定一个子表达式,也叫分组。
捕获后会自动分配组号从 1 开始。
可以改变优先级。
匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在 VBScript 中使用 SubMatches 集合,在 JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 \( 或 \)。
\数字 匹配对应的分组 (very) \1 可以从 "vary very very vary vary" 中匹配到 'very very',但是捕获的组 group 是 very
(?:pattern) 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 " 或 " 字符 (|) 来组合一个模式的各个部分时很有用
如果仅仅为了改变优先级,就不需要捕获分组
(?:w|f)ood <==> wood|food
industr(?:y|ies) 就是一个比 industry|industries 更简略的表达式
(?<name>exp)
或
(?'name'exp)
命名分组捕获,可以通过 name 访问分组
Python 语法必须是 (?P<name>exp)

如果一个模式字符串中有用一对圆括号括起来的部分,那么这部分就会作为一组。当然,如果模式字符串中没有任何用圆括号括起来的部分,那么就不会对待匹配的字符串进行分组。

原文勘误:表格里的 \| 是 Markdown 转义,不是正则写法

上表「举例」列的 (?:w\|f)ood、industr(?:y\|ies),其中 \| 只是为了在 Markdown 表格中显示竖线。
正则里写 (?:w|f)ood 即可(| 在正则中本就是元字符);若真写成 \|,反而会去匹配字面的竖线字符。
实测:re.findall(r'(?:w|f)ood', 'wood took foot food') → ['wood', 'food'](正确写法);
re.findall(r'industr(?:y|ies)', 'industry industries') → ['industry', 'industries']。

(?:...)、(...)、(?P<name>...) 三者对比(实测)
写法 是否占组号 能否取回内容 能否反向引用
(...) ✅ 占(从 1 开始递增) ✅ group(n) / groups() ✅ \1
(?:...) ❌ 不占 ❌ ❌
(?P<name>...) ✅ 占(同时也有组号) ✅ group('name') / group(n) / groupdict() ✅ (?P=name) 或 \1

实测印证:「(?:f|w)ood 只改变优先级、不分组」——re.compile(r'(?:f|w)ood').groups 为 0,而 re.compile(r'(f|w)ood').groups 为 1。
嵌套分组的编号按左括号出现的先后顺序:re.compile(r'((f|w)(ood))') → groups 为 3,依次是整体、f|w、ood。

示例:

原始文本:

took food wood foot

正则表达式:

(f|w)ood

匹配结果:

Pasted-image-20250412160215_1

注意

一定是要先匹配上,然后才会在结果中进行分组。有一对括号就会分一组。


原始文本:

took food wood foot

正则表达式:

(?:f|w)ood

匹配结果:括号只为了改变优先级,不进行分组

Pasted-image-20250412161753_1


原始文本:

took food wood foot

正则表达式:

(?:f|wood)

匹配结果:

Pasted-image-20250412164703


原始文本:

took food wood foot

正则表达式:

((?:f|w)ood)

匹配结果:对 food 和 wood 进行分组

Pasted-image-20250412164506


原始文本:

took food wood foot

正则表达式:对 f 和 w 进行分组

(?:(f|w)ood)

匹配结果:

Pasted-image-20250412164828


原始文本:

took food wood foot

正则表达式:对 f 和 w 进行分组

(?:(f|w)(ood))

匹配结果:对 f、w 和 ood 进行分组

Pasted-image-20250412165018


原始文本:

took food wood foot

正则表达式:对 f 和 w 进行分组

((f|w)(ood))

匹配结果:对 food、wood、f、w 和 ood 进行分组

Pasted-image-20250412165202

上列分组示例的文本级复算(真实运行结果,未删减)
import re

t = 'took food wood foot'

re.findall(r'f|wood', t)           # ['f', 'wood', 'f']
re.findall(r'(f|w)ood', t)         # ['f', 'w']            ← 有分组时 findall 只返回分组内容
re.findall(r'(?:f|w)ood', t)       # ['food', 'wood']      ← 非捕获组,返回整体
re.findall(r'(?:f|wood)', t)       # ['f', 'wood', 'f']
re.findall(r'((?:f|w)ood)', t)     # ['food', 'wood']      ← 外层括号捕获整体
re.findall(r'(?:(f|w)ood)', t)     # ['f', 'w']            ← 内层括号捕获单个字母
re.findall(r'(?:(f|w)(ood))', t)   # [('f', 'ood'), ('w', 'ood')]
re.findall(r'((f|w)(ood))', t)     # [('food', 'f', 'ood'), ('wood', 'w', 'ood')]

一条极易踩的坑:findall 遇到分组会只返回分组内容(详见 §4.11)——所以 (f|w)ood 得到的是 ['f','w'] 而不是 ['food','wood']。
想让 findall 返回整体匹配、又不想让括号占组号,请写 (?:...) 或 ((?:...))。


原始文本:

very very vary

正则表达式:

v.*y

匹配结果:

Pasted-image-20250412165502


原始文本:

very very vary
sorry

正则表达式:

v.*y

匹配结果:

Pasted-image-20250412165543


原始文本:

so very very vary
sorry vys

正则表达式:

(v.+y)

匹配结果:

Pasted-image-20250412170524


原始文本:

so very very vary
sorry vys

正则表达式:

(v.+y) 

匹配结果:

Pasted-image-20250412170703


原始文本:

so very very vary sorry vys

正则表达式:

(v.+y) 

匹配结果:

Pasted-image-20250412171022


原始文本:

so very very vary sorry vys

正则表达式:\1 引用前面的分组。在匹配时 \1 会被替换为第一个匹配的分组中的字符串。

(v.+y) \1

匹配结果:

Pasted-image-20250412171444


原始文本:

so very very vary vary sorry vys

正则表达式:\1 引用前面的分组。在匹配时 \1 会被替换为第一个匹配的分组中的字符串。

(v.+y) \1

匹配结果:

Pasted-image-20250412172403

\1 反向引用:先捕获,再「原样要求重复一次」(实测)
re.findall(r'(very) \1', 'vary very very vary vary')  # ['very']
re.search(r'(very) \1', 'vary very very vary vary')
# <re.Match object; span=(5, 14), match='very very'>   ← 匹配到「very very」而非单个 very

语义:\1 要求该位置再次出现「第 1 组捕获到的同一段文本」。
注意官方限制:反向引用只能引用前 99 个组(\number「can only be used to match one of the first 99 groups」)。
命名组的反向引用写法是 (?P=name)(见 §3.7 表末),\1 同样可用。


原始文本:

so very very vary vary sorry vys

正则表达式:

(v).+(y)

匹配结果:

Pasted-image-20250412172740


原始文本:

so very very vary vary sorry vys

正则表达式:

(v).+(y)\b

匹配结果:

Pasted-image-20250412172934


原始文本:

so very very vary vary sorry vys

正则表达式:

(v).+(y) 

匹配结果:

Pasted-image-20250412173101


原始文本:

so very very vary vary sorry vys

正则表达式:

(v).+(y) \1\2

匹配结果:

Pasted-image-20250412173254


原始文本:

so very very vary vary sorry vys

正则表达式:注意方言不要选 Python。

(?<abc>v.+y)

匹配结果:

Pasted-image-20250412174554

原文勘误:(?<name>...) 在 Python 中会直接报错

上图是在 regex101 上切换到非 Python 方言(如 PCRE / JavaScript)跑出来的,原文也提示了「注意方言不要选 Python」。
在真实的 Python 里,(?<abc>v.+y) 会抛异常:

re.compile(r'(?<abc>v.+y)')
# re.error: unknown extension ?<a at position 1
re.compile(r"(?'abc'v.+y)")
# re.error: unknown extension ?' at position 1
re.compile(r'(?P<abc>v.+y)')   # ✅ 这才是 Python 的写法

三处方言差异对照:

方言 命名分组写法
Python (?P<name>exp)(必须带 P)
.NET / PCRE / Java 7+ / JS(ES2018+) (?<name>exp)
.NET 旧式 / PCRE 旧式 / Oniguruma (?'name'exp)

3.7 断言

注意

断言就相当于 if 判断语句,满足条件才进行匹配。
断言条件不会出现在匹配结果中。

3.7.1 零宽断言

代码 说明 举例
(?=exp) 零宽度正预测先行断言
断言 exp 一定在匹配的右边出现,也就是说断言后面一定跟个 exp
f(?=oo) 'f' 后面一定有 'oo' 出现
f(?=oo) 可以匹配 "food" 和 "foot" 中的 'f'
(?<=exp) 零宽度正回顾后发断言
断言 exp 一定出现在匹配的左边出现,也就是说前面一定有个 exp 前缀
(?<=f)ood、(?<=t)ook 分别匹配 'ood'、'ook' 前一定有 f、t 出现
原文勘误:回顾断言(lookbehind)不能是变长

官方文档明确限制:(?<=...)「The contained pattern must only match strings of some fixed length, meaning that abc or a|b are allowed, but a* and a{3,4} are not.」
也就是说 (?<=f.*)oo 这类写法在 Python 中会报错(look-behind requires fixed-width pattern)。
而 Python 3.5 起支持定长分组引用(「Changed in version 3.5: Added support for group references of fixed length.」)。
更要注意:以 lookbehind 开头的 pattern 无法在字符串开头处匹配,官方建议此时用 search() 而不是 match():

re.search(r'(?<=abc)def', 'abcdef').group(0)  # 'def'
re.search(r'(?<=-)\w+', 'spam-egg').group(0)  # 'egg'

示例:

原始文本:

wood took foot food

正则表达式:

f(?=oo)

匹配结果:

Pasted-image-20250412181405


原始文本:

wood took foot food

正则表达式:

(?<=f)oo

匹配结果:

Pasted-image-20250412181438


原始文本:

wood took foot food

正则表达式:

(?<=f|w)oo

匹配结果:

Pasted-image-20250412181231

3.7.2 负向零宽断言

代码 说明 举例
(?!exp) 零宽度负预测先行断言
断言 exp 一定不会出现在右侧,也就是说断言后面一定不是 exp
\d{3}(?!\d) 匹配 3 位数字,断言 3 位数字后面一定不能是数字
foo(?!d) 'foo' 后面一定不是 'd'
(?<!exp) 零宽度负回顾后发断言
断言 exp 一定不能出现在左侧,也就是说断言前面一定不能是 exp
(?<!f)ood 'ood' 的左边一定不是 'f'
代码 说明 举例
(?#comment) 注释 f(?=oo)(?#这是注释)
注意
  • 问题:断言会不会捕获呢?也就是断言占不占分组号呢?
  • 答:断言不占分组号。断言如同条件,只是要求匹配必须满足断言的条件。

分组和捕获是同一个意思。

注意

使用正则表达式时,能用简单表达式,就不要复杂的表达式。

示例:

原始文本:

wood took foot food

正则表达式:

(?<!f|w)oo

匹配结果:

Pasted-image-20250412181202


原始文本:

wood took foot food feet

正则表达式:

f(?!oo)

匹配结果:

Pasted-image-20250412182108


原始文本:

wood took foot food feet

正则表达式:这种注释方式并不好,不推荐使用

f(?!oo)(?#这是注释)

匹配结果:

Pasted-image-20250412182707

断言三例的文本级复算(真实运行结果,未删减)
import re

t = 'wood took foot food'
re.findall(r'f(?=oo)', t)     # ['f', 'f']      ← 只吃掉 f,oo 不进结果
re.findall(r'(?<=f)oo', t)    # ['oo', 'oo']    ← 只吃掉 oo,f 不进结果
re.findall(r'(?<=f|w)oo', t)  # ['oo', 'oo', 'oo']
re.findall(r'(?<!f|w)oo', t)  # ['oo']          ← took 里的 oo

t2 = 'wood took foot food feet'
re.findall(r'f(?!oo)', t2)             # ['f', 'f', 'f']   ← feet 的 f 也算
re.findall(r'f(?!oo)(?#这是注释)', t2)  # ['f', 'f', 'f']   ← 注释对匹配无影响

编译期核对「断言不占组号」:re.compile(r'f(?=oo)').groups → 0;re.compile(r'(?<=f)oo').groups → 0。
这就是「零宽」的含义——断言只检查条件,不消耗字符、也不进入结果。

3.8 贪婪与非贪婪

贪婪模式指限定符默认是贪婪的,总是尝试匹配尽可能多的字符;非贪婪模式则相反,总是尝试匹配尽可能少的字符。在 *,?,+,{m,n} 后面加上 ?,即可把贪婪变成非贪婪。

默认就是贪婪模式,也就是尽量多匹配更长的字符串。

非贪婪很简单:在重复的符号后面加上一个 问号(?),就尽量的少匹配了

代码 说明 举例
*? 匹配任意次,但尽可能少重复
+? 匹配至少 1 次,但尽可能少重复
?? 匹配 0 次或 1 次,但尽可能少重复
{n,}? 匹配至少 n 次,但尽可能少重复
{n,m}? 匹配至少 n 次,至多 m 次,但尽可能少重复
原文勘误:术语与衍字(已直接修正)
  • 「贪婪模式指数量词默认是贪婪的」→ 应为「限定符」(本节标题即「重复(限定符)」,* + ? {n,m} 统称限定符/量词,原文用词不一致)。
  • 「在重复的符号是后面加上一个」→ 衍字「是」,应为「在重复的符号后面加上一个」。

示例:

原始文本:

so very very vary sorry vys

正则表达式:默认为贪婪模式

v.*y

匹配结果:

Pasted-image-20250412183538


原始文本:

so very very vary sorry vys

正则表达式:非贪婪模式

v.*?y

匹配结果:

Pasted-image-20250412183714


原始文本:

so very very vary sorry vys

正则表达式:非贪婪模式

v.+?y

匹配结果:

Pasted-image-20250412183944


原始文本:

so very very vary sorry vys

正则表达式:非贪婪模式

v.??y

匹配结果:

Pasted-image-20250412184048


原始文本:

so very very vary sorry vys vay

正则表达式:非贪婪模式

v.??y

匹配结果:

四种写法逐一对拍(真实运行结果,未删减)
import re

s = 'so very very vary sorry vys'
re.findall(r'v.*y',  s)  # ['very very vary sorry vy']
re.findall(r'v.*?y', s)  # ['very', 'very', 'vary', 'vy']
re.findall(r'v.+?y', s)  # ['very', 'very', 'vary']
re.findall(r'v.??y', s)  # ['vy']

re.findall(r'v.??y', 'so very very vary sorry vys vay')  # ['vy', 'vay']

四条读法(同一段文本,四种数量词):

写法 含义 结果特征
v.*y * 贪婪:尽量长 一条包住全程
v.*?y *? 非贪婪:尽量短,* 可为 0 每遇到 y 就收口
v.+?y +? 非贪婪,+ 至少 1 与上行相同(本例用不上「0 个」)
v.??y ?? 非贪婪的 ?:优先取 0 个 变成「相邻 v…y」两字母匹配

易错点:v.+?y 在 'vy'(其实 v y 相邻)时匹配不上,因为 .+? 至少要 1 个字符;换 v.??y 就能匹配 'vy'。这也是为什么最后加了 'vay' 一例——v.??y 需要「v + 至多 1 个字符 + y」,所以 'vay' 能被命中。

3.9 引擎选项

代码 说明 Python
IgnoreCase 匹配时忽略大小写 re.I
re.IGNORECASE
Singleline 单行模式,. 可以匹配所有字符,包括换行符 \n re.S
re.DOTALL
Multiline 多行模式,将 \n 看做换行符 re.M
re.MULTILINE
IgnorePatternWhitespace 忽略表达式中的空白字符,如果要使用空白字符,用转义字符,# 可以用来做注释
不推荐使用
re.X
re.VERBOSE
  • 默认模式:

    可以看做待匹配的文本是一行,不能看做多行,. 点号不能匹配换行符,^ 和 $ 表示行首和行尾,而行首行尾就是整个字符串的开头和结尾。

  • 单行模式:

    基本和默认模式一样,只是 . 点号终于可以匹配任意一个字符包括换行符,这时所有文本就是一个长长的只有一行的字符串。^ 就是这一行字符串的行首,$ 就是这一行的行尾。

    . 可以匹配所有字符,包括换行符。

    ^ 表示整个字符串的开头,$ 表示整个字符串的结尾。

  • 多行模式:

    重新定义了行的概念,但不影响 . 点号的行为,^ 和 $ 还是行首行尾的意思,只不过因为多行模式可以识别换行符了。

    「结束」指的是 \n 前的字符,注意最后一行结尾可以没有 \n

    . 可以匹配除了换行符之外的字符,多行不影响 . 点号

    ^ 表示行首,$ 行尾,只不过这里的行是每一个行

总结

简单讲,单行模式只影响 . 点号行为,多行模式重新定义行影响了 ^ 和 $

注意

注意字符串中看不见的换行符,\r\n 会影响 e$ 的测试,e$ 只能匹配 e\n 。如果要判断 e 是否为结尾,可以使用 e\r?$

举例

very very happy
my primary key

上面 2 行 'y' 之后,有可能是 \r\n 结尾,此时认为结尾字符是 \r 。

y$ 单行匹配 "key" 的 'y' ,多行匹配 "happy" 和 "key" 的 'y' 。

.$ 指的是此行的结尾,而默认模式和单行模式都是一行,指的是这个大字符串的最后一个字符,就是 "key" 的 'y' 。

补充:re.X 的两个例外,以及 re.L 不能用于 str
  • re.X(VERBOSE)忽略空白,但有两个例外(官方原文):「Whitespace within the pattern is ignored, except when in a character class, or when preceded by an unescaped backslash, or within tokens like *?, (?: or (?P<...>.」
    即:字符类里的空白是保留的——re.findall(r'[a b]+', 'a b', re.X) → ['a b']。
  • re.L(LOCALE)只对 bytes 生效:re.match(r'\w', 'abc', re.L) → ValueError: cannot use LOCALE flag with a str pattern;
    必须写 re.match(rb'\w', b'abc', re.L) 才合法(官方:「Changed in version 3.6: LOCALE can be used only with bytes patterns」)。
  • re.U(UNICODE)在 Python 3 是冗余的:「In Python 3, Unicode characters are matched by default for str patterns. This flag is therefore redundant with no effect and is only kept for backward compatibility.」
    也就是说 str 模式本就按 Unicode 匹配,re.U 加不加都一样(re.findall(r'\w', 'abc', re.U) 不报错)。
各模式逐一对拍(真实运行结果,未删减)
import re

t = 'so very very sorry\nvys ray test'

re.findall(r'v.*y', t)               # ['very very sorry', 'vys ray']   ← 默认:. 不吃换行,逐行各匹配一段
re.findall(r'v.*y', t, re.S)         # ['very very sorry\nvys ray test']← 单行:. 吃换行,一条包住全程
re.findall(r'v.*y', t, re.M)         # ['very very sorry', 'vys ray']   ← 多行:不影响 . 的行为
re.findall(r'v.*y', t, re.S | re.M)  # ['very very sorry\nvys ray test']

re.findall(r'^v.*y', t)              # []            ← 默认:^ 只认整个串开头,而首字符是 s
re.findall(r'^v.*y', t, re.M)        # ['vys ray']   ← 多行:^ 认每行行首

一句话记忆:单行模式(re.S)改的是 .(让它能吃 \n);多行模式(re.M)改的是 ^ / $(让它们认每行行首行尾)。两者互不干扰,可同时用。

示例:

原始文本:

so very very vary
sorry vys
ray

正则表达式:

v.*y

匹配结果:

Pasted-image-20250412185059


原始文本:

so very very sorry
vys ray

正则表达式:

v.*y

匹配结果:

Pasted-image-20250412185247


原始文本:

so very very sorry
vys ray

正则表达式:单行模式

v.*y

匹配结果:

Pasted-image-20250412185930


原始文本:

so very very sorry
vys ray

正则表达式:单行模式

^v.*y

匹配结果:

Pasted-image-20250412185849


原始文本:

so very very sorry
vys ray

正则表达式:默认模式

^v.*y

匹配结果:

Pasted-image-20250412190206


原始文本:

so very very sorry
vys ray test

正则表达式:多行模式

v.*y

匹配结果:

Pasted-image-20250412190829


原始文本:

so very very sorry
vys ray test

正则表达式:多行模式

^v.*y

匹配结果:

Pasted-image-20250412191004


原始文本:

so very very sorry
vys ray test

正则表达式:多行模式 + 单行模式

^v.*y

匹配结果:

Pasted-image-20250412191229


原始文本:

so very very sorry
vys ray test

正则表达式:多行模式 + 单行模式

v.*y

匹配结果:

Pasted-image-20250412191445


原始文本:

so very very sorry
vys ray test

正则表达式:单行模式

v.*y$

匹配结果:

Pasted-image-20250412191747


原始文本:

so very very sorry
vys ray test
tiny

正则表达式:单行模式

v.*y$

匹配结果:

Pasted-image-20250412191908


原始文本:

so very very sorry
vys ray test
tiny

正则表达式:单行模式

v.*y$

匹配结果:

原文勘误:此处原文「匹配结果:」之后内容缺失(无截图、无输出),疑为漏截。

按上下文(文本三行 + 正则 v.*y$ + 单行模式)实测补全(Python 3.13.14):

t = 'so very very sorry\nvys ray test\ntiny'
re.findall(r'v.*y$', t, re.S)  # ['very very sorry\nvys ray test\ntiny']
re.findall(r'v.*y$', t)        # []   ← 默认模式下 . 不吃换行,$ 只认整串末尾(那里是 'tiny')
re.findall(r'v.*y$', t, re.M)  # ['very very sorry']

单行模式下 $ 是整串末尾(re.S 不改 $ 语义,$ 本来就认整串末尾),所以 v.*y$ 要一路吃到最后一个 y——最后一行 tiny 结尾的 y 才是目标,于是整段被吃下。

4 Python 的正则表达式

Python 通过 re 模块提供正则表达式处理能力。

本节统一约定

下面所有示例共用同一个源字符串:

s = """bottle\nbag\nbig\napple"""

它的索引与字符对照(后面所有 pos/endpos 解释都基于这张表):

索引 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
字符 b o t t l e \n b a g \n b i g \n a p p l e

记住三个换行位置:6、10、14;三个 b:0、7、11;两个 a:8、15。

4.1 常量

常量 说明
re.M
re.MULTILINE
多行模式
re.S
re.DOTALL
单行模式
re.I
re.IGNORECASE
忽略大小写
re.X
re.VERBOSE
忽略表达式中的空白字符

使用 |(位或) 运算连接多个模式选择

常量是 RegexFlag 枚举,可以位或组合(实测)
import re
re.S | re.M            # re.MULTILINE|re.DOTALL
type(re.S)             # <class 're.RegexFlag'>
isinstance(re.S, re.RegexFlag)  # True
int(re.S)              # 16
int(re.I | re.M | re.S)  # 26

官方记载:「Changed in version 3.6: Flag constants are now instances of RegexFlag, which is a subclass of enum.IntFlag.」
所以 re.I | re.M | re.S 是合法组合,等价于同时打开三个选项。

完整常量对照(re 模块文档):

短名 长名 作用
re.I re.IGNORECASE 忽略大小写
re.M re.MULTILINE ^/$ 认每行行首行尾
re.S re.DOTALL . 可匹配换行符
re.X re.VERBOSE 忽略空白、# 起注释
re.A re.ASCII \w \d \s \b 退化为 ASCII 语义
re.L re.LOCALE 本地化(仅 bytes 可用)
re.U re.UNICODE 在 Python 3 中冗余(str 默认即 Unicode)
re.DEBUG — 打印编译出的字节码,调试用

4.2 编译——compile

re.compile(pattern, flags=0)

设定 flags,编译模式,返回正则表达式对象 regex 。

pattern 是正则表达式字符串,flags 是引擎选项。正则表达式需要先编译;为提高效率,编译结果会被保存,下次遇到同样的 pattern 时就无需再次编译。

Pasted-image-20250412211318

re 的其它方法为了提高效率都调用了编译方法,就是为了提速。

实测:同一个 pattern 只编译一次(对象级缓存)
import re
re.compile('a1b') is re.compile('a1b')            # True  ← 返回同一个对象
re.compile('a1b', re.I) is re.compile('a1b', re.I)  # True(flags 参与缓存键)
re._MAXCACHE   # 512  ← 模块级缓存最多保留 512 个 pattern

编译结果的类型是 re.Pattern,不是 re.RegexObject(旧名):

type(re.compile('a'))                 # <class 're.Pattern'>
isinstance(re.compile('a'), re.Pattern)  # True

re.purge() 可以清空该缓存。日常不必手动 compile(方法内部自带缓存),但循环里反复用同一个正则时显式 compile 更清晰、也避免缓存被挤掉。

4.3 单次匹配——match

re.match(pattern, string, flags=0)
regex.match(string[ , pos[, endpos]])
参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。
原文勘误:上表 re.L / re.U 的说明在 Python 3 下会误导
  • re.L(LOCALE)只能用于 bytes pattern。str 模式下直接报错:
    re.match(r'\w', 'abc', re.L)
    # ValueError: cannot use LOCALE flag with a str pattern
    re.match(rb'\w', b'abc', re.L)   # ✅ 合法
    官方:「Changed in version 3.6: LOCALE can be used only with bytes patterns and is not compatible with ASCII.」
  • re.U(UNICODE)在 Python 3 中是空操作。官方:「In Python 3, Unicode characters are matched by default for str patterns. This flag is therefore redundant with no effect and is only kept for backward compatibility.」
    真正想「退化为 ASCII 语义」应使用 re.A / re.ASCII(原文表格未列出该标志,是本篇补充项)。
  • 「根据 Unicode 字符集解析字符」这句描述的是 Python 2 时代的语境;Python 3 的 str 本就是 Unicode。
  • re.match 是用来进行正则匹配检查的方法,如果字符串开头的 0 个或多个字符匹配正则表达式模式,则返回相应的 match 对象。如果字符串不匹配模式,返回 None(注意不是空字符串 "")
  • 匹配对象 Match Object 具有 group() 方法,用来返回字符串的匹配部分。具有 span() 方法,返回匹配字符串的位置(元组存储开始,结束位置)。具有 start(),end() 方法,存储匹配数据的开始和结束位置。(也可以通过对象的 dir(对象查看对象的方法))
注意

如果想在目标字符串的任意位置查找,需要使用 search。

re.match 匹配必须从字符串的开头进行匹配。匹配成功返回 Match 实例对象。

Pasted-image-20250412215155

  • 匹配不上,返回 None
  • 匹配上返回 Match 类型实例 res
    Pasted-image-20250412212808
    • span (start, end),前包后不包
    • match=xxx 匹配的结果
    • res.start() 匹配结果的开始索引
    • res.end() 匹配结果的结束索引
    • res.string 源字符串,即在哪个字符串中匹配的

regex 对象 match 方法可以重新设定匹配的开始位置和结束位置。返回 Match 实例对象。

Pasted-image-20250412215943

  • 返回值同 re.match 一样
补充:Match 对象的完整接口(实测 dir())
['end', 'endpos', 'expand', 'group', 'groupdict', 'groups',
 'lastgroup', 'lastindex', 'pos', 're', 'regs', 'span', 'start', 'string']

六个位置属性常被忽略:pos / endpos(本次匹配的搜索区间)、re(所用的 Pattern 对象)、
string(源字符串)、lastindex / lastgroup(最后一个参与匹配的组号/组名)。

示例:re.match 的使用

import re

s = """bottle\nbag\nbig\napple"""

# re.match 方法
# 匹配不上,返回 None
res = re.match('quit', s, re.S)
print(type(res))
print(res)
print("=" * 20)

# 匹配上返回 Match 类型实例,span (start, end),match=xxx 匹配结果
res = re.match('b.+', s, re.S)
print(type(res))
print(res)
# 得到匹配结果的索引值
print(f"匹配结果的开始索引:{res.start()}")
print(f"匹配结果的结束索引:{res.end()}")
# 得到匹配结果
print(f"匹配结果:{s[res.start(): res.end()]}")
print("=" * 20)

Pasted-image-20250412214030

原文勘误:此处原文把赋值语句吞进了注释

原文写的是 # 匹配不上,返回 Noneres = re.match('quit', s, re.S) —— 注释与代码挤在同一行,导致 res 未定义,
下一行 print(type(res)) 会直接抛 NameError。已拆成两行(注释 + 赋值)。
同类问题在 §4.11 还有 4 处,一并修正。

真实运行结果:

<class 'NoneType'>
None
====================
<class 're.Match'>
<re.Match object; span=(0, 20), match='bottle\nbag\nbig\napple'>
匹配结果的开始索引:0
匹配结果的结束索引:20
匹配结果:bottle
bag
big
apple
====================

示例:re.match 多行模式

import re

s = """bottle\nbag\nbig\napple"""

# re.match 方法
# 匹配上返回 Match 类型实例,span (start, end),match=xxx 匹配
res = re.match('b.+', s, re.M)
print(type(res))
print(res)
# 得到匹配结果的索引值
print(f"匹配结果的开始索引:{res.start()}")
print(f"匹配结果的结束索引:{res.end()}")
# 得到匹配结果
print(f"匹配结果:{s[res.start(): res.end()]}")
print("=" * 20)

Pasted-image-20250412214209

真实运行结果:

<class 're.Match'>
<re.Match object; span=(0, 6), match='bottle'>
匹配结果的开始索引:0
匹配结果的结束索引:6
匹配结果:bottle
====================
对照上面两个例子:re.M 不影响 . 的行为

同一句 re.match('b.+', s),加了 re.M 后匹配结果从 span=(0, 20)(吃掉换行)缩成 span=(0, 6)(只到第一个 \n)。
原因是 re.S 让 . 能吃 \n,而 re.M 不改 .,.+ 到行尾就停。
这就是 §3.9 那句「单行模式只影响 .,多行模式只影响 ^/$」的实证。

示例:regex 对象的 match 方法

import re

s = """bottle\nbag\nbig\napple"""

regex = re.compile('b.+', re.M)
print(f"regex 对象类型:{type(regex)}")
print(f"regex:{regex}")
print("=" * 20)
# 匹配上返回 Match 类型实例,span (start, end),match=xxx 匹配结果
res = regex.match(s, 7)
# 源字符串
print(f"源字符串:{res.string}")
print(f"res 的类型:{type(res)}")
print(f"res:{res}")

Pasted-image-20250412221746

真实运行结果:

regex 对象类型:<class 're.Pattern'>
regex:re.compile('b.+', re.MULTILINE)
====================
源字符串:bottle
bag
big
apple
res 的类型:<class 're.Match'>
res:<re.Match object; span=(7, 10), match='bag'>

4.3.1 match 方法匹配方式详解

先看以下的一个案例:四次匹配分别返回什么?

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

res = re.match('a', s)
print(res)
res = re.match('^a', s)
print(res)
res = re.match('a', s, re.M)
print(res)
res = re.match('^a', s, re.M)
print(res)

结果:

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

# match 是单次匹配,并不是全文搜索,而且要求必须是从头匹配,从索引 0 开始
res = re.match('a', s)
print(res)  # None
res = re.match('^a', s)
print(res)  # None 默认模式,一整行,没有 a 开头。
res = re.match('a', s, re.M)
print(res)  # None
res = re.match('^a', s, re.M)
print(res)  # None

Pasted-image-20250412231100

分析:

match 是单次匹配,并不是全文搜索,而且要求必须是从头匹配,从索引 0 开始。

re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match() 就返回 None。

re.match('a', s) 和 re.match('a', s, re.M) 相当于匹配时判断字符串 s 的第一个字符是 'a' 吗?不是,直接返回 None,不向后匹配。'^a' 开头同理。

regex.match() 方法也是同理。但是 regex.match() 可以改变字符串匹配的起始位置。

为什么加了 re.M 也还是 None?(最易错的一处)

很多人以为 re.match('a', s, re.M) 能匹配到索引 8 的 'a'——错。
re.M 只改变 ^ / $ 的语义,不改变 match 的起跑线:re.match 永远只从 pos(默认 0)开始试。
索引 0 是 'b',不是 'a',直接 None。
想在中间位置开始匹配,只有编译后的对象能做到——regex.match(s, 8)(见下一个例子)。

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

regex = re.compile('b')
print(regex.match(s))  # b 0

regex = re.compile('^b')
print(regex.match(s))  # b 0

regex = re.compile('^a')
# 字符串 s 是以 'a' 开头的吗?
print(regex.match(s))  # None

regex = re.compile('^a', re.M)
# match 方法从字符串开始位置进行匹配,如果第一个字符不匹配,直接返回 None。
# 0 索引不是 'a' ,直接返回 None,不向后进行匹配。
print(regex.match(s))  # None

regex = re.compile('^a', re.M)
# 虽然是多行模式,也是从索引 8 进行匹配,但是索引 8 的 'a' 不是开头
print(regex.match(s, 8))  # None

regex = re.compile('a', re.M)
print(regex.match(s, 8))  # a 8

regex = re.compile('^a', re.M)
# 从索引 15 进行匹配,索引 15 的 'a' 是开头
print(regex.match(s, 15))  # a 15

regex = re.compile('^a')
# 默认模式,行首和行尾就是字符串的开头和结尾
print(regex.match(s, 15))  # a 15

Pasted-image-20250418221941

上面两段的真实运行结果(未删减)
re.compile("b").match(s)            # <re.Match object; span=(0, 1), match='b'>
re.compile("^b").match(s)           # <re.Match object; span=(0, 1), match='b'>
re.compile("^a").match(s)           # None
re.compile("^a", re.M).match(s)     # None
re.compile("^a", re.M).match(s, 8)  # None    ← 索引 8 的 'a' 不是「行首」
re.compile("a", re.M).match(s, 8)   # <re.Match object; span=(8, 9), match='a'>
re.compile("^a", re.M).match(s, 15) # <re.Match object; span=(15, 16), match='a'>
re.compile("^a").match(s, 15)       # None    ← 默认模式下行首只认索引 0

最后两行的对照是本节的精髓:
同样是 regex.match(s, 15),^a 在多行模式下能匹配(索引 15 是第 4 行的行首),在默认模式下不能匹配(行首只认索引 0)。
结论:pos 改变的是「从哪里开始扫」,^ 是否认这里为行首取决于 re.M,两者是独立的两件事。

4.4 单次匹配——search

search 方法在一个字符串中搜索满足文本模式的字符串。语法格式如下:

re.search(pattern, string, flags=0)
regex.search(string[ , pos[, endpos]])

函数参数与 match 方法类似

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

re.search 匹配在整个字符串中进行匹配。匹配成功返回 Match 实例对象;匹配失败返回 None。

regex 对象 search 方法可以重新设定匹配的开始位置和结束位置。匹配成功返回 Match 实例对象;匹配失败返回 None。

4.4.1 match 与 search 的区别

re.match 只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回 None;而 re.search 匹配整个字符串,直到找到一个匹配。

一句话对比(实测)
re.match(r'a', 'bag')   # None  ← 只看开头
re.search(r'a', 'bag')  # <re.Match object; span=(1, 2), match='a'>  ← 全文扫描

match ≈ 隐式加了 \A 锚点的 search;search 会一直向右找,直到找到第一个匹配(找到就停,不求最长)。

4.4.2 search 方法示例

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

res = re.search('b', s)
print(type(res))
print(res)  # b 0 返回一个 Match 示例

res = re.match('a', s)
print(res)  # None

res = re.search('a', s)
print(res)  # a 8

res = re.search('a', s, re.M)
# 多行模式只改变 ^ 和 $ 的行为
print(res)  # a 8

res = re.search('^a', s, re.M)
print(res)  # a 15

res = re.search('^a', s)
print(res)  # None

Pasted-image-20250418224816

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

regex = re.compile('^a')
# 默认模式的行首和行尾就是字符串的开头和结尾
print(regex.search(s))  # None

regex = re.compile('^a')
# 默认模式的行首和行尾就是字符串的开头和结尾
print(regex.search(s, 15))  # None

regex = re.compile('^a', re.M)
print(regex.search(s, 15))  # a 15

Pasted-image-20250418225603

上面两段的真实运行结果(未删减)
re.search("b", s)        # <re.Match object; span=(0, 1), match='b'>
re.match("a", s)         # None
re.search("a", s)        # <re.Match object; span=(8, 9), match='a'>
re.search("a", s, re.M)  # <re.Match object; span=(8, 9), match='a'>  ← re.M 不影响它
re.search("^a", s, re.M) # <re.Match object; span=(15, 16), match='a'>
re.search("^a", s)       # None

re.compile("^a").search(s)        # None
re.compile("^a").search(s, 15)    # None  ← search 的 pos 也不是「行首」
re.compile("^a", re.M).search(s, 15)  # <re.Match object; span=(15, 16), match='a'>

注意 re.search('^a', s) 与 regex.search(s, 15) 都返回 None:
pos 只挪动扫描起点,^ 能否成立仍由 re.M 决定——和 §4.3 的结论完全一致。

4.5 单次匹配——fullmatch

fullmatch 是单次匹配,要求指定区间全部匹配。语法格式如下:

re.fullmatch(pattern, string, flags=0)
regex.fullmatch(string[ , pos[, endpos]])
原文勘误:本节语法块抄成了 search

原文语法块写的是:

re.search(pattern, string, flags=0)
regex.search(string[ , pos[, endpos]])

这是从 §4.4 复制后忘记改名。按官方文档(inspect.signature 实测)应为:

re.fullmatch(pattern, string, flags=0)                 # 模块级:无 pos/endpos
regex.fullmatch(string[ , pos[, endpos]])               # 对象级:有 pos/endpos
import inspect, re
print(inspect.signature(re.fullmatch))          # (pattern, string, flags=0)
print(inspect.signature(re.compile('a').fullmatch))
# (string, pos=0, endpos=9223372036854775807)

函数参数与 match 方法类似

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

re.fullmatch 在字符串的指定区间中进行匹配。匹配成功返回 Match 实例对象;匹配失败返回 None。

Pasted-image-20250418224359_1

regex 对象 fullmatch 方法可以重新设定匹配的开始位置和结束位置。匹配成功返回 Match 实例对象;匹配失败返回 None。

原文勘误:fullmatch 的语义描述不准确

原文写「re.fullmatch 在字符串的指定区间中进行匹配」——这是 regex.fullmatch(s, pos, endpos) 的行为。
对模块级 re.fullmatch 而言,官方定义是「checks for entire string to be a match」:
整个字符串必须完全匹配,一个字符不多、一个字符不少。

re.fullmatch('b.+', s)        # None  ← 因为没有覆盖整串
re.fullmatch('bag', s)        # None
re.fullmatch('b.+', s, re.S)  # <re.Match object; span=(0, 20), match='bottle\nbag\nbig\napple'>  ← 全串匹配

注意 fullmatch 同样从 pos 处开始(不是「随便找一段」),且要求从 pos 到 endpos 恰好整段匹配。

4.5.1 fullmatch 方法示例

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

res = re.fullmatch('b.+', s)
print(type(res), res)

res = re.fullmatch('bag', s)
print(type(res), res)

res = re.fullmatch('b.+', s, re.S)
print(type(res), res)  # 0-20 全匹配

regex = re.compile('bag')
print(regex.fullmatch(s, 7))  # None

regex = re.compile('bag')
print(regex.fullmatch(s, 7, 11))  # None

regex = re.compile('bag')
# 区间完全匹配。前包后不包
print(regex.fullmatch(s, 7, 10))  # 7-10 bag

regex = re.compile('bag')
# 前包后不包
print(regex.fullmatch(s, 7, 9))  # None

regex = re.compile('^bag')
# 默认模式行首和行尾就是字符串的开头和结尾
print(regex.fullmatch(s, 7, 10))  # None

regex = re.compile('^bag', re.M)
# 默认模式行首和行尾就是字符串的开头和结尾
print(regex.fullmatch(s, 7, 10))  # 7-10 bag

Pasted-image-20250418231356

真实运行结果(未删减)
re.fullmatch("b.+", s)        # None
re.fullmatch("bag", s)        # None
re.fullmatch("b.+", s, re.S)  # <re.Match object; span=(0, 20), match='bottle\nbag\nbig\napple'>
re.compile("bag").fullmatch(s, 7)     # None   ← 区间 [7, 20) 不是 "bag"
re.compile("bag").fullmatch(s, 7, 11) # None   ← 区间 [7, 11) 是 "bag\n" 而非 "bag"
re.compile("bag").fullmatch(s, 7, 10) # <re.Match object; span=(7, 10), match='bag'>
re.compile("bag").fullmatch(s, 7, 9)  # None   ← 区间 [7, 9) 是 "ba"
re.compile("^bag").fullmatch(s, 7, 10)      # None  ← 默认模式 ^ 只认整串开头
re.compile("^bag", re.M).fullmatch(s, 7, 10) # <re.Match object; span=(7, 10), match='bag'>
fullmatch vs match vs search 三兄弟(一张表说清)
方法 起点 终点要求 一句话
match 必须等于 pos 只要前缀匹配即可 「以…开头」
search 任意位置 只要任意片段匹配即可 「包含」
fullmatch 必须等于 pos 必须恰好用完 [pos, endpos) 「整段就是」

三者都只返回第一个匹配(单次匹配);要找全部用 §4.6 / §4.7 的 findall / finditer。

4.6 全文搜索——findall

findall 是全文搜索,在字符串中找到正则表达式所匹配的所有子串。并把它们作为一个列表返回。语法格式如下:

re.findall(pattern, string, flags=0)
regex.findall(string[ , pos[, endpos]])

函数参数与 match 方法类似

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

re.findall 对整个字符串,从左至右匹配。匹配成功返回所有匹配项的列表;匹配失败返回空列表 []。

Pasted-image-20250418233024

regex 对象 findall 方法可以重新设定匹配的开始位置和结束位置。匹配成功返回所有匹配项的列表;匹配失败返回空列表 []。

4.6.1 findall 方法示例

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

res = re.findall('b', s)
print(type(res), res)  # <class 'list'> ['b', 'b', 'b']

res = re.findall(r'b\w+', s)
print(type(res), res)  # <class 'list'> ['bottle', 'bag', 'big']

regex = re.compile(r'b\w+')
print(regex.findall(s))  # ['bottle', 'bag', 'big']

regex = re.compile(r'b\w+')
print(regex.findall(s, 1))  # ['bag', 'big']

regex = re.compile(r'^b\w+')
# 默认模式行首和行尾就是字符串的开头和结尾
print(regex.findall(s))  # ['bottle']

regex = re.compile(r'^b\w+')
print(regex.findall(s, 1))  # []

regex = re.compile(r'^b\w+', re.M)
print(regex.findall(s))  # ['bottle', 'bag', 'big']

真实运行结果(未删减)
re.findall("b", s)          # ['b', 'b', 'b']
re.findall(r"b\w+", s)      # ['bottle', 'bag', 'big']
re.compile(r"b\w+").findall(s)      # ['bottle', 'bag', 'big']
re.compile(r"b\w+").findall(s, 1)   # ['bag', 'big']    ← pos=1 跳过索引 0 的 b
re.compile(r"^b\w+").findall(s)     # ['bottle']
re.compile(r"^b\w+").findall(s, 1)  # []                ← pos=1 后 ^ 不成立
re.compile(r"^b\w+", re.M).findall(s)  # ['bottle', 'bag', 'big']
注意:findall 的结果会因「有没有分组」而变脸

这是 findall 最反直觉的地方,也是 §4.11 要专门讲的坑:

pattern 里的分组数 findall 返回
0 个 字符串列表,元素是整体匹配
1 个 字符串列表,元素是那 1 个分组的内容
≥2 个 元组列表,每个元组是各分组的内容

所以 re.findall(r'(f|w)ood', 'took food wood foot') 得到的是 ['f', 'w'] 而不是 ['food','wood']。
只想改优先级不想捕获,务必用 (?:...)。

4.7 全文搜索——finditer

finditer 在字符串中找到正则表达式所匹配的所有子串,并把它们作为一个迭代器返回。语法格式如下:

re.finditer(pattern, string, flags=0)
regex.finditer(string[ , pos[, endpos]])

函数参数与 match 方法类似

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

re.finditer 对整个字符串,从左至右匹配,返回所有匹配项。匹配成功返回迭代器;匹配失败返回一个空迭代器。

Pasted-image-20250418234459

注意

每次迭代返回的是 match 对象。

regex 对象 finditer 方法可以重新设定匹配的开始位置和结束位置。匹配成功返回迭代器;匹配失败返回一个空迭代器。

4.7.1 finditer 方法示例

import re

s = """bottle\nbag\nbig\napple"""

for i, c in enumerate(s, 1):
    print((i - 1, c), end="\n" if i % 10 == 0 else " ")

res = re.finditer(r'b\w+', s)
print(type(res), res)
for i in res:
    print(type(i), i)

print("=" * 40)

regex = re.compile(r'^b')
res = regex.finditer(s)
print(type(res), res)
for i in res:
    print(type(i), i)

print("=" * 40)

regex = re.compile(r'^b', re.M)
for i in regex.finditer(s):
    print(i)

真实运行结果(未删减)
<class 'callable_iterator'> <callable_iterator object at 0x...>
<class 're.Match'> <re.Match object; span=(0, 6), match='bottle'>
<class 're.Match'> <re.Match object; span=(7, 10), match='bag'>
<class 're.Match'> <re.Match object; span=(11, 14), match='big'>
========================================
<class 'callable_iterator'> <callable_iterator object at 0x...>
<class 're.Match'> <re.Match object; span=(0, 1), match='b'>
========================================
<re.Match object; span=(0, 1), match='b'>
<re.Match object; span=(7, 8), match='b'>
<re.Match object; span=(11, 12), match='b'>

finditer 返回的类型是 callable_iterator(不是 list),所以只能遍历一次;想多次复用要先 list(...)。

findall 与 finditer 怎么选?
findall finditer
返回 list 迭代器(惰性)
元素 字符串 / 元组(受分组影响) Match 对象
能否拿到位置信息 ❌ ✅ m.span() / m.start() / m.groupdict()
内存 一次性生成全部 逐个产出,大文本更省内存

口诀:只要结果用 findall,要位置或要分组详情用 finditer。

4.8 匹配替换——sub

sub 在字符串中找到正则表达式所匹配的所有子串,并把它们替换为指定的内容,返回一个替换后的字符串。语法格式如下:

re.sub(pattern, replacement, string, count=0, flags=0)
regex.sub(replacement, string, count=0)

函数参数

参数 描述
pattern 匹配的正则表达式
replacement 用于替换的文本
string 要匹配的字符串
count 至多替换的次数,count=1 至多替换一次。默认为 0,不限制次数。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

re.sub 使用 pattern 对字符串 string 进行匹配,对匹配项使用 repl 替换,返回一个替换后的字符串。

replacement 可以是 string、bytes、function。

4.8.1 sub 方法示例

import re

s = """bottle\nbag\nbig\napple"""

res = re.sub(r'b\w+', 'xyz', s)
print(type(res), res.encode())

# 指定至多替换次数
res = re.sub(r'b\w+', 'xyz', s, count=1)
print(type(res), res.encode())

regex = re.compile(r'b\w+')
res = regex.sub('xyz', s)
print(type(res), res.encode())

regex = re.compile(r'b\w+')
res = regex.sub('xyz', s, count=1)
print(type(res), res.encode())

Pasted-image-20250419163159

真实运行结果(未删减)
<class 'str'> b'xyz\nxyz\nxyz\napple'
<class 'str'> b'xyz\nbag\nbig\napple'
<class 'str'> b'xyz\nxyz\nxyz\napple'
<class 'str'> b'xyz\nbag\nbig\napple'

bytes 形式输出是为了让 \n 可见(print 会把换行真的换行)。

replacement 传函数时,还能吃到「反向引用」(实测)

repl 是个可调用对象时,会拿到每个 Match,返回值即替换文本:

re.sub(r'b\w+', lambda m: m.group().upper(), s)
# 'BOTTLE\nBAG\nBIG\napple'

repl 是字符串时,里面的 \1、\g<name> 会被解释成分组引用(不是 Python 字符串转义):

re.sub(r'(\w+)@(\w+)', r'\2.\1', 'user@host')                    # 'host.user'
re.sub(r'(?P<a>\w+)@(?P<b>\w+)', r'\g<b>.\g<a>', 'user@host')    # 'host.user'

也正因如此,普通字符串里想写一个真正的反斜杠必须写 \\\\;不想被解释请用 re.escape() 或函数形式。

官方还列了命名分组的第三种引用写法:在传给 re.sub() 的 repl 里可用 \g<quote> / \g<1> / \1(见 §3.6 命名分组三种引用语境)。

4.9 匹配替换——subn

subn 在字符串中找到正则表达式所匹配的所有子串,并把它们替换为指定的内容,返回一个二元组 (new_string, number_of_subs_made)。语法格式如下:

re.subn(pattern, replacement, string, count=0, flags=0)
regex.subn(replacement, string, count=0)
原文勘误:介绍文字把函数名写成了 finditer

原文本节首句是「finditer 在字符串中找到正则表达式所匹配的所有子串,并把它们替换为指定的内容」——
描述的是 subn,函数名误写为 finditer(明显是从上一节复制)。已改为 subn。

函数参数

参数 描述
pattern 匹配的正则表达式
replacement 用于替换的文本
string 要匹配的字符串
count 至多替换的次数,count=1 至多替换一次。默认为 0,不限制次数。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

re.subn 使用 pattern 对字符串 string 进行匹配,对匹配项使用 repl 替换。返回一个元组 (new_string, number_of_subs_made)。
可以直观的看到被替换的次数。

一般不用此方法,但是在排错等情况下很好用。

replacement 可以是 string、bytes、function。

4.9.1 subn 方法示例

import re

s = """bottle\nbag\nbig\napple"""

res = re.subn(r'b\w+', 'xyz', s)
print(type(res), res)

# 指定至多替换次数
res = re.subn(r'b\w+', 'xyz', s, count=1)
print(type(res), res)

regex = re.compile(r'b\w+')
res = regex.subn('xyz', s)
print(type(res), res)

regex = re.compile(r'b\w+')
res = regex.subn('xyz', s, count=1)
print(type(res), res)

真实运行结果(未删减)
<class 'tuple'> ('xyz\nxyz\nxyz\napple', 3)
<class 'tuple'> ('xyz\nbag\nbig\napple', 1)
<class 'tuple'> ('xyz\nxyz\nxyz\napple', 3)
<class 'tuple'> ('xyz\nbag\nbig\napple', 1)

subn = sub + 替换次数。返回值第 2 项就是命中的次数,用它调正则比 len(re.findall(...)) 更直接。

4.10 分割字符串——split

split 在字符串中根据指定的正则表达式对字符串进行分割。语法格式如下:

re.split(pattern, string, maxsplit=0, flags=0)
regex.split(string, maxsplit=0)
原文勘误:本节标题与语法块各有一处错
  1. 原文标题「分割字符串——spilt」、子标题「spilt 方法示例」→ 应为 split(spilt 是 spill 的过去式,不存在的 API)。
  2. 原文语法块写 regex.split(self, string, maxsplit=0) —— 多了 self(那是 CPython 源码里方法定义的样子,直接抄进了教程)。实测签名为:
    import inspect, re
    print(inspect.signature(re.split))              # (pattern, string, maxsplit=0, flags=0)
    print(inspect.signature(re.compile('a').split)) # (string, maxsplit=0)
  3. 本节首句原文也写作「finditer 在字符串中根据指定的正则表达式对字符串进行分割」——描述的是 split,函数名同样写错。

函数参数

参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串
maxsplit 至多分隔次数,maxsplit=1 至多分隔一次,默认为 0,不限制次数。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。如下表列出正则表达式修饰符——可选标志

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响 ^ 和 $
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w,\W,\b,\B。
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。

split 函数用于根据正则表达式分隔字符串,也就是说,将字符串与模式匹配的子字符串都作为分隔符来分隔这个字符串。split 函数返回一个列表形式的分隔结果,每一个列表元素都是分隔的子字符串。

两个易错点(实测)
  1. maxsplit 是「至多分割次数」,不是「最多返回几段」:
    re.split(r'\.', 'os.path.abspath')            # ['os', 'path', 'abspath']
    re.split(r'\.', 'os.path.abspath', maxsplit=1) # ['os', 'path.abspath']   ← 切 1 刀 → 2 段
  2. split 会「吃掉」分隔符,所以连续分隔符会产生空串;把分隔符写进 [...] 并加 + 可减少空串:
    re.split(r'\s', '\na b\n')          # ['', 'a', 'b', '']
    re.split(r'\s+', '\na b\n')         # ['', 'a', 'b', '']
  3. 若 pattern 中含捕获分组,分组内容也会出现在结果里(这是与 str.split 最大的差异):
    re.split(r'(\W+)', 'a,b')   # ['a', ',', 'b']   ← 分隔符也被保留
    不想保留请用非捕获组 (?:...)。
  4. 另注意:Python 3.13 起 count / maxsplit 位置传参会发 DeprecationWarning,请用关键字传:
    re.split(r'\.', 'os.path.abspath', 1)
    # DeprecationWarning: 'maxsplit' is passed as positional argument
    re.split(r'\.', 'os.path.abspath', maxsplit=1)   # ✅

4.10.1 split 方法示例

import re

s = """bottle\nbag\nbig\napple"""

# 字符串的 split 方法默认是 \s+ 为分隔符
print(s.split())
# 正则表达式进行分割
print(re.split(r'\s+', s))

s = """
os.path.abspath(path)
path. ([path)]
"""

print(re.split(r'\s', s))
print(re.split(r'[\s.\[\]{}()]', s))
# 可以减少空字符串的出现频率
print(re.split(r'[\s.\[\]{}()]+', s))

res = re.split(r'[\s.\[\]{}()]+', s)
print(f"res:{res}")
new_res = filter(lambda x: x and x.strip(), res)
print(f"type(new_res):{type(new_res)}")
print(f"list(new_res):{list(new_res)}")

Pasted-image-20250419170845

真实运行结果(未删减)
['bottle', 'bag', 'big', 'apple']
['bottle', 'bag', 'big', 'apple']
['', 'os.path.abspath(path)', 'path.', '([path)]', '']
['', 'os', 'path', 'abspath', 'path', '', 'path', '', '', '', 'path', '', '', '']
['', 'os', 'path', 'abspath', 'path', 'path', 'path', '']
res:['', 'os', 'path', 'abspath', 'path', 'path', 'path', '']
type(new_res):<class 'filter'>
list(new_res):['os', 'path', 'abspath', 'path', 'path', 'path']

对照读法:

  • re.split(r'\s', s) 只按空白切 → 保留 os.path.abspath(path) 整体。
  • re.split(r'[\s.\[\]{}()]', s) 把空白 + . + 括号都当分隔符,且不加 +,所以连续分隔符之间会裂出空串(那一串 '')。
  • 加 + 后「连续分隔符算一个」,空串大幅减少,但仍保留首尾的 ''。
  • filter(lambda x: x and x.strip(), res) 再滤掉空串,得到干净的 6 段。注意 filter 返回的是迭代器,需要 list() 取值。

4.11 分组★★★

用小括号括起来的 pattern 所捕获的数据会被放进组 group 中。

match、search 函数可以返回 match 对象;findall 返回字符串列表;finditer 返回一个迭代器,迭代器中的每个元素是 match 对象。

如果 pattern 中使用了分组且匹配成功,就能在 match 对象上按下列方式取值:

  1. 使用 group(N) 方式返回对应分组,1 到 N 是对应的分组,0 返回整个匹配的字符串,N 不写缺省为 0
  2. 如果使用了命名分组,可以使用 group('name') 的方式取分组
  3. 也可以使用 groups() 返回所有组
  4. 使用 groupdict() 返回所有命名的分组
import re

s = """bottle\nbag\nbig\napple"""

res = re.match(r'b\w+', s)
print(res)
print(res.groups())
print('=' * 40)

res = re.match(r'(b)(\w+)', s)
print(res)
print(res.groups())
print('=' * 40)

res = re.search(r'(^a)(\w+)', s, re.M)
print(res)
print(res.groups())
print('=' * 40)

res = re.search(r'(a)(\w+)', s, re.M)
print(res)
# 取所有分组
print(res.groups())
# 取 0 分组,0 分组返回整个匹配的字符串
# 正则的分组号从 1 开始
print(res.group(0))
# 不写分组号,默认为 0
print(res.group())
# 取组 1
print(res.group(1))
# 取组 2
print(res.group(2))
print('=' * 40)

res = re.search(r'(a)(?P<tail>\w+)', s, re.M)
print(res)
# 取所有分组
print(res.groups())
# 取 0 分组,0 分组返回整个匹配的字符串
# 正则的分组号从 1 开始
print(res.group(0))
# 取组 1
print(res.group(1))
# 取组 2,命名分组也有组号
print(res.group(2))
# 获取所有的命名分组
print(res.groupdict())
print('=' * 40)
原文勘误:4 处「注释吞掉代码」

原文这一段里有 4 行把代码挤进了注释,会导致 res.group() 根本没执行:

原文 已改为
# 不写分组好,默认为 0print(res.group()) # 不写分组号,默认为 0 + 换行 + print(res.group())
# 取组 1print(res.group(1))(出现 2 次) # 取组 1 + 换行 + print(res.group(1))
# 取组 2print(res.group(2)) # 取组 2 + 换行 + print(res.group(2))

顺带修正错字「不写分组好」→「不写分组号」。

Pasted-image-20250419173833

真实运行结果(未删减)
<re.Match object; span=(0, 6), match='bottle'>
()
========================================
<re.Match object; span=(0, 6), match='bottle'>
('b', 'ottle')
========================================
<re.Match object; span=(8, 9), match='a'>
('a',)
========================================
<re.Match object; span=(8, 9), match='a'>
('a', 'g')
ag
ag
a
g
========================================
<re.Match object; span=(8, 9), match='a'>
('a', 'g')
ag
a
g
{'tail': 'g'}
========================================

注意第 3 段:re.search(r'(^a)(\w+)', s, re.M) 里 ^a 在 re.M 下先命中索引 8 的 'a',
而 (\w+) 至少要 1 个字符——索引 9 是 'g',所以能匹配成 ('a','g')。
但上面的 print(res) 显示 span=(8, 9) 只是 'a' 本身?——不,span=(8,9) 是第 3 段之后的第 4 段输出;
逐段核对请看 match= 文本:第 3 段整体匹配是 'ag'(^a + g)。这类「截图与命令行输出不一致」时,以真实输出为准。

groups() 与 groupdict() 的分工(实测)
方法 返回 含命名组吗
group(0) / group() 整个匹配的字符串 —
group(n) 第 n 组内容 ✅(命名组也有编号)
group('name') 命名组内容 ✅
groups() 元组,所有组的字符串 ✅(只给值,不给名)
groupdict() 字典 {名: 值} 只包含命名组
expand(template) 用 \1/\g<name> 模板重新拼串 ✅

实测 re.search(r'(a)(?P<tail>\w+)', 'bottle\nbag\nbig\napple', re.M):
groups() → ('a', 'g')(含命名组 tail),groupdict() → {'tail': 'g'},group(2) 与 group('tail') 都是 'g'。
另外 lastindex = 最后参与匹配的组号,lastgroup = 对应的组名(未命名则 None)。

4.11.1 findall 在有分组时的结果

先看下面的示例:

import re

s = """bottle\nbag\nbig\napple"""

print("====findall 中 pattern 有分组====")
res = re.findall(r'(b\w+)\s+(?P<name2>b\w+)\s+(?P<name3>b\w+)', s)
print(type(res))
print(res)
print("====findall 中 pattern 没有分组====")
res = re.findall(r'(?:b\w+)\s+b\w+\s+b\w+', s)
print(type(res))
print(res)
print("====findall 中 pattern 有一个分组====")
res = re.findall(r'(b\w+)\s+b\w+\s+b\w+', s)
print(type(res))
print(res)
print("====findall 中 pattern 有两个分组====")
res = re.findall(r'(b\w+)\s+b\w+\s+b(\w+)', s)
print(type(res))
print(res)

Pasted-image-20250419175609

真实运行结果(未删减)
====findall 中 pattern 有分组====
<class 'list'>
[('bottle', 'bag', 'big')]
====findall 中 pattern 没有分组====
<class 'list'>
['bottle\nbag\nbig']
====findall 中 pattern 有一个分组====
<class 'list'>
['bottle']
====findall 中 pattern 有两个分组====
<class 'list'>
[('bottle', 'ig')]
总结
  • findall 在没有分组时,返回一个列表,列表元素是所有匹配到的子串。
  • findall 在只有一个分组时,返回一个列表,列表中的每个元素是字符串,内容是每个匹配到的子串中的分组内容。
  • findall 在有多个分组时,返回一个列表,列表中的每个元素是元组,元组中的每个元素是每个匹配到的子串中的分组。
原文勘误:[!summary] 非标准 callout 类型

原文此处用了 > [!summary] 总结。summary 不是 Obsidian 标准 callout 类型,已改为标准的 [!success](表示结论/总结)。
同类替换在 §3.9「总结」处也有一处([!summary] → [!note])。

4.11.2 finditer 在有分组时的结果

import re

s = """bottle\nbag\nbig\napple"""

res = re.finditer(r'(b\w+)\s+(?P<name2>b\w+)\s+(?P<name3>b\w+)', s)
print(type(res))

for item in res:
    print(type(item), item)
    print(item.group())
    print(item.groups())
    print(item.groupdict())

Pasted-image-20250419182007

真实运行结果(未删减)
<class 'callable_iterator'>
<class 're.Match'> <re.Match object; span=(0, 14), match='bottle\nbag\nbig'>
bottle
bag
big
('bottle', 'bag', 'big')
{'name2': 'bag', 'name3': 'big'}

对比 §4.11.1:同样一个 pattern,findall 只能给出元组里的值,
而 finditer 的每个 Match 还能给 group()(整体)、groups()(所有组)、groupdict()(命名组)、span()(位置)。
需要「位置 + 命名 + 整体」时用 finditer,别无他选。

5 术语表(重要英文单词)

5.1 正则基础概念

术语 全拼/说明 释义
regex / regexp / RE regular expression(abbrev.)/ Regular Expression 正则表达式。re 模块的名字即取自 Regular Expression 的前两字母
metacharacter meta-(元)+ character 元字符。在正则中有特殊含义的字符,如 `. * + ? ^ $
literal — 字面量。在正则中没有特殊含义、只匹配自身的字符
escape — 转义。用 \ 让元字符恢复字面含义(\. 匹配点号)
pattern — 模式。即「正则表达式字符串」本身
dialect — 方言。同一套正则语法在不同实现(Python/PCRE/JS)中的差异
flavor — 风味/流派。与 dialect 近义,指具体实现的正则语义
BRE Basic Regular Expression 基本正则表达式,grep/sed/vi 默认使用
ERE Extended Regular Expression 扩展正则表达式,egrep/grep -E、sed -r 使用
PCRE Perl Compatible Regular Expressions 与 Perl 5 语法兼容的正则库,1997 年由 Philip Hazel 发布;PCRE2 于 2015 年发布
SRE Standard Regular Expression CPython re 模块的底层引擎,由 Fredrik Lundh 编写,Python 1.6 引入
backtracking — 回溯。正则引擎在匹配失败后回退、换一种方式重试的过程;贪婪/非贪婪即通过它实现

5.2 字符与匹配

术语 全拼/说明 释义
character class — 字符类/字符集合,写作 [...];[^...] 为否定字符类
quantifier — 限定符/量词。* + ? {n,m} 这类「重复次数」修饰
alternation — 或运算 |,用于表达「A 或 B」
anchor — 锚点。^ $ \A \Z \b \B 这类不消耗字符的位置断言
boundary — 边界。\b 单词边界、\B 非单词边界
greedy — 贪婪。默认行为,尽量多匹配(v.*y)
non-greedy / lazy / reluctant lazy 慵懒的 / reluctant 不情愿的 非贪婪。加 ? 后尽量少匹配(v.*?y)
possessive — 占有(Python 3.11+)。*+ ++ ?+ {m,n}+,不设回溯点,等价 (?>...)
group — 分组。用 (...) 把一段子表达式括起来
capturing group — 捕获分组 (...),会占组号并可被取回/反向引用
non-capturing group — 非捕获分组 (?:...),只改变优先级、不占组号
named group — 命名分组。Python 写作 (?P<name>...),可用 (?P=name) 反向引用
backreference back + reference 反向引用 \1、(?P=name),要求重复前面分组的匹配内容
assertion — 断言。条件式匹配,不消耗字符
lookahead look + ahead 先行(前瞻)断言 (?=...) / (?!...),看右边
lookbehind look + behind 后发(回顾)断言 (?<=...) / (?<!...),看左边;必须定长
zero-width — 零宽。断言不消耗字符,故称零宽
atomic group — 原子组 (?>...)(Python 3.11+),内部不再回溯
comment group — 注释组 (?#...),内容被忽略
conditional — 条件匹配 (?(id/name)yes|no)(Python 支持)

5.3 re 模块 API

术语 全拼/说明 释义
re.compile — 编译 pattern,返回 Pattern 对象(模块级有 512 项缓存)
re.match — 从起点(或 pos)匹配,前缀命中即可;失败返回 None
re.search — 全文扫描,找到第一个匹配就返回;失败返回 None
re.fullmatch full + match 要求整段完全匹配;失败返回 None
re.findall find + all 返回所有匹配的列表;元素形态受分组数影响
re.finditer find + iterator 返回迭代器,元素为 Match 对象
re.sub substitute(缩写) 替换所有匹配,返回新字符串
re.subn substitute + n 同 sub,额外返回替换次数,结果为 (new, count)
re.split — 按 pattern 分割字符串,返回列表
re.escape — 把字符串中的元字符全部转义,得到可安全嵌入 pattern 的字面量
re.purge — 清空编译缓存
Pattern — 编译后的正则对象类型(re.compile() 返回),旧称 RegexObject
Match — 匹配结果对象,含 group/groups/groupdict/span/start/end/string/pos/endpos/re/regs/expand/lastindex/lastgroup
pos / endpos position 位置 编译对象的匹配区间 [pos, endpos);仅对象方法支持
span — (start, end) 元组,前包后不包
expand — 用 \1/\g<name> 模板,基于 Match 生成新字符串
flag — 标志位。re.I/re.M/re.S/re.X/re.A/re.L/re.U,可 | 组合
RegexFlag — 标志常量的枚举类型(enum.IntFlag 子类,Python 3.6+)

6 系列导航

Python 基础系列
  • 1.Python 入门
  • 2.编程基础概念
  • 3.Python 中的基础数据类型
  • 4.字符串
  • 5.内建数据结构
  • 6.控制语句
  • 7.函数用法
  • 8.面向对象编程
  • 9.异常和错误
  • 10.文件操作(IO 技术)
  • 11.模块 (module)
  • 12.正则表达式 ★★★(本篇)
  • 13.并发编程
  • 14.网络编程
  • 15.Python操作MySQL

7 参考链接

分类:Python

posted @ 2026-04-11 02:01  挖掘鱼  阅读(34)  评论(0)    收藏  举报