【Python】正则表达式
目录
- 正则表达式
- 基本概念
- re 模块常用函数
- 正则表达式修饰符(Flags)
- 高级用法
- 应用案例
正则表达式
基本概念
什么是正则表达式?
正则表达式(Regex Expressions)是一种 用来匹配和处理字符串的规则表达式。它不是编程语言,但几乎所有编程语言都支持它(Python、Java、JavaScript、C#、Go……)。
直观上,正则表达式就是一种 字符串模式(pattern)。
比如:
\d{3}-\d{4}-\d{4}
→\to→ 可以匹配一个中国手机号的格式(xxx-xxxx-xxxx)。^[A-Za-z0-9_]{6,12}$
→\to→ 可以匹配一个 6–12 位的账号名(只允许字母、数字和下划线)。
换句话说:正则表达式是 描述字符串特征的“公式”。
Python 的原始字符串(Raw String)
在正则里,很多特殊符号都用 \
(反斜杠)开头。但 Python 自己也用 \
作为转义符(比如 \n
换行)。
为了避免冲突,Python 提供了 原始字符串:
normal = "Hello\nWorld"
print(normal) # 换行raw = r"Hello\nWorld"
print(raw) # 输出 Hello\nWorld
所以写正则时,几乎总是用 r'...'
。
正则表达式的基本组成
正则表达式是由一些 元字符(meta characters) 和 普通字符 组成的。
-
普通字符
普通字符表示 自身。
-
abc
→\to→ 匹配 “abc”。 -
123
→\to→ 匹配 “123”。
-
-
元字符(特殊符号)
元字符是正则表达式的“魔法”,用来表示特殊意义。
-
匹配字符类
符号 含义 示例 .
匹配除换行符以外的任意单个字符 a.c
可以匹配 “abc”, “axc”\d
数字 [0-9] \d\d\d
→ “123”\w
单词字符 [A-Za-z0-9_] \w+
→ “hello_123”\s
空白符(空格、制表符、换行) a\sb
可以匹配 “a b”\D
非数字 \W
非单词字符 \S
非空白符 -
数量限定符
符号 含义 示例 *
匹配 0 次或多次 ab*
可以匹配 “a”, “ab”, “abbb”+
匹配 1 次或多次 ab+
匹配 “ab”, “abbb”,但不匹配 “a”?
匹配 0 次或 1 次 ab?
匹配 “a” 或 “ab”{n}
恰好 n 次 \d{4}
匹配 “2025”{n,}
至少 n 次 \d{2,}
匹配 “12”, “12345”{n,m}
n 到 m 次 a{2,4}
匹配 “aa”, “aaa”, “aaaa” -
位置锚点
符号 含义 示例 ^
匹配字符串开头 ^abc
只能匹配以 “abc” 开头的字符串$
匹配字符串结尾 abc$
匹配以 “abc” 结尾的字符串\b
单词边界 \bcat\b
匹配 “cat”,但不匹配 “category”\B
非单词边界 -
分组和选择
符号 含义 示例 ()
分组 (abc){2}
匹配 “abcabc”` ` ` []
字符集 [abc]
匹配 “a” 或 “b” 或 “c”[^]
非字符集 [^0-9]
匹配非数字字符
-
匹配原理
正则表达式的底层原理基于 有限自动机(Finite Automaton, FA)。
你可以简单理解为:
- 正则是一个 规则引擎。
- 引擎会从字符串开头扫描,尝试匹配模式。
- 如果当前路径失败,会 回溯(backtracking),尝试别的匹配方式。
- 成功 → 返回匹配结果;失败 → 返回 “不匹配”。
举个例子:
正则:ab*c
匹配字符串:abbbc
- 从左到右扫描:
- 匹配到
a
✅ - 匹配到
b*
→ 有 3 个 “b” ✅ - 匹配到
c
✅
- 匹配到
- 完全匹配成功。
常见应用场景
- 数据验证
- 邮箱:
^[\w.-]+@[\w.-]+\.[A-Za-z]{2,6}$
- 手机号:
^1[3-9]\d{9}$
- 邮箱:
- 文本搜索与替换
- 替换多个空格为一个:
\s+ → " "
- 找出所有数字:
\d+
- 替换多个空格为一个:
- 日志分析
- 匹配 IP 地址:
\b\d{1,3}(\.\d{1,3}){3}\b
- 匹配 IP 地址:
- 网页爬虫
- 提取 HTML 标签内容:
<title>(.*?)</title>
- 提取 HTML 标签内容:
注意事项
- 可读性差 → 正则太复杂时,维护成本高。
解决:用 分段注释 或 编译模式re.VERBOSE
(Python)。 - 性能问题 → 复杂正则可能导致回溯爆炸。
比如^(a+)+$
匹配 “aaaaaaaaaaaaaaaaaaaa!” 时,会卡死。 - 不同语言差异 → 各语言的正则引擎略有不同(Python、Java、JS 都有差别)。
re 模块常用函数
Python 的正则库:
import re
re.match(pattern, string, flags=0)
作用
- 只在字符串开头 尝试匹配。
- 如果开头不符合规则,就直接返回
None
,不会继续查找。
参数
pattern
:正则表达式模式。string
:要匹配的目标字符串。flags
:匹配模式(忽略大小写、多行、点号匹配换行等)。
返回值
- 匹配成功:返回
Match
对象(可以用.group()
、.span()
等方法取结果)。 - 匹配失败:返回
None
。
示例
import retext = "Cats are smarter than dogs"m = re.match(r'Cats', text)
print(m.group()) # Cats
print(m.start(), m.end()) # (0, 4)print(re.match(r'dogs', text)) # None,因为不是开头
注意
- 常见用途:判断字符串格式,比如是否以
"http://"
开头。 - 如果想要在任意位置匹配,用
re.search()
。
re.search(pattern, string, flags=0)
作用
- 在整个字符串中搜索 第一个匹配项。
- 只返回第一个,不会继续找。
示例
m = re.search(r'dogs', "Cats are smarter than dogs")
print(m.group()) # dogs
应用场景
- 检查字符串中是否包含某个模式(如检测邮箱中是否包含
@
)。
match
vs search
的区别
line = "Cats are smarter than dogs"print(re.match(r'dogs', line)) # None(因为开头不是 dogs)
print(re.search(r'dogs', line)) # <re.Match object>(找到了 dogs)
总结:
match
→\to→ 从开头匹配。search
→\to→ 搜索整个字符串,找到第一个即可。
re.findall(pattern, string, flags=0)
作用
- 找出 所有非重叠的匹配项,以 列表形式返回。
示例
s = "Simple is better than complex."
print(re.findall(r"ple", s)) # ['ple', 'ple']
print(re.findall(r"\w+", s)) # ['Simple', 'is', 'better', 'than', 'complex']
应用场景
- 批量提取关键词、邮箱地址、手机号等。
re.finditer(pattern, string, flags=0)
作用
- 返回一个 迭代器,里面是 Match 对象。
- 比
findall
更灵活,因为能拿到更多信息(位置、分组等)。
示例
s = "Simple is better than complex."for m in re.finditer(r"is", s):print(m.group(), m.span())
# is (7, 9)
应用场景
- 需要知道匹配结果的 具体位置(如文本标注、日志分析)。
re.sub(pattern, repl, string, count=0, flags=0)
作用
- 用
repl
替换字符串中匹配到的内容。 count
控制替换次数(默认替换所有)。
示例
phone = "2004-959-559 # This is Phone Number"# 去掉注释
num = re.sub(r"#.*$", "", phone)
print(num) # 2004-959-559# 去掉非数字
num = re.sub(r"\D", "", phone)
print(num) # 2004959559
应用场景
- 数据清洗,比如去掉 HTML 标签、过滤特殊字符。
re.compile(pattern, flags=0)
作用
- 把正则表达式编译成一个 正则对象,后续可重复使用,提高效率。
示例
pattern = re.compile(r"is")
print(pattern.findall("Simple is better than complex.")) # ['is']
print(pattern.findall("This is a test.")) # ['is', 'is']
优点
- 多次使用同一个正则时,效率更高。
- 代码更清晰:
pattern.findall()
代替re.findall(pattern, ...)
。
补充:Match
对象常用方法
无论是 match()
、search()
、finditer()
,匹配成功时都会返回一个 Match
对象。
常用方法:
.group()
→\to→ 返回匹配的字符串.span()
→\to→ 返回 (起始位置, 结束位置).start()
/.end()
→\to→ 起始和结束下标.groups()
→\to→ 返回所有分组内容
例:
m = re.search(r'(\d+)-(\d+)-(\d+)', "2004-959-559")
print(m.group()) # 2004-959-559
print(m.group(1)) # 2004
print(m.group(2)) # 959
print(m.group(3)) # 559
print(m.groups()) # ('2004', '959', '559')
正则表达式修饰符(Flags)
在 Python 的 re
模块中,修饰符(flags)用来改变正则表达式的默认匹配行为。
它们通常通过两种方式使用:
-
在函数调用时传参
re.findall(pattern, string, flags=re.I | re.M)
多个 flags 可以用
|
连接。 -
在正则表达式内部使用
(?修饰符)
re.findall("(?i)abc", "ABC") # 忽略大小写匹配
re.I
/ re.IGNORECASE
—— 忽略大小写
作用
- 默认情况下,正则匹配是区分大小写的,比如
abc
不能匹配ABC
。 - 加上
re.I
后,匹配时不区分大小写。
示例
import retext = "Python, PYTHON, pyThOn"# 不加 re.I
print(re.findall("python", text))
# []# 加 re.I
print(re.findall("python", text, re.I))
# ['Python', 'PYTHON', 'pyThOn']
注意事项
- 对于 Unicode 字符(比如 ä, ü, ç),忽略大小写的行为依赖 Python 的 Unicode 数据库,可能和你的预期不完全一致。
- 例如:德语 ß 会被当作 “ss”,这在某些情况下会带来歧义。
re.M
/ re.MULTILINE
—— 多行模式
作用
- 默认情况下,
^
只匹配整个字符串的开头,$
只匹配整个字符串的结尾。 - 开启
re.M
后,^
和$
会匹配每一行的开头和结尾(遇到换行符\n
会重置)。
示例
text = """hello world
python regex
goodbye"""# 默认模式
print(re.findall("^python", text))
# [] (只检查字符串开头,没有匹配到)# 多行模式
print(re.findall("^python", text, re.M))
# ['python'] (匹配到第二行开头的 python)
注意事项
\A
和\Z
永远只匹配字符串的开头和结尾,不受re.M
影响。- 所以如果你只想匹配整个文本开头结尾,而不是逐行,就该用
\A
、\Z
。
re.S
/ re.DOTALL
—— 点号匹配换行符
作用
- 默认情况下,
.
匹配除了换行符\n
以外的任意字符。 - 加上
re.S
后,.
也能匹配换行符。
示例
text = """hello
world"""# 默认模式
print(re.findall("hello.*world", text))
# [] (因为 . 不会跨行,所以匹配失败)# DOTALL 模式
print(re.findall("hello.*world", text, re.S))
# ['hello\nworld']
注意事项
- 很常见的场景是“贪婪匹配跨行”,比如抓取 HTML 文档中
<div> ... </div>
的内容时要加re.S
。 - 但要小心,这样会让
.*
跨越非常多内容,可能导致“过度匹配”(贪婪问题),通常要配合?
限制。
re.X
/ re.VERBOSE
—— 忽略正则中的空格和注释
作用
- 默认情况下,正则表达式里写的所有空格都会被认为是匹配内容的一部分。
- 开启
re.X
后,正则里的空格会被忽略(除非用\
转义),并且可以在正则中写注释#
,大大提高可读性。
示例
pattern = re.compile(r"""^ # 开头\d{4} # 年份(4位数字)- # 中间的横杠\d{2} # 月份(2位数字)- # 中间的横杠\d{2} # 日期(2位数字)$ # 结尾
""", re.X)print(pattern.match("2025-09-24"))
# <re.Match object; span=(0, 10), match='2025-09-24'>
注意事项
re.X
下,正则里的空格默认会被忽略,如果你真的要匹配空格,必须写成\
或者[ ]
。- 用
re.X
可以写多行注释型正则,非常适合复杂场景。
修饰符 | 全名 | 作用 | 常见用途 |
---|---|---|---|
re.I | IGNORECASE | 忽略大小写 | 搜索关键字不区分大小写 |
re.M | MULTILINE | ^ 和 $ 匹配每行开头结尾 | 按行匹配日志、配置文件 |
re.S | DOTALL | . 匹配包括换行符 | 跨行匹配多行文本 |
re.X | VERBOSE | 忽略正则里的空格和注释 | 书写复杂正则时提高可读性 |
高级用法
贪婪(Greedy) vs 非贪婪(Non-Greedy / Lazy)
概念
- 贪婪模式(默认行为):在满足整体匹配的前提下,
*
、+
、?
、{m,n}
等量词会尽可能多地匹配字符。 - 非贪婪模式:在满足整体匹配的前提下,量词会尽可能少地匹配字符。写法是 在量词后面加
?
。
示例
import retext = "<python>perl>"# 贪婪:.* 会尽可能多
print(re.findall(r"<.*>", text))
# ['<python>perl>']# 非贪婪:.*? 会尽可能少
print(re.findall(r"<.*?>", text))
# ['<python>']
贪婪匹配(.\*
):
- 正则表达式中的
.*
会尽可能匹配最长的符合条件的字符串 - 在
<.*>
中,.*
会从第一个<
开始,一直匹配到最后一个>
结束 - 所以
<python>perl>
整个字符串被匹配为一个结果
非贪婪匹配(.\*?
):
- 当在
*
、+
、?
、{n,m}
等量词后加上?
,就变成了非贪婪模式 - 非贪婪模式会尽可能匹配最短的符合条件的字符串
- 在
<.*?>
中,.*?
会从第一个<
开始,匹配到第一个>
就停止 - 所以只匹配到
<python>
这个最短的符合条件的字符串
使用场景
- 贪婪适合匹配“一段完整的最大范围内容”。
- 非贪婪适合匹配“多个小片段”,比如提取 HTML 标签。
注意事项
- 非贪婪模式依然会在整体能匹配的情况下,逐步尝试最少字符。
- 如果后面没有限制条件,贪婪和非贪婪的结果可能一样。
贪婪模式 | 非贪婪模式 | 含义 |
---|---|---|
* | *? | 匹配 0 次或多次 |
+ | +? | 匹配 1 次或多次 |
? | ?? | 匹配 0 次或 1 次 |
{n,m} | {n,m}? | 匹配 n 到 m 次 |
分组与反向引用
分组(()
)
- 用
()
可以把正则的一部分括起来,单独当作一个子模式。 - 匹配成功后,可以通过
group(n)
取出。
m = re.match(r"(\d+)-(\d+)", "123-456")
print(m.group(1)) # 123
print(m.group(2)) # 456
print(m.groups()) # ('123', '456')
反向引用
- 在正则表达式中,用
\1
、\2
等表示对前面分组的“再次引用”。 - 常用于匹配重复的内容。
text = "hello hello world"# (\w+) 匹配一个单词,\1 要求后面再出现相同的单词
print(re.findall(r"(\w+)\s+\1", text))
# ['hello']
匹配过程:
- 在字符串
"hello hello world"
中: - 第一个
hello
被(\w+)
捕获(分组 1 的内容为hello
) - 中间的空格被
\s+
匹配 \1
要求匹配与分组 1 相同的内容(即hello
)- 因此整个模式匹配
"hello hello"
,最终返回捕获的分组内容['hello']
命名分组
- 可以给分组命名,方便读取。
m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2025-09")
print(m.group("year")) # 2025
print(m.group("month")) # 09
正则表达式 r"(?P<year>\d{4})-(?P<month>\d{2})"
:
(?P<year>\d{4})
:定义一个名为year
的分组,匹配 4 位数字(年份)-
:匹配横杠分隔符(?P<month>\d{2})
:定义一个名为month
的分组,匹配 2 位数字(月份)(?P<name>pattern)
是命名分组的语法,name
是分组名称,pattern
是该分组的匹配模式
匹配结果处理:
m.group("year")
:通过分组名称year
获取该分组的匹配结果(2025
)m.group("month")
:通过分组名称month
获取该分组的匹配结果(09
)- 除了使用名称,仍然可以使用索引引用分组(
m.group(1)
对应year
,m.group(2)
对应month
)
断言(Lookaround)
断言是一种“零宽断言”,意思是它只检查条件,不实际消耗字符。
常见的有 前瞻(lookahead) 和 后顾(lookbehind)。
前瞻(lookahead)
-
肯定前瞻
(?=...)
- 含义:当前位置 后面必须跟着 …
- 不会消耗字符,只是检查条件。
s = "Python! Python?" print(re.findall(r"Python(?=!)", s)) # ['Python'] (匹配后面跟 ! 的 Python)
-
否定前瞻
(?!...)
- 含义:当前位置 后面不能跟着 …
print(re.findall(r"Python(?!\?)", s)) # ['Python'] (匹配后面不是 ? 的 Python)
后顾(lookbehind)
-
肯定后顾
(?<=...)
- 含义:当前位置 前面必须是 …
s = "I like #Python and #Regex" print(re.findall(r"(?<=#)\w+", s)) # ['Python', 'Regex']
-
否定后顾
(?<!...)
- 含义:当前位置 前面不能是 …
print(re.findall(r"(?<!#)\w+", s)) # ['I', 'like', 'and']
比如我们要从 HTML 里提取 <title>...</title>
的内容:
html = "<title>My Site</title><title>Blog</title>"# 贪婪模式(错误)
print(re.findall(r"<title>.*</title>", html))
# ['<title>My Site</title><title>Blog</title>'] -> 贪婪吃掉太多# 非贪婪模式(正确)
print(re.findall(r"<title>.*?</title>", html))
# ['<title>My Site</title>', '<title>Blog</title>']# 去掉标签,只保留内容(用分组)
print(re.findall(r"<title>(.*?)</title>", html))
# ['My Site', 'Blog']
应用案例
匹配邮箱地址
基本模式
import repattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"print(re.findall(pattern, "My email is test@mail.com"))
# ['test@mail.com']
解释
[a-zA-Z0-9._%+-]+
:邮箱用户名部分,可以包含字母、数字、点、下划线、百分号、加号、减号。@
:邮箱固定格式,必须有一个@
。[a-zA-Z0-9.-]+
:邮箱域名部分,可以包含字母、数字、点、减号。\.
:点号要转义,表示真正的.
。[a-zA-Z]{2,}
:顶级域名(TLD),至少两个字母,比如.com
、.cn
、.org
。
注意事项
- 这个正则能匹配大部分常见邮箱,但不是 RFC 标准,所以不一定涵盖所有情况(例如带引号的用户名)。
- 比如
user+alias@gmail.com
能匹配,但user@[123.123.123.123]
不行。
改进版
如果想更严格,可以这样:
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[A-Za-z]{2,}$"
加上 ^
和 $
,确保整个字符串就是邮箱,而不是文本中的一部分。
匹配中国手机号
基本模式
pattern = r"^1[3-9]\d{9}$"print(re.match(pattern, "13812345678"))
# <re.Match object; span=(0, 11), match='13812345678'>
解释
^
:开头。1
:中国手机号都是以1
开头。[3-9]
:第二位号码范围(中国手机号第二位是 3–9 之间的数字)。\d{9}
:后面 9 位数字。$
:结尾。
总长度正好 11 位。
注意事项
- 这个正则覆盖了常见的手机号段(13x、15x、18x、17x、19x 等)。
- 但并不是所有
[3-9]
的组合都有效,比如140xxxx
曾是上网卡号,现在基本停用。
改进版(更严格)
如果想匹配常见号段,可以写:
pattern = r"^1(3\d|4[5-9]|5[0-35-9]|6[5-7]|7[0-8]|8\d|9[0-35-9])\d{8}$"
这会更精准,但缺点是可读性差。
匹配所有副词(以 -ly 结尾)
基本模式
text = "He was carefully disguised but captured quickly by police."print(re.findall(r"\w+ly\b", text))
# ['carefully', 'quickly']
解释
\w+
:匹配一个或多个字母/数字/下划线(这里主要是单词)。ly
:以ly
结尾。\b
:单词边界,确保ly
出现在单词末尾(避免匹配applysomething
这种情况)。
注意事项
- 这种方式能匹配大多数副词,但也可能误伤,比如 “silly” 其实是形容词。
- 正则只能做“形式匹配”,没办法理解语法,所以不能百分百正确识别词性。
改进版
如果只想匹配 纯单词,而不是带符号的:
pattern = r"\b[a-zA-Z]+ly\b"
这样就避免匹配到数字或下划线。