干Python这些年,要说哪项技能性价比最高,我一定会把正则表达式排在前三。你写爬虫要清洗HTML、做数据分析要处理脏数据、维护Linux服务器要翻日志,说到底都是在和字符串打交道。正则表达式的厉害之处在于,它可以让你用一小段模式描述,把原本要写几十行循环和判断的活儿一次性做完,而且做出来的代码还更好读、更好维护。
这篇文章是我自己从入门到应用正则的真实总结,先从语法核心说起,再到Python的re模块,最后用几个实际项目案例把文本处理串起来。其中很多细节是我踩过坑之后才明白的,比如match和search的区别、贪婪匹配如何坑人、findall遇到分组会返回什么,这些在官方文档里其实都有写,但不结合场景很容易忽略。这篇内容比较适合想系统学正则的Python新手,也适合写了不少年代码、但每次看到正则就头大的老手。
1. 正则表达式的核心原理与设计思路
1.1 正则的本质:不是查找,而是模式描述
很多人第一次接触正则,会把它理解成“高级查找功能”,这个方向对了一半。普通查找,比如字符串自带的find、index,解决的是“某个固定文本在不在、在哪”的问题;正则解决的是“某种形状的文本在不在、在哪”的问题。
我举个例子你就明白了。假设你在一本很厚的通讯录里找自己的手机号,直接搜“13800138000”没问题,但如果你要找出所有以1开头、总共11位数字的号码,常规查找就无能为力了。这时候你用r"1\d{10}",一次就能把符合条件的号码全部拎出来。
正则的“模式描述”思路,本质上就是把人对文本的模糊感知,变成一种精确、可计算的表达。跟翻通讯录这个例子一样,正则匹配的过程也是逐字符推进、不断尝试的。理解到这一层,后面学元字符、学量词、学分组就不会觉得是一堆死记硬背的符号,而是有一个贯穿始终的逻辑在那里。
所以在动手写正则之前,先养成一个习惯:先问自己“我要匹配的文本,在形状上有什么共同特征”,再动手写模式。这个思维习惯,比背一百个语法细节都管用。
1.2 字符、量词、位置:三个最基础的构建块
正则语法看起来多,拆开其实就是三类东西:字符、量词、位置。
字符类决定“这一个位置允许出现什么”。\d代表数字,\w代表字母数字下划线,\s代表空白字符。方括号[abc]表示匹配其中任意一个字符,[a-z]表示a到z任意一个。反义写法就是大写字母,\D表示非数字,[^abc]表示除了abc以外的字符。
量词决定“前面的字符出现多少次”。*出现0次或多次,+出现1次或多次,?出现0次或1次。想精确控制就用花括号,{3}恰好3次,{2,5}2到5次,{2,}至少2次。这个设计很像自然语言里的“很多”“几个”“一两个”,但它是精确的数量约束。
位置锚定则是一种特殊的存在,它不匹配具体字符,只匹配位置。^匹配字符串开头,$匹配结尾,\b匹配单词边界。比如你要在一大段英文里找完整单词“cat”,用r"cat"会把“category”里的“cat”也揪出来,用r"\bcat\b"就只会匹配独立的“cat”。
我把常用的元字符整理成了一张速查表,建议你直接存在笔记里,实际写的时候拿出来对照。
| 类型 | 语法 | 含义 | 示例 |
|---|---|---|---|
| 字符类 | \d | 数字 | \d{4}匹配4位数字 |
| 字符类 | \w | 字母数字下划线 | \w+匹配一个单词 |
| 字符类 | \s | 空白字符 | \s+匹配连续空格 |
| 字符类 | [^x] | 除了x以外的任意字符 | [^0-9]匹配非数字 |
| 量词 | * | 0次或多次 | ab*c匹配“ac”或“abc” |
| 量词 | + | 1次或多次 | \d+匹配至少一个数字 |
| 量词 | ? | 0次或1次 | colou?r匹配“color”或“colour” |
| 量词 | {m,n} | m到n次 | \d{3,5}匹配3到5位数字 |
| 位置 | ^/$ | 开头 / 结尾 | ^python匹配以python开头 |
| 位置 | \b | 单词边界 | \bcat\b匹配独立单词cat |
| 分支 | | | 或者 | cat|dog匹配cat或dog |
需要特别提醒的是元字符的转义问题。点号.在正则有特殊含义(匹配任意字符),如果你想匹配字面上的“.”,必须写成\.。这是一半以上新手报错“匹配不到”的根源。同理,想匹配*、+、?、(、)、[、]、\,全部要加反斜杠。Python字符串里反斜杠本身又是转义符,所以我强烈建议正则一律写成原始字符串,也就是r"\.txt",这样反斜杠不会被Python解释器吃掉。
1.3 为什么文本处理项目离不开正则
我复盘过自己经手的各种Python项目,只要涉及文本,几乎都能看到正则的影子。
爬虫场景最典型。从网页上抓下来的价格、评论数、发布时间,经常掺杂HTML标签、空白字符、货币符号。用re.sub(r"<[^>]+>", "", html)可以快速剥掉标签,用re.search(r"价格[::]?\s*(\d+\.?\d*)", text)能把价格数字提取出来。没有正则,这堆清洗逻辑写起来就是一个接一个replace,又啰嗦又脆弱。
日志分析场景同样如此。服务器产生的每一条日志都遵循一定格式,但具体数值千变万化。用正则把时间、级别、接口、耗时等字段一次性提取出来,后面不管是做统计、告警还是画趋势图,就都顺理成章了。
还有数据脱敏。给测试库导入真实数据前,手机号、身份证号、邮箱必须脱敏。re.sub(r"(1[3-9]\d)\d{4}(\d{4})", r"\1****\2", phone)一行代码,把中间四位替换成星号。
再说个实在的,Python的re模块是标准库自带模块,不需要pip安装,不需要配置环境,装了Python就能用。这一点也让它成了文本处理场景里最没有使用门槛的工具。
2. Python re模块核心细节与实操要点
2.1 match、search、findall、sub 四个函数的使用边界
re模块的核心函数就四个,但很多人用混过。我按自己的理解把它们讲透。
re.match(pattern, string)只从字符串的起始位置尝试匹配。哪怕模式本身能在字符串后面匹配到,match也会因为开头对不上而返回None。这就导致了一个经典误用:有人想在一大段文本里搜某个关键词,用了match,结果怎么都搜不到,代码看起来又没问题,排查半天。
re.search(pattern, string)会在整个字符串中扫描,找到第一个能匹配的位置。这是我日常用得最多的入口函数。它返回一个Match对象,你可以通过group()方法取匹配结果,通过span()方法拿位置。
re.findall(pattern, string)返回所有非重叠匹配的列表。这一点要注意的是,当模式里包含分组(即括号)时,findall的返回结构会变:如果有一个分组,返回的是分组内容的列表;如果有多个分组,返回的是元组的列表。这个行为跟search不一样,很多人第一次遇到会懵。
re.sub(pattern, repl, string)是替换函数,把字符串中所有匹配正则的子串替换为repl。repl里可以用\1、\2引用分组捕获的内容,这使得它不只是简单的文本替换,更像是一个“按规则重写字符串”的工具。
四个函数的分工可以这样记:想验证开头用match,想找第一次出现用search,想把所有结果都捞出来用findall,想改文本用sub。
配合一个实际例子来看:
import re text = "我的手机号是13800138000,备用号15912345678" result = re.findall(r"1[3-9]\d{9}", text) print(result) # ['13800138000', '15912345678'] m = re.search(r"1[3-9]\d{9}", text) print(m.group()) # 13800138000 print(m.span()) # (6, 17) new_text = re.sub(r"1[3-9]\d{9}", "手机号已隐藏", text) print(new_text) # 我的手机号是手机号已隐藏,备用号手机号已隐藏2.2 预编译、分组与命名分组实战
我早年写正则,喜欢在一个函数里现写现用,后来发现问题很大——同一个模式在循环里被重复编译,不仅慢,而且模式一长就完全没法看。后来我养成了习惯,凡是会被多次使用的正则,一律先编译成Pattern对象:
import re log_pattern = re.compile( r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+(\S+)\s+(.+?)(\d+)ms" )编译之后,pattern.search()、pattern.findall()、pattern.sub() 都能直接用,参数里不需要再传正则字符串。代码可读性提升一个档次,性能也有微小但实打实的提升,尤其在多次调用的场景下。
分组是正则里最强大的特性之一。用括号把模式的一部分包起来,结果就会被单独捕获。捕获之后,既可以用group(n)按编号取,也可以用命名分组之后按名字取。命名分组的语法是(?P<name>...),Python的官方写法,其他语言里写成(?<name>...),注意别串了。
我拿解析日期时间的例子说明。有一串文本“今天是2025-03-06下午14:22”,我要提取日期和时间,并且分开处理:
import re text = "今天是2025-03-06下午14:22" pattern = re.compile(r"(?P<date>\d{4}-\d{2}-\d{2}).*?(?P<time>\d{2}:\d{2})") m = pattern.search(text) if m: print(m.group("date")) # 2025-03-06 print(m.group("time")) # 14:22命名分组的可读性优势在复杂正则里尤其明显。你不需要去数第几个括号是哪个字段,直接按名字引用。配合后面要讲的反向引用,能玩出很多花活,比如查找重复出现的单词:r"\b(\w+)\s+\1\b"就能匹配“hello hello”这种重复。
还有一种场景很实用:分组配合sub做格式重构。比如把美国的日期格式“03/06/2025”转成ISO格式“2025-03-06”:
import re text = "event on 03/06/2025" print(re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\1-\2", text)) # event on 2025-03-062.3 匹配标志与贪婪陷阱
re模块有一些匹配标志,它们会改变整个匹配行为,而且不显式写在正则字符串里,容易被人忽略。
re.I(IGNORECASE)让匹配忽略大小写;re.S(DOTALL)让点号.能匹配换行符;re.M(MULTILINE)让^和$在每一行的开头和结尾生效,而不仅仅是整个字符串的开头和结尾。
最容易被忽略的是re.S。默认情况下点号不匹配换行,这意味着你要匹配一个跨行的文本块时会失败。我见过有人在解析HTML时用r"<div class='content'>(.*?)</div>"去抓一个跨多行的内容块,结果总是返回空,就是因为内容里有换行。加上re.S就好了。
贪婪和懒惰是另一个高频坑点。默认情况下,量词是贪婪的,也就是会尽可能多地匹配。比如文本是<a>1</a><a>2</a>,你用r"<a>.*</a>"去匹配,得到的是整条<a>1</a><a>2</a>,而不是第一个<a>1</a>。
如果只想匹配到第一个出现的位置,就要用懒惰写法,在量词后面加一个问号:.*?。这也是我在实战里几乎标配的写法——抓HTML片段、日志字段、JSON里的文本,一律.*?起步。
说一个我印象深刻的调试经历。有一次写爬虫解析一个页面,产品列表总长度对不上,后来发现某个产品的描述里包含了“ ”这种字面文本,贪婪匹配把后续好几个产品块全吞进去了。换成懒惰匹配之后,问题立刻消失。从那以后,我在写面向未知内容的正则时,默认都会考虑懒惰写法,除非我明确知道目标内容的结构。
别忘了还有零宽断言,这个属于进阶技巧:(?=...)是向前断言,表示后面跟着什么;(?!...)是负向向前断言,表示后面不跟着什么。比如从“price: 199"里提取数字且不包含冒号,可以写r"(?<=price: )\d+"。这类断言在很多复杂场景里能省不少事,但新手阶段可以先不用强求。
3. 文本处理实战:从清洗到结构化
3.1 案例:日志接口耗时统计
这个案例我几乎每次给人讲正则都会用,因为太能说明“模式提取”的价值了。
假设你有一个日志文件,每行长这样:
2025-03-06 14:22:31 INFO com.example.api.OrderController getOrderById 132ms 2025-03-06 14:22:33 WARN com.example.api.UserController getUserInfo 89ms 2025-03-06 14:22:35 ERROR com.example.api.PayController payOrder 1203ms现在要统计每个接口的平均耗时,找出最慢的接口。用正则把每行的字段提取出来:
import re from collections import defaultdict pattern = re.compile( r"(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+" r"(?P<level>INFO|WARN|ERROR)\s+" r"(?P<class>\S+)\s+" r"(?P<method>\S+)\s+" r"(?P<cost>\d+)ms" ) costs = defaultdict(list) with open("app.log", "r", encoding="utf-8") as f: for line in f: m = pattern.search(line) if m: api = f"{m.group('class')}.{m.group('method')}" costs[api].append(int(m.group("cost"))) for api, values in sorted(costs.items(), key=lambda x: -sum(x[1]) / len(x[1]))[:10]: avg_cost = sum(values) / len(values) print(f"{api}: avg {avg_cost:.1f}ms, count {len(values)}")注意我在正则里用了verbose风格的多行字符串拼接,这样做不仅仅是为了好看,更重要的原因是让每个字段名一目了然。以后日志格式变了,要加字段或者改分隔符,你只需要改这一行模式,不需要在几十行的解析代码里来回找。
这里还有一个实战经验:能不开文件就不开文件,能用生成器就不全量load。日志文件有时候几百MB,一次性readlines会吃掉大量内存。上面这种逐行读取、逐行匹配的方式,内存占用非常稳定。
3.2 案例:从HTML中提取商品价格
爬虫场景里,正则最常用来做页面数据的快速提取。但这个场景有两面性——结构简单可控的模板,正则真的是又快又爽;结构复杂或不信任的页面,我更建议用BeautifulSoup这类专门工具,硬用正则解析HTML风险极高。
我把安全边界说清楚:如果你要处理的是自己生成的HTML片段、或者来源相对固定的接口页面,正则完全够用;如果你面对的是整个互联网上任意结构的HTML,别用正则,真的会崩溃。
举个例子,我要从一段HTML里提取商品价格:
import re html = ''' <div class="product"> <span class="name">无线鼠标</span> <span class="price">199.00</span> </div> <div class="product"> <span class="name">机械键盘</span> <span class="price">449.90</span> </div> ''' pattern = re.compile( r'<span class="name">(?P<name>[^<]+)</span>\s*' r'<span class="price">(?P<price>\d+\.\d{2})</span>' ) for m in pattern.finditer(html): name = m.group("name").strip() price = float(m.group("price")) print(f"{name}: {price:.2f}元")这里我特意用了[^<]+而不是.*?,原因是[^<]+明确表示“不包含尖括号的连续字符”,在HTML片段里它天然不会跨过标签边界,比.*?更稳。这个小细节在处理嵌套内容时能省掉很多麻烦。
不过我还是想强调:如果HTML属性顺序变化、标签里多了样式属性,这个正则马上会失效。比如<span id="price" class="price">,上面这个写法就匹配不到。所以写这种代码,要在注释里明确标注“依赖页面结构稳定,改版后需要同步更新正则”,否则三个月后页面改了一次版,你的爬虫会悄悄返回空数据,而且没有任何报错提示。
3.3 案例:批量文件名归一化
文件名的处理是个看着不起眼、实际非常磨人的活儿。下载下来一堆文件,命名规则五花八门:有的带日期、有的带“final”、有的带版本号、有的纯乱来。用正则做归一化,再配合os模块重命名,能在一分钟内让整个目录变得整齐。
假设有一批文件:
2024_01_report_final_v2.pdf 2024_03_report_final_v3.pdf 2024_06_report_final_v4.pdf我想把它们改成统一格式:报告-2024-01-最终版-2.pdf。
import os import re pattern = re.compile(r"(\d{4})_(\d{2})_report_final_v(\d+)\.pdf") for filename in os.listdir("downloads"): if filename.endswith(".pdf"): m = pattern.match(filename) if m: year, month, version = m.group(1), m.group(2), m.group(3) new_name = f"报告-{year}-{month}-最终版-{version}.pdf" old_path = os.path.join("downloads", filename) new_path = os.path.join("downloads", new_name) # 先打印预览,确认无误后再真正重命名 print(f"{filename} -> {new_name}") # os.rename(old_path, new_path)这里有个我踩过很多次的坑:批量修改文件前,千万先打印预览,确认每一条转换结果都符合预期,再真正执行os.rename。有些人图省事直接跑,结果等到文件全被重命名才发现日期和版本号搞错了位置,再想恢复就非常痛苦。
正则里我用了\.pdf而不是.pdf,就是因为点号有特殊含义,不转义的话会把“apdf”“xpdf”这种鬼名字也匹配上。这个错误很隐蔽,因为绝大多数文件名恰好不会触发,于是你以为自己写对了,直到某天遇到一个怪名字才炸出来。
3.4 案例:敏感信息脱敏
现在做开发,几乎绕不开数据脱敏。测试环境要用接近真实的数据,但真实数据里的手机号、身份证号、邮箱又不能明文出现。正则处理这种“部分保留、部分打码”的操作非常顺手。
手机号脱敏,保留前3位和后4位:
import re phone = "13800138000" masked = re.sub(r"(1[3-9]\d)\d{4}(\d{4})", r"\1****\2", phone) print(masked) # 138****8000身份证号脱敏,保留前6位和后4位:
id_card = "110101199001011234" masked = re.sub(r"(\d{6})\d{8}(\d{4})", r"\1********\2", id_card) print(masked) # 110101********1234邮箱脱敏,保留第一个字符和域名部分:
email = "zhang.san@example.com" masked = re.sub(r"(\w)(\w+)@(\w+\.\w+)", r"\1***@\3", email) print(masked) # z***@example.com写这类正则的注意事项是:脱敏规则要写在正则注释里,并且要考虑边界情况。比如手机号可能是86开头、可能带空格、可能是国际号码,一概而论地写1[3-9]\d{9}会漏掉很多情况。如果你的数据源本身格式统一,这样没问题;如果格式多样,建议先做格式化统一,再进脱敏流程。
我还习惯把脱敏函数封装成一个独立模块,统一管理正则模式,方便后续维护和审计。一旦脱敏规则有变化,只需在一个文件里改几行,而不是满项目搜“138”这种硬编码。
4. 常见问题与排查技巧实录
4.1 新手必踩的五个坑
我平时帮别人review代码,看到最多的正则问题就是下面这几种,列成清单,你可以对照检查自己有没有踩过。
第一个坑:match和search不分。想全文本搜索,却用了re.match,导致匹配结果永远是None。我建议默认用search,只有当你明确知道要从字符串开头匹配时再选match。
第二个坑:忘了加r前缀。re.sub("\d+", "", text)和re.sub(r"\d+", "", text)看起来只差一个r,实际行为完全不一样。没有r前缀的时候,Python会把\d当作普通字符d来处理,匹配结果天差地别。我这几年至少见过十个项目里因为这个导致正则“失灵”。
第三个坑:点号不能匹配换行。处理多行文本时默认失败,必须在编译时传入re.S,或者把换行符显式写进字符类[.\s\S]这种写法里。
第四个坑:贪婪量词吞掉太多内容。用.*去匹配标签,结果把整段文本都吞了。遇到“匹配到第一次出现为止”的需求,一定要用.*?。
第五坑:findall和分组叠加后返回数据变了。这是我最常被问到的“为什么findall返回的是元组不是字符串”问题。当模式里有多个括号时findall把每组捕获结果打包成元组返回,这在设计上是合理的,但新手很容易被这个变化吓到。
这五个坑基本覆盖了90%的新手报错场景。我整理的时候特意没有放那种看一眼报错就明白的语法错误,而是选了这些在逻辑上正确、但结果不符合预期的“隐形坑”,因为它们最难排查。
4.2 四步排查法:从报错到定位
正则出错时,最忌讳的是盯着报错信息反复猜。我写过一套自己的排查流程,基本能覆盖绝大多数情况。
第一步,最小样例验证。取一段包含目标文本的短字符串,把正则单独丢进去跑,排除业务逻辑干扰。很多时候问题出在正则在长文本、特殊字符干扰下表现不同,缩减样例会快速定位是模式本身的问题,还是数据的问题。
第二步,在线调试工具辅助。我用过几个正则调试网站,支持实时高亮匹配结果。这比自己写print观察快得多。当然,如果涉及敏感数据,我不会把真实数据贴进去,会先用脱敏后的样例。
第三步,化繁为简逐步验证。一个长正则拆成几个短正则,分别用search或findall跑,确认每部分都正常后,再逐步组装回去。比如我先验证时间的\d{4}-\d{2}-\d{2}、再验证IP的\d+\.\d+\.\d+\.\d+,分开都对,合起来才有讨论价值。
第四步,verbose模式加注释。对于复杂的正则,编译时加上re.X,允许你写注释和换行。正则本身变成多行可读的结构,下次接手的人能快速看懂每一段在匹配什么,排错成本直线下降。
pattern = re.compile( r""" (?P<ip>\d+\.\d+\.\d+\.\d+) # 客户端IP \s+ (?P<time>\d{2}:\d{2}:\d{2}) # 请求时间 """, re.X )这一套流程下来,九成以上正则问题都能在半小时内定位。剩下的那一成,大多不是“正则语法错误”,而是“模式本身思路上就不成立”,比如试图用正则解析有递归结构的JSON或HTML,这种时候要果断换工具,而不是硬撑。
4.3 性能与安全防线:别被正则拖垮
正则不是小心用就没问题,有些模式性能会差到让人怀疑人生,甚至成为安全隐患。
最典型的性能杀手是灾难性回溯。当你写了一个类似(a+)+$的模式,去匹配一个很长的、末尾没有期待字符的字符串时,引擎会不断尝试各种分组组合,时间呈指数级增长。你可能觉得这只是一个简单的匹配,实际上CPU已经烧到90%了。
这类问题在开发环境不明显,一旦部署到线上、碰到恶意构造的输入,直接就是故障。防范思路有几个:第一,尽量避免嵌套量词,比如(a+)+、(\d*)*这种组合,能拆就拆开;第二,能用固定次数{m,n}就不要用无限量词*、+;第三,匹配完立即止损,Python标准库的re模块不直接支持超时,如果确实要处理不可信输入且模式复杂,可以考虑第三方regex模块,它提供了超时参数。
实战里,我还有一个习惯:编写正则时顺手估算一下最坏情况下要回溯多少次。如果模式里同时出现了多个.*,而且数据可能很长,我基本能预判它性能不会好。这时候与其绞尽脑汁优化回溯,不如先把问题拆小,比如先切分文本再匹配。
5. 正则的生态联动:从Linux命令到pandas
5.1 在Linux命令行里用正则快速排查
正则不止属于Python,Linux命令行工具本身就是正则的重度使用者,几个最常用的命令值得掌握。
grep -E用扩展正则搜索文件。比如从access.log里找出所有5xx状态码的请求:grep -E "HTTP/1.1\" [5][0-9][0-9]" access.log。这个操作在排查线上问题时是秒级响应,不需要写脚本。
sed -E做流式文本替换。比如要把日志里的时间戳从2025-03-06 14:22:31改成03/06/2025 14:22:31,可以写成sed -E "s/([0-9]{4})-([0-9]{2})-([0-9]{2})/\2\/\1\/\3/" app.log,这跟你用Python的re.sub写的模式逻辑完全一致。
awk则更适合按列做文本分析。它把每行按分隔符切分成字段,配合正则可以做一些很有意思的操作,比如统计状态码为200的接口的响应时间均值。
这里想提醒一个细节:Linux基础正则和扩展正则在字符转义上有些区别,比如{1,3}在基础正则里要写成\{1,3\},而grep -E或egrep直接写{1,3}就行。如果你在命令行里写了看起来正确的正则在Python里通了,但grep里不通,多半是正则口味不同的问题,不一定是模式逻辑错了。
我处理超大日志的常用流程是:先命令行里用grep快速把可疑行捞出来,再用Python脚本做精细化解析和统计。命令行工具的优势是快、无需写代码、内存友好,但复杂逻辑还是Python更擅长。两者配合可以说是文本处理的最佳拍档。
5.2 pandas中基于正则的数据清洗
数据分析里,pandas处理表格数据很顺手,而它的字符串方法背后本身支持正则。用好了,数据清洗环节能从“写循环”变成“一行代码”。
最常见的几个操作:str.contains筛选包含特定模式的记录,str.extract从文本列中提取结构化字段,str.replace清洗脏数据。
举个例子。有一列销售数据,值五花八门,有“价格:199.00元”“¥199.00”“199元含税”等等,我想统一提取出数值:
import pandas as pd df = pd.DataFrame({"raw_price": ["价格:199.00元", "¥199.00", "199元含税", "无报价"]}) # 提取数字和小数点部分 df["price"] = df["raw_price"].str.extract(r"(\d+\.?\d*)") df["price"] = pd.to_numeric(df["price"], errors="coerce")str.extract会把括号捕获的内容提取成新列,没匹配到的记录变成NaN。如果你想一步到位把非数字字符全部去掉,可以这样:
df["price_clean"] = df["raw_price"].str.replace(r"[^\d.]", "", regex=True)这样得到的是“199.00”这类纯数字字符串,然后再做类型转换。注意新版pandas里str.replace默认不启用正则模式,需要显式加上regex=True,这个版本差异很多人容易踩。
与此相关的一个思路是:pandas的向量化正则匹配会比逐行调用Python的re模块快很多,因为底层是向量化执行的。所以当你的数据量达到十万行级,清洗逻辑能用pandas自带的str.extract就不要手写循环加re.search。
说到pandas,就不得不提它和正则结合的一个经典场景——爬虫结果的清洗。爬虫抓下来一堆结构半乱的数据,先落到DataFrame,用str.extract把目标字段(价格、时间、手机号)全部提取出来,再统一清洗和类型转换,整个链路非常流畅。这比在爬虫解析阶段就面面俱到地处理所有异常要省心得多。
写到最后,分享一点我自己的体会:正则这个东西,刚开始背语法没用,真正会用是在调试过程中把一个个案例磨出来的。我见过很多团队一遇到字符串处理就写一串又臭又长的if-else,其实换个思路用正则三五行就解决;也见过有人把正则写得极其复杂,结果代码连他自己都看不懂,出了问题两小时定位不了。
我的建议很简单:遇到文本处理任务,先停下来想一想能不能用正则;写正则时尽量短小、可读、有注释;拿不准的时候,先写一个最小样例再继续。如果你能把这篇文章里的案例跟着敲一遍,再回到自己的项目里动手改一改,正则这门手艺基本就长在你身上了。之后再做爬虫清洗、日志统计、数据脱敏,你会发现那些曾经让你头疼的脏文本,其实都很听话。