【Python】正则表达式
正则表达式
基本概念
什么是正则表达式?
正则表达式(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 re
text = "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 re
text = "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 re
text = "<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 re
pattern = 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"
这样就避免匹配到数字或下划线。
更多推荐



所有评论(0)