🎯 学习目标
- 理解正则表达式的用途,熟练使用 re 模块六大函数
- 掌握元字符、量词、字符集合、边界锚点等核心语法
- 彻底理解贪婪与非贪婪匹配的区别
- 掌握分组捕获、命名分组、反向引用与替换回调
- 会编译正则、使用标志位、处理常见中文场景
- 能写出手机号、邮箱、身份证、URL 等常用模式
1正则表达式是什么
正则表达式(Regular Expression,regex)是用一套符号描述"文本规律"的微型语言,用来在字符串中做查找、提取、替换、校验、切分。
- 校验:手机号格式是否正确
- 提取:从网页/日志中抠出所有邮箱、URL
- 清洗:去掉 HTML 标签、emoji、多余符号
- 切分:按多种分隔符同时切分文本
学习工具推荐:正则可视化 regex101.com(选 Python 方言,实时高亮+解释)。Python 中模式串一律写 原始字符串 r"...",避免反斜杠被 Python 先转义一次。
2re 模块六大核心函数
import re
# search:在文本中【搜索第一个】匹配,返回 Match 对象,找不到返回 None
m = re.search(r"\d+", "订单号 10086 已发货")
if m:
print(m.group()) # 10086
# match:只从字符串【开头】匹配(不常用,记住和 search 的区别即可)
print(re.match(r"\d+", "10086abc")) # <re.Match>
print(re.match(r"\d+", "abc10086")) # None
# fullmatch:整个字符串必须【完全匹配】模式(做校验用)
print(bool(re.fullmatch(r"1[3-9]\d{9}", "13812345678"))) # True
# findall:找出【全部】匹配,返回列表(最常用)
print(re.findall(r"\d+", "a12b34c56")) # ['12','34','56']
# finditer:返回匹配对象的迭代器(大量结果时省内存,可取分组)
for m in re.finditer(r"\d+", "a12b34"):
print(m.group(), m.span()) # span() 返回 (起始,结束) 位置
# sub:替换
re.sub(r"\s+", " ", "a b\tc\nd") # 'a b c d'
# split:切分
re.split(r"[,,;;]", "a,b,c;d;e") # ['a','b','c','d','e']
| 需求 | 函数 | 返回 |
|---|---|---|
| 是否包含 / 取第一个 | search | Match 或 None |
| 整体格式校验 | fullmatch | Match 或 None |
| 全部提取 | findall | str 列表 |
| 大量结果且要分组 | finditer | Match 迭代器 |
| 替换 | sub | 新字符串 |
| 智能切分 | split | 列表 |
判断"是否匹配"永远用 if re.search(...):,不要用 if re.findall(...):(找全量浪费性能),也不要写成 if re.search(...) == True。
3正则语法速查表
3.1 字符与字符类
| 写法 | 含义 | 例子 |
|---|---|---|
. | 任意单个字符(不含换行) | a.c 匹配 abc、a-c |
\d | 数字 [0-9] | \d+ |
\D | 非数字 | |
\w | 字母数字下划线(Python3 默认含汉字!) | |
\W | 非单词字符 | |
\s | 空白(空格、制表、换行等) | |
\S | 非空白 | |
[abc] | 字符集合:a 或 b 或 c | [aeiou] 元音 |
[^abc] | 取反:不是 a/b/c 的任意字符 | |
[a-z] | 范围:小写字母 | [a-zA-Z0-9_] |
[\u4e00-\u9fa5] | 常用汉字范围 | NLP 高频 |
3.2 量词(重复次数)
| 写法 | 次数 |
|---|---|
* | 0 次或多次 |
+ | 1 次或多次(至少 1 次) |
? | 0 次或 1 次(可选) |
{n} | 恰好 n 次 |
{n,} | 至少 n 次 |
{n,m} | n 到 m 次 |
3.3 边界与逻辑
| 写法 | 含义 |
|---|---|
^ | 字符串开头(多行模式下为行首) |
$ | 字符串结尾 |
\b | 单词边界(\w 与非 \w 的交界处) |
| | 或:cat|dog |
(...) | 分组:整体控制 + 捕获内容 |
(?:...) | 只分组不捕获 |
(?P<name>...) | 命名分组 |
# 转义:元字符要匹配其字面含义时前面加 \
re.findall(r"\.", "a.b.c") # 匹配真正的点
re.findall(r"\\", r"C:\dir") # 匹配真正的反斜杠
re.findall(r"[.+*?]", "a+b.c?") # 在字符类[]中,大多数字符失去特殊含义
4贪婪与非贪婪(核心难点)
* + {n,m} 默认都是贪婪的:尽可能多匹配,再通过回溯满足整体模式;在其后加 ? 变为非贪婪(懒惰):尽可能少匹配。
html = '<h1>标题</h1><p>段落</p>'
# 贪婪:.* 一口气吃到最后一个 </...>,再回溯
re.findall(r"<.+>", html)
# ['<h1>标题</h1><p>段落</p>'] ← 整个一坨,不是想要的!
# 非贪婪:.+? 匹配到第一个 > 就停
re.findall(r"<.+?>", html)
# ['<h1>', '</h1>', '<p>', '</p>']
# 经典场景:提取标签内文本
re.findall(r"<h1>(.*?)</h1>", html) # ['标题']
规则:跨字符提取内容时,默认先想非贪婪 .*?;而 [^字符]*(否定字符类)是比非贪婪更高效的替代,如提取双引号内容 "([^"]*)"。
5分组与捕获
# 分组让量词作用于整体,并把括号内内容单独"捕获"出来
re.findall(r"(ab)+", "ababab") # ['ab'](整体只捕获最后一次,注意)
# findall 中有【一个】捕获组时,只返回组内内容;有多个组时返回元组列表
text = "张三:90, 李四:85"
re.findall(r"(\w+):(\d+)", text)
# [('张三','90'), ('李四','85')]
# search + group:按编号取分组(1,2,3...,group(0) 是整个匹配)
m = re.search(r"(\w+):(\d+)", text)
m.group(0) # '张三:90' 完整匹配
m.group(1) # '张三'
m.group(2) # '90'
m.groups() # ('张三','90')
# 命名分组:可读性更好
m = re.search(r"(?P<name>\w+):(?P<score>\d+)", text)
m.group("name") # '张三'
m.groupdict() # {'name':'张三','score':'90'}
# 非捕获组 (?:...):只用来分组,不占编号
re.findall(r"(?:https?://)?([\w.]+)", "http://x.com")
反向引用与替换回调
# 反向引用:\1 表示第1组已匹配到的内容(找重复词)
re.findall(r"\b(\w+)\s+\1\b", "hello hello world", re.I) # ['hello']
# sub 中用 \1 \g<name> 引用分组(给手机号打码)
re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", "13812345678")
# '138****5678'
# sub 支持用函数处理每个匹配(脱敏金额翻倍等)
def mask(m):
return m.group(1) + "*" * len(m.group(2))
re.sub(r"(\w{1})(\w+)", mask, "张三和李四") # '张*和李*'
6标志位 flags
re.IGNORECASE / re.I # 忽略大小写
re.DOTALL / re.S # 让 . 也能匹配换行符(提取多行 HTML 常用)
re.MULTILINE / re.M # ^ $ 按行匹配
re.VERBOSE / re.X # 模式中可写空白和注释(复杂正则用)
re.findall(r"python", "Python PYTHON", re.I) # ['Python','PYTHON']
re.search(r"<div>(.*?)</div>", "<div>a\nb</div>", re.S) # 跨行提取
# 多个标志用 | 组合:flags=re.I | re.S
7编译复用与性能
# 同一个模式要在循环/大量文本中反复使用,先编译成对象更快
pattern = re.compile(r"\d+")
pattern.findall("a12b34") # 用法与 re.函数 完全一样
pattern.sub("#", "a12b")
# 预编译放模块顶层(全局只编译一次)
PHONE_RE = re.compile(r"1[3-9]\d{9}")
def has_phone(text):
return bool(PHONE_RE.search(text))
8实战案例库
① 手机号 / 邮箱 / 身份证 / URL
patterns = {
"手机号": r"1[3-9]\d{9}",
"邮箱": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
"身份证": r"\d{17}[\dXx]",
"URL": r"https?://[^\s一-龥]+",
"日期": r"\d{4}-\d{1,2}-\d{1,2}",
"IP地址": r"(?:\d{1,3}\.){3}\d{1,3}",
}
text = "联系我 13812345678 或 a.b@qq.com,主页 https://demo.com"
for name, p in patterns.items():
print(name, re.findall(p, text))
手机号 ['13812345678']
邮箱 ['a.b@qq.com']
URL ['https://demo.com']
② 文本清洗(NLP 预处理最常用)
import re
raw = "<p>Python 真 好用!!!</p>\n访问 https://x.com 或致电13800000000"
text = re.sub(r"<[^>]+>", "", raw) # 1. 去 HTML 标签
text = re.sub(r"https?://\S+", "", text) # 2. 去网址
text = re.sub(r"1[3-9]\d{9}", "[PHONE]", text)# 3. 手机号归一化
text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", "", text) # 4. 只保留中英文数字
text = re.sub(r"\s+", " ", text).strip() # 5. 压缩空白
print(text) # Python真好用访问或致电PHONE
③ 从日志中提取结构化信息
log = '2026-10-25 12:00:01 INFO 用户 admin 登录成功 ip=192.168.1.5'
row = re.search(
r"(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+"
r"(?P<level>\w+)\s+用户\s+(?P<user>\w+).*ip=(?P<ip>[\d.]+)",
log)
print(row.groupdict())
# {'time':'2026-10-25 12:00:01','level':'INFO','user':'admin','ip':'192.168.1.5'}
④ 抽取所有中文字词 / 英文单词
text = "我爱 NLP,也爱 Python3.12"
print(re.findall(r"[\u4e00-\u9fa5]+", text)) # ['我爱','也爱'] 连续中文
print(re.findall(r"[a-zA-Z]+", text)) # ['NLP','Python']
print(re.findall(r"[a-zA-Z]+|\d+", text)) # 英文词或数字
正则是强大的通用工具,但处理中文语义(分词、断句)应交给专业库——这正是 NLP 篇 jieba 等工具要解决的问题。正则适合规则明确的模式,不要用它解析完整 HTML(用 BeautifulSoup/lxml),也不要用它做中文分词。
9本章小结与练习
📌 关键结论
- 判断存在用 search、校验整体用 fullmatch、全提取用 findall、替换用 sub。
- 模式串一律 r"";量词默认贪婪,提内容用
.*?或否定字符类。 - findall 有捕获组时返回组内容;命名分组
(?P<n>...)配合 groupdict 最清晰。 - 反复使用先
re.compile;中文匹配[\u4e00-\u9fa5]。
练习1:写出匹配并提取字符串中所有 QQ 邮箱(@qq.com 之前是 5~11 位数字)的代码。
re.findall(r"(\d{5,11})@qq\.com", text)。注意点号要转义 \.,{5,11} 限定长度,捕获组只取 QQ 号;若需完整邮箱去掉外层括号。
练习2:为什么 re.findall(r"<div>(.*)</div>", "<div>a</div>x<div>b</div>") 结果出乎意料?怎么改?
.* 贪婪,会从第一个 <div> 一直吃到最后一个 </div>,结果是 'a</div>x<div>b' 一整段。改为非贪婪 <div>(.*?)</div>,即可分别得到 ['a','b']。跨行文本再加 flags=re.S。
练习3:把文本中所有连续的空白(空格/制表/换行)压缩为一个空格并去除首尾空白。
re.sub(r"\s+", " ", text).strip()。\s 匹配任意空白,+ 表示一段连续空白统一替换成单个普通空格,这是 NLP 清洗的标准操作之一。