文章

Python 正则表达式完全指南

🔤 Python 正则表达式完全指南:从零到精通

本文基于我们之前解析耳温枪日志的真实场景,系统讲解 Python 正则表达式的每一个知识点。学完本文,你将能应对 90% 以上的文本匹配需求。


目录


〇、正则表达式是什么?为什么需要它?

正则表达式(Regular Expression,简称 regex)是一种描述字符串模式的”迷你语言”。 打个比方:

1
2
3
4
5
6
7
8
9
10
你要在一个 10 万行的日志文件中,找出所有类似这样的行:
    <ma>i=23,O=3706
    <ma>i=105,O=2891
    <ma>i=7,O=4023
用普通字符串方法:
    line.startswith('<ma>') and 'i=' in line and 'O=' in line ...
    → 写一长串 if-else,容易漏,难维护
用正则表达式:
    r'^<ma>i=(\d+),O=(\d+)$'
    → 一行搞定,还能直接提取数字

核心价值:精确匹配 + 自动提取。

一、最快速的上手:re 模块的四个核心函数

Python 通过内置的 re 模块使用正则表达式。你只需要记住 4 个函数

函数作用返回值
re.match()从字符串 开头 匹配Match 对象 或 None
re.search()在字符串中 搜索第一个 匹配Match 对象 或 None
re.findall()找出 所有 匹配的子串列表
re.finditer()找出 所有 匹配(返回迭代器)迭代器(每个元素是 Match)

还有一个 工厂函数,推荐始终使用:

1
2
3
pattern = re.compile(r'你的正则')
# 之后用 pattern.match() / pattern.search() / pattern.findall()
# 比 re.match(r'...', text) 更高效(只编译一次)

1.1 re.match() — 从开头匹配

1
2
3
4
5
6
7
8
9
import re
pattern = re.compile(r'hello')
# ✅ 匹配成功:字符串以 hello 开头
result = pattern.match('hello world')
print(result)       # <re.Match object; span=(0, 5), match='hello'>
print(result.group())  # 'hello'
# ❌ 匹配失败:hello 不在开头
result = pattern.match('say hello')
print(result)       # None

1.2 re.search() — 搜索任意位置

1
2
3
4
pattern = re.compile(r'hello')
# ✅ 搜索成功:hello 在字符串中间也行
result = pattern.search('say hello world')
print(result.group())  # 'hello'

match vs search 的区别:match 只看开头,search 扫描全文。 在解析日志等逐行处理场景中,match 更安全(确保没有意外匹配)。

1.3 re.findall() — 找出所有匹配

1
2
3
4
text = "价格100元,折扣50元,税费10元"
# 找出所有连续数字
result = re.findall(r'\d+', text)
print(result)  # ['100', '50', '10']

1.4 re.finditer() — 所有匹配(带位置信息)

1
2
3
4
5
6
7
text = "价格100元,折扣50元,税费10元"
for match in re.finditer(r'\d+', text):
    print(f"找到: {match.group()}, 位置: {match.span()}")
# 输出:
# 找到: 100, 位置: (2, 5)
# 找到: 50, 位置: (9, 11)
# 找到: 10, 位置: (14, 16)

二、匹配单个字符:普通字符与元字符

正则表达式中的字符分为两类:

2.1 普通字符 — 直接匹配自身

1
re.findall(r'abc', 'abc abc xabc')  # ['abc', 'abc', 'abc']

字母、数字、大部分标点都是普通字符,写什么匹配什么。

2.2 元字符 — 有特殊含义的字符(必须背熟)

正则中有 12 个元字符,它们不是匹配自身,而是代表某种规则:

1
.  ^  $  *  +  ?  {  }  [  ]  \  |

⚠️ 当你要匹配这些字符本身时,必须用 \ 转义。

1
2
3
4
# 我想匹配字面量 "a.b"(点号是元字符)
re.findall(r'a\.b', 'a.b aXb a3b')  # ['a.b']  ← 只有 a.b 匹配
# 如果不转义,. 代表任意字符
re.findall(r'a.b', 'a.b aXb a3b')   # ['a.b', 'aXb', 'a3b']  ← 全部匹配

2.3 最常用的单字符元字符:.(点号)

. 匹配 除换行符 \n 之外的任意单个字符

1
re.findall(r'a.b', 'aXb a1b a\nb')  # ['aXb', 'a1b']  ← \n 不匹配

如果想让 . 也匹配换行符,使用 re.DOTALL 标志:

1
re.findall(r'a.b', 'a\nb', re.DOTALL)  # ['a\nb']

2.4 转义序列 — 匹配特定类型的字符

这些是 \ 开头的特殊组合,每个匹配一类字符:

表达式含义等价写法
\d任意一个数字 (0-9)[0-9]
\D任意一个非数字[^0-9]
\w任意一个”单词字符” (字母、数字、下划线)[a-zA-Z0-9_]
\W任意一个非单词字符[^a-zA-Z0-9_]
\s任意一个空白字符 (空格、Tab、换行)[ \t\r\n\f\v]
\S任意一个非空白字符[^ \t\r\n\f\v]
\b单词边界(无等价写法)
1
2
3
4
text = "abc 123 _xyz  价格@100"
re.findall(r'\d+', text)   # ['123', '100']   ← 所有数字串
re.findall(r'\w+', text)   # ['abc', '123', '_xyz']  ← 所有单词
re.findall(r'\S+', text)   # ['abc', '123', '_xyz', '价格@100']  ← 所有非空白

三、量词:控制匹配次数

量词放在 字符或分组 后面,表示”前面的内容重复多少次”。

3.1 四个基本量词

量词含义示例匹配
*0 次或多次ab*cac, abc, abbc, abbbc
+1 次或多次(至少1次)ab+cabc, abbc, abbbc… 但不匹配 ac
?0 次或 1 次(可选)ab?cac, abc
{n}恰好 n 次ab{3}cabbbc(恰好3个b)
{n,}至少 n 次ab{2,}cabbc, abbbc, abbbbc
{n,m}n 到 m 次ab{2,4}cabbc, abbbc, abbbbc
1
2
3
4
5
6
# 提取连续数字(1个或多个数字)
re.findall(r'\d+', 'a12b345c6')  # ['12', '345', '6']
# 提取连续数字(0个或多个)→ 注意空串
re.findall(r'\d*', 'a12b345c')   # ['', '12', '', '345', '', '']
# 匹配 3 到 5 位数的数字
re.findall(r'\d{3,5}', '1234567')  # ['12345']  ← 贪婪匹配,优先匹配最多的

3.2 贪婪 vs 非贪婪(重要!)

默认情况下,量词是 贪婪模式(Greedy):尽可能多地匹配。 在量词后面加 ? 变为 非贪婪模式(Lazy):尽可能少地匹配。

1
2
3
4
5
6
7
html = '<div>内容1</div><div>内容2</div>'
# 贪婪:.* 匹配到尽可能远的 </div>
re.findall(r'<div>.*</div>', html)
# ['<div>内容1</div><div>内容2</div>']  ← 一次性吞掉了两个 div!
# 非贪婪:.*? 匹配到最近的 </div> 就停
re.findall(r'<div>.*?</div>', html)
# ['<div>内容1</div>', '<div>内容2</div>']  ← 正确!分别匹配两个

口诀:加 ? 就是非贪婪,”见好就收”。

四、边界锚点:定位匹配位置

锚点不匹配任何字符,而是匹配一个 位置

锚点含义示例
^字符串 开头^hello → 匹配以 hello 开头的行
$字符串 结尾world$ → 匹配以 world 结尾的行
\b单词边界\bcat\b → 匹配 cat 但不匹配 catalog

4.1 ^$ — 行首行尾

1
2
3
4
5
6
7
text = "hello world\nhello python\nhi world"
# 匹配以 hello 开头的行
re.findall(r'^hello.*$', text, re.MULTILINE)
# ['hello world', 'hello python']
# 匹配以 world 结尾的行
re.findall(r'.*world$', text, re.MULTILINE)
# ['hello world', 'hi world']

关键:^$ 默认匹配整个字符串的首尾。 如果要匹配每一行的首尾,必须加 re.MULTILINE 标志。

4.2 \b — 单词边界

单词边界 = 单词字符与非单词字符之间的位置。

1
2
3
4
5
6
7
text = "the cat scattered the catalog"
# 匹配独立的单词 cat
re.findall(r'\bcat\b', text)
# ['cat']  ← 只有第一个 cat 匹配,scattered 和 catalog 中的不匹配
# 不加 \b,匹配所有包含 cat 的
re.findall(r'cat', text)
# ['cat', 'cat', 'cat']  ← scattered 中也有 cat

五、字符集与范围:匹配”某类字符”

5.1 [...] — 字符集

方括号内的字符,匹配其中任意一个

1
2
3
4
5
6
# 匹配 a、b、c 中的任意一个
re.findall(r'[abc]', 'xaybzc')  # ['a', 'b', 'c']
# 匹配任意数字
re.findall(r'[0123456789]', 'a1b2c')  # ['1', '2']
# 匹配任意小写字母
re.findall(r'[a-z]', 'Hello World 123')  # ['e', 'l', 'l', 'o', 'o', 'r', 'l', 'd']

5.2 范围写法 — -

1
2
3
4
re.findall(r'[a-z]+', 'Hello World')     # ['ello', 'orld']    ← 小写字母串
re.findall(r'[A-Z]+', 'Hello World')     # ['H', 'W']          ← 大写字母串
re.findall(r'[0-9]+', 'abc 123 def 456') # ['123', '456']      ← 数字串
re.findall(r'[a-zA-Z]+', 'Hello World')  # ['Hello', 'World']  # 所有字母串

5.3 取反 — [^...]

方括号内第一个字符是 ^,表示 不匹配这些字符

1
2
3
4
# 匹配所有非数字字符
re.findall(r'[^0-9]+', 'abc123def456')  # ['abc', 'def']
# 匹配所有非元音字母
re.findall(r'[^aeiou]+', 'hello')       # ['h', 'll']

⚠️ [^...] 中的 ^ 和行首锚点 ^完全不同的东西。方括号内的 ^ 表示取反,方括号外的 ^ 表示行首。

5.4 - 在字符集中的特殊处理

-[...] 内表示范围,但如果需要匹配字面量 -,有两种方式:

1
2
3
4
5
# 把 - 放在开头或结尾
re.findall(r'[-abc]', 'a-b')     # ['a', '-', 'b']
re.findall(r'[abc-]', 'a-b')     # ['a', '-', 'b']
# 或者用 \ 转义
re.findall(r'[a\-z]', 'a-z')     # ['a', '-', 'z']

六、分组与捕获:提取你想要的数据

这是正则表达式 最强大的功能,没有之一。

6.1 捕获组 ( ... )

用圆括号把一部分正则包起来,匹配成功后可以通过 .group() 提取。

1
2
3
4
5
6
7
pattern = re.compile(r'(\d{4})-(\d{2})-(\d{2})')
m = pattern.match('2024-01-15')
print(m.group(0))   # '2024-01-15'  ← 整个匹配(group(0) 永远是整体)
print(m.group(1))   # '2024'        ← 第1个括号
print(m.group(2))   # '01'          ← 第2个括号
print(m.group(3))   # '15'          ← 第3个括号
print(m.groups())   # ('2024', '01', '15')  ← 所有分组组成的元组

6.2 命名捕获组 (?P<name> ... )

给分组取个名字,用名字提取,代码可读性拉满。

1
2
3
4
5
6
pattern = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
m = pattern.match('2024-01-15')
print(m.group('year'))    # '2024'
print(m.group('month'))   # '01'
print(m.group('day'))     # '15'
print(m.groupdict())      # {'year': '2024', 'month': '01', 'day': '15'}

6.3 findall 与分组的特殊行为

findall 遇到分组时,行为会发生变化——它只返回分组内容,不返回整体

1
2
3
4
5
6
7
8
9
# 无分组:返回整个匹配
re.findall(r'\d{4}-\d{2}-\d{2}', '日期: 2024-01-15 和 2023-12-01')
# ['2024-01-15', '2023-12-01']
# 有1个分组:返回该分组的内容
re.findall(r'(\d{4})-\d{2}-\d{2}', '日期: 2024-01-15 和 2023-12-01')
# ['2024', '2023']  ← 只返回年份!
# 有2个分组:返回元组列表
re.findall(r'(\d{4})-(\d{2})-\d{2}', '日期: 2024-01-15 和 2023-12-01')
# [('2024', '01'), ('2023', '12')]

记忆规则:findall 有分组时,返回的是分组内容(元组),不是整个匹配。

6.4 分组的引用

在正则内部引用:\1, \2 引用之前匹配的内容。

1
2
3
# 匹配重复的单词
re.findall(r'\b(\w+)\s+\1\b', 'the the cat cat dog')
# ['the', 'cat']  ← the the 和 cat cat 各匹配一次

在替换字符串中引用:\g<1>\1

1
2
3
# 把 "姓,名" 格式转为 "名 姓"
re.sub(r'(\w+),(\w+)', r'\2 \1', 'Smith,John; White,Tom')
# 'John Smith; Tom White'

七、特殊结构:或、非捕获、断言

7.1 或 | — 匹配多种模式之一

1
2
3
# 匹配 cat 或 dog
re.findall(r'cat|dog', 'I have a cat and a dog')
# ['cat', 'dog']

⚠️ | 的优先级非常低! 注意加括号控制范围:

1
2
3
4
5
# ❌ 错误:意思是 "cata" 或 "b"
r'cata|b'

# ✅ 正确:意思是 "cat" 或 "dog",后面都跟 "food"
r'(cat|dog)food'

7.2 非捕获组 (?: ... )

有时候你只想”分组”但不”捕获”(不需要通过 group() 提取)。用 (?:...) 可以提升性能,避免干扰 findall 的返回结果。

1
2
3
4
# 捕获组:findall 只返回分组内容
re.findall(r'(cat|dog)s?', 'cats and dogs')  # ['cat', 'dog']
# 非捕获组:findall 返回整个匹配
re.findall(r'(?:cat|dog)s?', 'cats and dogs')  # ['cats', 'dogs']

7.3 零宽断言(Lookaround)

断言不消耗字符,只检查某个位置的前后是否满足条件。这是正则表达式的 高级技能

断言含义示例
(?=...)正向前瞻:后面跟着…\d+(?=px) → 匹配 100px 中的 100(但不含 px)
(?!...)负向前瞻:后面不跟着…\d+(?!px) → 匹配 100em 中的 100(100 后面不是 px)
(?<=...)正向后顾:前面是…(?<=\$)\d+ → 匹配 $100 中的 100(但不含 $)
(?<!...)负向后顾:前面不是…(?<!\$)\d+ → 匹配 100(前面没有 $)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 正向前瞻:提取单位为 px 的数字
text = "width:100px height:200em margin:50px"
re.findall(r'\d+(?=px)', text)
# ['100', '50']  ← 只匹配 px 前面的数字
# 正向后顾:提取美元金额(不含 $ 符号)
text = "$100 spent, €50 remaining"
re.findall(r'(?<=\$)\d+', text)
# ['100']  ← 只匹配 $ 后面的数字
# 组合使用:提取 "key=value" 中的 value(value 不含引号)
text = 'name="Alice" age=25 city="Beijing"'
re.findall(r'(?<=\=)[^=]+(?=$|\s)', text)
# 没办法完美做到,换一种思路

Python 的后顾断言 (?<=...) 要求必须是固定长度的,不能写 (?<=\d+),但可以写 (?<=\d{3})


八、实战回顾:耳温枪日志正则逐字拆解

回到我们之前的真实场景,现在你应该能完全看懂这三条正则了:

正则 1:^\s*<m\.>n=(\d+),o=(\d+)$

1
2
3
4
5
6
7
8
9
10
^\s*<m\.>n=(\d+),o=(\d+)$
│ │  │    │  │    │      │
│ │  │    │  │    │      └── $ 行尾锚点
│ │  │    │  │    └───────── (\d+) 捕获组2:匹配 o 的值(1个或多个数字)
│ │  │    │  └────────────── ,o= 匹配字面量
│ │  │    └───────────────── (\d+) 捕获组1:匹配 n 的值
│ │  └────────────────────── \. 转义的点号,匹配字面量 "."
│ └───────────────────────── <m.> 匹配字面量(< 和 > 是普通字符)
└──────────────────────────── \s* 匹配 0个或多个空白(兼容行首空格)
                              ^ 行首锚点

正则 2:^<ma>i=(\d+),O=(\d+)$

1
2
3
4
5
6
^<ma>i=(\d+),O=(\d+)$
│     │   │     │
│     │   │     └── (\d+) 捕获组2:大写 O 的值
│     │   └──────── ,O= 大写 O,与小写 o 区分
│     └────────────── (\d+) 捕获组1:i 的值
└──────────────────── <ma> 无需转义(无特殊字符)

正则 3:^<me>o=([\d.]+),x=(\d+)$

1
2
3
4
5
6
7
^<me>o=([\d.]+),x=(\d+)$
│      │      │     │
│      │      │     └── (\d+) 捕获组2:x 的值(整数)
│      │      └──────── ,x= 字面量
│      └─────────────── ([\d.]+) 捕获组1:o 的值
│                         [\d.] = 数字或小数点,兼容 int 和 float
└──────────────────────── <me> 字面量

九、常见陷阱与最佳实践

陷阱 1:忘记转义 .(最高频错误)

1
2
3
4
5
# ❌ . 匹配任意字符,"a.b" 和 "aXb" 都会被匹配
re.match(r'a.b', 'aXb')  # ✅ 匹配成功(但可能不是你想要的)
# ✅ 转义后才匹配字面量点号
re.match(r'a\.b', 'aXb')  # ❌ 不匹配
re.match(r'a\.b', 'a.b')  # ✅ 匹配成功

陷阱 2:findall 有分组时行为变化

1
2
3
re.findall(r'<(\w+)>', '<div><p>hello</p></div>')
# 你可能期望: ['<div>', '<p>', '</p>', '</div>']
# 实际返回:    ['div', 'p', '/p', '/div']  ← 只有括号内的内容!

解决方案:用 finditer 代替 findall

1
2
[m.group(0) for m in re.finditer(r'<(\w+)>', '<div><p>hello</p></div>')]
# ['<div>', '<p>', '</p>', '</div>']  ← group(0) 是整体匹配

陷阱 3:\b 对中文的行为

1
2
3
4
5
# \b 是基于 \w(ASCII 字母数字下划线)的边界
# 中文不是 \w,所以 \b 在中文前后不生效
re.findall(r'\b价格\b', '价格100元')  # []  ← 匹配不到!
# 中文场景建议直接用 ^ $ 或自定义边界
re.findall(r'价格', '价格100元')      # ['价格']

最佳实践清单

#实践原因
1始终使用 re.compile()只编译一次,循环中性能差 10 倍以上
2始终使用原始字符串 r'...'避免 Python 转义和正则转义冲突
3多用命名分组 (?P<name>...)代码可读性好,维护方便
4不确定时用 re.findall() 测试先看匹配了什么,再加分组提取
5能不加 .* 就不加.* 太宽泛,容易过度匹配,尽量精确
6需要匹配字面量特殊字符时记得转义. * + ? \ ( ) [ ] { } \| ^ $
7正则太长时,拆成多行加注释re.VERBOSE 标志允许写注释

re.VERBOSE 写可读正则

1
2
3
4
5
6
7
8
9
10
pattern = re.compile(r"""
    ^\s*                # 行首,允许前导空白
    <m\.>               # 字面量 <m.>
    n=(\d+)             # 捕获 n 的值
    ,                   # 逗号分隔符
    o=(\d+)             # 捕获 o 的值
    $                   # 行尾
""", re.VERBOSE)
# 等价于:r'^\s*<m\.>n=(\d+),o=(\d+)$'
# 但可读性天壤之别!

十、速查表

元字符速查

符号含义
.任意单个字符(除 \n
^行首
$行尾
\d数字 [0-9]
\D非数字 [^0-9]
\w单词字符 [a-zA-Z0-9_]
\W非单词字符
\s空白字符
\S非空白字符
\b单词边界

量词速查

符号含义
*0 次或多次
+1 次或多次
?0 次或 1 次
{n}恰好 n 次
{n,}至少 n 次
{n,m}n 到 m 次
*? +? ??非贪婪版本

分组速查

符号含义
(...)捕获组
(?:...)非捕获组
(?P<name>...)命名捕获组
\1 \2引用第 N 个分组
(?!...)负向前瞻
(?=...)正向前瞻
(?<!...)负向后顾
(?<=...)正向后顾

re 模块函数速查

函数用途
re.compile(r'...')编译正则对象
pattern.match(text)从开头匹配
pattern.search(text)全文搜索第一个
pattern.findall(text)找出所有匹配(返回列表)
pattern.finditer(text)找出所有匹配(返回迭代器)
pattern.sub(r'...', text)替换所有匹配
pattern.split(text)按正则分割字符串

学完这篇,你已经掌握了 Python 正则表达式的核心知识。 剩下的就是多练——遇到复杂的匹配需求时,先用 re.findall() 在 IPython 里调试,确认匹配结果正确后再写进代码。如果遇到拿不准的正则,推荐使用 regex101.com 在线调试,它会逐段解释你的正则含义。

本文由作者按照 CC BY 4.0 进行授权