影刀RPA 正则表达式实战进阶:分组断言替换的高级用法
作者:林焱
什么情况用这个
会用 \d+ 匹配数字、.* 匹配任意内容,你的正则入门已经过了。但实际采集数据时,经常遇到这样的需求:从"订单号:ORD20250315001,金额:199元"中只提取订单号和金额(不要前后的文字);从HTML中提取所有链接的href但不包含其他属性;把"张三13800138000"拆成姓名和手机号。
这些场景靠 \d+ 和 .* 解决不了。你需要正则的进阶武器——分组、断言、替换。这三样学好,你能应对90%以上的文本提取和清洗需求。
怎么做
第一步:捕获分组——精确提取你想要的
分组用 () 包裹,提取时只拿括号内的内容:
import re
text = "订单号:ORD20250315001,金额:199.00元"
# 不用分组:只能匹配整个模式
result = re.search(r'订单号:[A-Z]+\d+,金额:[\d.]+元', text)
print(result.group()) # "订单号:ORD20250315001,金额:199.00元" ← 整串
# 用分组:分别提取订单号和金额
result = re.search(r'订单号:([A-Z]+\d+),金额:([\d.]+)元', text)
if result:
order_id = result.group(1) # "ORD20250315001"
amount = result.group(2) # "199.00"
print(f"订单号: {order_id}, 金额: {amount}")
命名分组——数字编号分不清时用名字:
# 命名分组 (?P<name>...)
result = re.search(
r'订单号:(?P<order_id>[A-Z]+\d+),金额:(?P<amount>[\d.]+)元',
text
)
if result:
print(result.group('order_id')) # "ORD20250315001"
print(result.group('amount')) # "199.00"
# 还可以用字典形式
print(result.groupdict()) # {'order_id': 'ORD...', 'amount': '199.00'}
第二步:非捕获分组——只做条件不做提取
[video(video-TdAtxuo4-1784395804441)(type-csdn)(url-live.csdn.net/v/embed/526…)]
有时你需要用括号做条件限制,但不想要这个分组被捕获:
# (?:...) — 非捕获分组
# 匹配"19.9元"或"199元",但不捕获"元"
text = "价格:19.9元,优惠价:15.5元"
# 普通分组:捕获了单位
result = re.findall(r'([\d.]+)(元)', text)

print(result) # [('19.9', '元'), ('15.5', '元')] ← 单位也被捕获了
# 非捕获分组:只捕获价格
result = re.findall(r'([\d.]+)(?:元)', text)
print(result) # ['19.9', '15.5'] ← 只有价格
第三步:贪婪匹配 vs 非贪婪匹配
这是新手最容易写错的地方:
html = "<div>第一个div</div><div>第二个div</div>"
# 贪婪匹配(默认):尽可能多地匹配
result = re.findall(r'<div>(.*)</div>', html)
print(result) # ['第一个div</div><div>第二个div'] ← 不是你想要的!
# 非贪婪匹配(加?):尽可能少地匹配
result = re.findall(r'<div>(.*?)</div>', html)
print(result) # ['第一个div', '第二个div'] ← 对了!
# 规则:.* → 贪婪(多吃);.*? → 非贪婪(少吃)
实战例子——提取所有链接的URL:
html = '''
<a href="/page1" class="link">页面1</a>
<a href="/page2" class="link">页面2</a>
<a href="/page3">页面3</a>
'''
# 非贪婪匹配href
urls = re.findall(r'href="(.*?)"', html)
print(urls) # ['/page1', '/page2', '/page3']
第四步:断言——只看不吃的匹配
断言(lookaround)让你的正则"检查某个条件是否满足但不消耗字符":
# 正向前瞻 (?=...) — 后面跟着什么
# 匹配"元"前面的数字
text = "价格199元,运费10元"
prices = re.findall(r'[\d.]+(?=元)', text)
print(prices) # ['199', '10']
# 正向后顾 (?<=...) — 前面是什么
# 匹配"¥"后面的数字
text = "¥199.00,¥10.00"
prices = re.findall(r'(?<=¥)[\d.]+', text)
print(prices) # ['199.00', '10.00']
# 负向前瞻 (?!...) — 后面不是什么
# 匹配不是"元"结尾的数字
text = "价格199元,数量50件,重量30kg"
nums = re.findall(r'\d+(?!元)', text)
print(nums) # ['19', '50', '30'] ← 199的9也被匹配了
# 负向后顾 (?<!...) — 前面不是什么
# 匹配不在"¥"后面的数字
text = "原价¥199,现价99"
prices = re.findall(r'(?<!¥)\b\d+\b', text)
print(prices) # ['99']
实战:提取中文姓名后的手机号,不提取固话号:
text = "张三13800138000,李四010-12345678,王五13900139000"
# 后顾断言:前面是中文名(2-4个汉字)
phones = re.findall(r'(?<=[\u4e00-\u9fa5]{2,4})\d{11}', text)
print(phones) # ['13800138000', '13900139000']
# 固话010-12345678不符合11位,被排除了
第五步:re.sub的高级替换
# 基础替换
text = "价格:199元"
result = re.sub(r'元', '¥', text) # "价格:199¥"
# 替换时引用分组——用 \1, \2 引用捕获的内容
text = "ORD-2025-0315"
result = re.sub(r'(\w+)-(\d{4})-(\d{4})', r'\1_\2\3', text)
print(result) # "ORD_20250315"
# 替换中使用函数——复杂的转换逻辑
def format_price(match):
"""格式化价格:去掉多余的0"""
price = float(match.group(1))
if price == int(price):
return f"¥{int(price)}"
return f"¥{price}"
text = "价格1:199.00元,价格2:299.50元,价格3:100.00元"
result = re.sub(r'价格\d+:([\d.]+)元', format_price, text)
print(result) # "¥199,¥299.5,¥100"
# 实战:HTML标签清洗
def strip_html(text):
"""去除HTML标签,保留文本"""
# 去除所有标签
text = re.sub(r'<[^>]+>', '', text)
# 替换HTML实体
text = re.sub(r' ', ' ', text)
text = re.sub(r'&', '&', text)
text = re.sub(r'<', '<', text)
text = re.sub(r'>', '>', text)
# 压缩多个空格
text = re.sub(r'\s+', ' ', text)
return text.strip()
第六步:常见的实战模式
# 1. 提取邮箱
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
# 2. 提取URL
urls = re.findall(r'https?://[^\s<>"\']+', text)
# 3. 提取身份证号(18位)
id_cards = re.findall(r'\d{17}[\dXx]', text)
# 4. 提取日期(多种格式)
dates = re.findall(r'\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?', text)
# 5. 提取金额(含货币符号)
amounts = re.findall(r'[¥¥$]\s*[\d,]+\.?\d*', text)
# 6. 分词——提取所有中文字符
chinese = re.findall(r'[\u4e00-\u9fa5]+', text)
# 7. 提取键值对
kv_pattern = r'(\w+)\s*[::=]\s*([^,,\s]+)'
pairs = re.findall(kv_pattern, "name:张三, age:25, city:深圳")
# [('name', '张三'), ('age', '25'), ('city', '深圳')]
有什么坑
坑一:. 不匹配换行符
现象:.* 匹配不到跨行的内容。
解决:加 re.DOTALL 标志:
text = "第一行\n第二行\n第三行"
# 默认 . 不匹配 \n
print(re.findall(r'(.*)', text)) # 只匹配到每行
# re.DOTALL 让 . 匹配包括 \n 的所有字符
print(re.findall(r'第一行(.*)第三行', text, re.DOTALL)) # 匹配到了!
坑二:\w 不匹配中文
# \w 默认只匹配 [a-zA-Z0-9_]
print(re.findall(r'\w+', "用户名user123")) # ['user123'] ← 中文丢了
# 解决:用Unicode属性或明确指定范围
[video(video-n3ps2nMg-1784395810971)(type-csdn)(url-https://live.csdn.net/v/embed/526817)(image-https://v-

blog.csdnimg.cn/asset/1d3c3709da119dd8c13ab01e9b282520/cover/Cover0.jpg)(title-TEMU店群矩阵自动化运营核价报活动)]
print(re.findall(r'[\w\u4e00-\u9fa5]+', "用户名user123")) # ['用户名user123']
坑三:后顾断言不支持不定长
现象:(?<=订单号:\w*) 这种写法报错。
原因:Python的 re 模块中,后顾断言 (?<=...) 必须是定长的。
解决:用 regex 模块代替 re(支持不定长后顾),或改用捕获分组:
import regex # pip install regex
# regex模块支持不定长后顾
result = regex.findall(r'(?<=订单号:\w{3,})\d+', text)
坑四:正则太复杂没法维护
现象:写了一个100个字符的正则表达式,一星期后自己看不懂了。
解决:用 re.VERBOSE 模式写注释:
pattern = re.compile(r"""
(?P<protocol>https?):// # 协议
(?P<domain>[a-zA-Z0-9.-]+) # 域名
(?P<port>:\d+)? # 端口(可选)
(?P<path>/[^\s]*) # 路径
""", re.VERBOSE)
坑五:正则表达式匹配太慢
现象:在大文本上用复杂正则,流程卡住不动。
原因:正则引擎的回溯可能导致指数级时间复杂度。
解决:避免在大量文本上用 .* 再跟一个精确模式;尽量用更精确的前缀缩小匹配范围。
总结:正则从入门到进阶的分水岭就三个:分组(精确提取)、非贪婪(避免过度匹配)、断言(只检查不消费)。把这三个组合起来,90%的文本处理任务一行正则搞定。