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

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

作者:林焱

什么情况用这个

会用\d+匹配数字、.*匹配任意内容,你的正则入门已经过了。但实际采集数据时,经常遇到这样的需求:从"订单号:ORD20250315001,金额:199元"中只提取订单号和金额(不要前后的文字);从HTML中提取所有链接的href但不包含其他属性;把"张三13800138000"拆成姓名和手机号。

这些场景靠\d+.*解决不了。你需要正则的进阶武器——分组、断言、替换。这三样学好,你能应对90%以上的文本提取和清洗需求。

怎么做

第一步:捕获分组——精确提取你想要的

分组用()包裹,提取时只拿括号内的内容:

importre 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)ifresult: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)ifresult:print(result.group('order_id'))# "ORD20250315001"print(result.group('amount'))# "199.00"# 还可以用字典形式print(result.groupdict())# {'order_id': 'ORD...', 'amount': '199.00'}

第二步:非捕获分组——只做条件不做提取

拼多多店群自动化报活动上架!

有时你需要用括号做条件限制,但不想要这个分组被捕获:

# (?:...) — 非捕获分组# 匹配"19.9元"或"199元",但不捕获"元"text="价格:19.9元,优惠价:15.5元"# 普通分组:捕获了单位result=re.findall(r'([\d.]+)(元)',text)![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/63c1c8014ac34a28b8ea19233ded872e.png#pic_center)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> '''# 非贪婪匹配hrefurls=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"# 替换中使用函数——复杂的转换逻辑defformat_price(match):"""格式化价格:去掉多余的0"""price=float(match.group(1))ifprice==int(price):returnf"¥{int(price)}"returnf"¥{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标签清洗defstrip_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)returntext.strip()

第六步:常见的实战模式

# 1. 提取邮箱emails=re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',text)# 2. 提取URLurls=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第三行"# 默认 . 不匹配 \nprint(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://i-blog.csdnimg.cn/direct/226229b63d1a4e8c9e884e0cb4e8a47c.png#pic_center)blog.csdnimg.cn/asset/1d3c3709da119dd8c13ab01e9b282520/cover/Cover0.jpg)(title-TEMU店群矩阵自动化运营核价报活动)]print(re.findall(r'[\w\u4e00-\u9fa5]+',"用户名user123"))# ['用户名user123']

坑三:后顾断言不支持不定长

现象(?<=订单号:\w*)这种写法报错。

原因:Python的re模块中,后顾断言(?<=...)必须是定长的。

解决:用regex模块代替re(支持不定长后顾),或改用捕获分组:

importregex# 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%的文本处理任务一行正则搞定。