前言
在上一节课中,我们学会了用正则表达式匹配"一类"字符串(比如所有手机号、所有邮箱)。但有时候,我们的需求会更精细:
- 我想匹配邮箱,但我只想要
@前面的用户名,不要后面的域名。 - 我想匹配 html 标签
<h1>标题</h1>,但我必须确保前后的标签名是一致的(不能是<h1>标题</h2>)。 - 我想匹配 “windows 10” 中的 “windows”,但前提是它后面必须跟着 “10”(也就是不匹配 “windows xp”)。
这就需要用到正则的进阶大招:分组 (groups) 和 断言 (assertions)。掌握了它们,你就能像外科手术刀一样精准地切割和处理文本。
本节内容:
- 分组
()与group()提取 - 后向引用
\1:匹配重复出现的文本 re.sub的高级替换技巧- 零宽断言:
(?=...)和(?<=...) - 实战练习:html 解析与数据清洗
一、分组 (grouping):精准提取
1.1 什么是分组?
在正则中,用圆括号 () 包起来的部分就是一个分组。
它的作用主要有两个:
- 整体操作:比如
(ab)+表示匹配 “ab” 重复多次(abab…),而不加括号ab+表示 a 后面跟着多个 b(abbb…)。 - 独立提取:匹配成功后,可以单独获取括号内匹配到的内容。
1.2 使用 group() 获取内容
import re
text = "我的邮箱是 alice@google.com,请联系我。"
# 需求:提取用户名和域名
# 使用两个括号创建两个分组
pattern = r"(\w+)@(\w+\.\w+)"
match = re.search(pattern, text)
if match:
print(match.group()) # alice@google.com (完整匹配)
print(match.group(0)) # 同上
print(match.group(1)) # alice (第一个括号的内容)
print(match.group(2)) # google.com (第二个括号的内容)
print(match.groups()) # ('alice', 'google.com') (所有分组组成的元组)
二、后向引用 (backreference):前后一致
假设我们要匹配 html 标题标签,如 <h1>hello</h1> 或 <h3>hi</h3>。
如果我们写 <h\d>.*</h\d>,它可能会错误地匹配到 <h1>hello</h2>(头尾不一致)。
为了确保结束标签和开始标签匹配,我们需要用到后向引用:\1 代表引用第一个分组匹配到的实际内容。
import re
htmls = ["<h1>valid</h1>", "<h2>wrong</h3>", "<h3>also valid</h3>"]
# <(h\d)> : 第一个分组,匹配 h1-h9
# .*? : 中间内容(非贪婪)
# </\1> : 引用第一个分组匹配到的内容(如果前面是h1,这里必须也是h1)
pattern = r"<(h\d)>.*?</\1>"
for html in htmls:
if re.search(pattern, html):
print(f"匹配成功: {html}")
else:
print(f"匹配失败: {html}")
# 输出:
# 匹配成功: <h1>valid</h1>
# 匹配失败: <h2>wrong</h3>
# 匹配成功: <h3>also valid</h3>
三、re.sub 的高级替换:调整顺序
re.sub(pattern, repl, string) 不仅可以把匹配到的内容替换成固定字符串,还可以调整内容顺序。
在 repl 参数中,使用 \1, \2 来引用分组。
# 需求:将日期格式从 "2026-01-20" 转换为 "01/20/2026" (月/日/年)
text = "today is 2026-01-20."
# 分组1: 年, 分组2: 月, 分组3: 日
pattern = r"(\d{4})-(\d{2})-(\d{2})"
# 替换为:分组2/分组3/分组1
new_text = re.sub(pattern, r"\2/\3/\1", text)
print(new_text)
# today is 01/20/2026.
四、零宽断言 (lookaround):只看不吃
断言是一种特殊的正则,它只进行判断,不消耗字符(即匹配结果中不包含它)。
4.1 正向先行断言(?=...)
含义:匹配 xxx,但前提是 xxx 后面必须跟着 yyy。
语法:xxx(?=yyy)
场景:匹配 “windows 10” 中的 “windows”,但不匹配 “windows xp”。
text = "windows 10 is better than windows xp." # windows(?= 10) -> 查找 windows,要求后面紧跟 " 10" # 注意:结果只返回 "windows",不包含 " 10" matches = re.findall(r"windows(?= 10)", text) print(matches) # ['windows'] (只匹配了第一个)
4.2 正向后行断言(?<=...)
含义:匹配 yyy,但前提是 yyy 前面必须是 xxx。
语法:(?<=xxx)yyy
场景:匹配价格数字,但只匹配带 $ 符号的。如 $99 中的 99,不匹配 item 99。
text = "price: $99, quantity: 99" # (?<=\$)\d+ -> 查找数字,要求前面紧跟 "$" matches = re.findall(r"(?<=\$)\d+", text) print(matches) # ['99'] (只匹配了第一个99)
记忆口诀:
- 先行 (
=):往后看。- 后行 (
<=):往前看(箭头向左)。
五、实战练习
练习1:提取超链接 url
从 html 字符串中提取 href="..." 里面的链接地址。
html = '<a href="https://www.google.com" rel="external nofollow" class="link">google</a>' # 方法1:使用分组提取 # 匹配 href="任意内容" rel="external nofollow" pattern1 = r'href="(.*?)" rel="external nofollow" ' print(re.findall(pattern1, html)) # ['https://www.google.com'] # 方法2:使用断言 (逼格更高,且不需要处理分组) # 前面是 href=",后面是 " rel="external nofollow" pattern2 = r'(?<=href=").*?(?=" rel="external nofollow" )' print(re.findall(pattern2, html)) # ['https://www.google.com']
练习2:密码强度验证
要求:
- 至少 6 位。
- 必须包含至少一个数字。
- 必须包含至少一个字母。
这是一个经典的断言应用场景(同时满足多个条件)。
def check_password(pwd):
# ^ : 开头
# (?=.*\d) : 往后看,必须包含至少一个数字
# (?=.*[a-za-z]): 往后看,必须包含至少一个字母
# .{6,} : 任意字符至少6位
# $ : 结尾
pattern = r"^(?=.*\d)(?=.*[a-za-z]).{6,}$"
return re.match(pattern, pwd) is not none
print(check_password("123456")) # false (没字母)
print(check_password("abcdef")) # false (没数字)
print(check_password("abc12")) # false (太短)
print(check_password("abc1234")) # true
六、常见问题
q1:非捕获分组(?:...)是什么?
有时候我们需要用括号来整体重复 (ab)+,但又不想让它占一个分组编号(不想被 group(1) 取到)。这时可以用 (?:ab)+。
它参与匹配,但不参与提取,效率稍微高一点点。
q2:断言能匹配不定长的内容吗?
python 的 re 模块中,后行断言 (?<=...) 里的内容必须是固定长度的。你不能写 (?<=a*)。但是第三方库 regex 支持不定长。
七、小结

关键要点:
- 想要提取某部分内容?用
()。 - 想要确保前后内容一致?用
\1。 - 想要匹配"位置"而不是"字符"(比如只要数字不要单位)?用 断言。
八、课后作业
- markdown 图片链接提取:markdown 图片格式为
。编写正则,提取出所有的alt文字和url链接。 - 数字千分位:将 “1234567890” 转换为 “1,234,567,890”。提示:使用
re.sub和零宽断言(匹配"空位置",该位置后面紧跟的数字个数是3的倍数)。 - 日志提取:日志格式
[info] 2026-01-20 user login。请提取出:日志级别(info)、日期(2026-01-20) 和 消息(user login)。
总结
到此这篇关于python正则表达式进阶之分组与断言的文章就介绍到这了,更多相关python正则表达式分组与断言内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论