影刀RPA 正则表达式实战进阶:分组断言替换的高级用法

0 阅读6分钟

影刀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)
![在这里插入图片描述](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b62050f376544833a1ffa1209c4aa085~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p6X54SxX1JQQUFJ:q75.awebp?rk3s=f64ab15b&x-expires=1786209447&x-signature=Nm9R8ybEJSEZxzF142t0RKlPlek%3D)

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'&nbsp;', ' ', text)
    text = re.sub(r'&amp;', '&', text)
    text = re.sub(r'&lt;', '<', text)
    text = re.sub(r'&gt;', '>', 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-
![在这里插入图片描述](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8b20929d617742e6bda99881b6f2f4da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p6X54SxX1JQQUFJ:q75.awebp?rk3s=f64ab15b&x-expires=1786209447&x-signature=ury9TK%2FuxSZgZCpBR5gShqLCx7A%3D)
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%的文本处理任务一行正则搞定。 在这里插入图片描述