1. 项目概述:为什么字符串清洗是数据工程师每天都在做的“隐形体力活”
你有没有遇到过这样的场景:从网页爬下来的一堆商品价格,混着“¥”“$”“USD”“折后价:”“原价¥”;日志文件里一行报错信息,夹杂着时间戳、IP、用户ID、错误码,全挤在同一个字段里;或者Excel里导出的客户姓名列,突然冒出几个“张三(VIP)”“李四[已注销]”“王五#202305”。这些不是脏数据,是“毛坯数据”——它有信息,但没结构;它能读,但没法算。而正则表达式(Regular Expressions),就是我们手里的那把“数字刻刀”,不靠人工逐条删改,而是用一套可复用、可验证、可嵌入脚本的规则,一次性把毛坯雕成标准件。
我做数据清洗七年,经手过金融交易流水、电商评论、IoT设备上报日志、医疗文本报告,最深的体会是: 80%的数据质量问题,不出现在缺失值或类型错误上,而出现在字符串的格式混乱里 。一个电话号码字段里混进“暂无”“/”“-”“空格+换行”,比整列都是NaN更致命——因为NaN好识别、好填充,而“138****1234”和“138-1234-5678”和“+86 138 1234 5678”看起来都像有效值,却根本无法聚合、去重、关联。这时候, re.findall() 不是锦上添花的技巧,而是救命的呼吸机。它不关心你后面要建模还是画图,只负责把“6 strawberries”里的6、“$19.99”里的19.99、“ABC-123”里的ABC和123,干净利落地抠出来,塞进一个list里,让你能立刻开始下一步。这不是炫技,是让数据真正“可用”的第一道工序。本文所有内容,都来自我在真实项目中反复打磨过的清洗逻辑——没有虚构案例,没有理想化假设,每一个正则模式,我都贴出它在生产环境里处理过的真实样本,以及踩坑后调整的参数依据。
2. 核心思路拆解:为什么不用split()、replace(),而必须用正则?
很多人初学时会本能地想:“不就是去掉符号吗?用 str.replace() 一个个删不行?”或者“电话号码有横杠,用 str.split('-') 再拼起来?”——这想法很自然,但放到真实数据里,会迅速崩盘。让我用三个真实项目片段说明为什么正则不可替代。
2.1 场景一:电商价格字段的“混沌战场”
某次处理某平台商品CSV时,价格列长这样:
price_raw
"¥129.00"
"$24.99 (促销价)"
"US$19.5"
"158元"
"¥ 89 . 5 0"
"暂无报价"
"面议"
"12,345.67"
如果用 replace() ,你得写:
s.replace('¥', '').replace('$', '').replace('US$', '').replace('元', '').replace(' ', '')
但问题来了:“¥ 89 . 5 0”里的空格和点怎么办? replace('.', '') 会把小数点也干掉,变成“8950”;而“12,345.67”里的逗号是千分位分隔符,该留还是该删? replace(',', '') 又会误伤“ABC,123”这种带逗号的SKU。更麻烦的是,“暂无报价”“面议”这种非数值, replace 后变成空字符串,转float直接报错。而正则 r'\d+(?:,\d{3})*(?:\.\d+)?' (稍后详解)能精准匹配“12,345.67”为一个整体,跳过“面议”,且保留小数点——因为它匹配的是“数字结构”,不是“字符替换”。
2.2 场景二:日志行的“多层嵌套提取”
一条Nginx访问日志:
192.168.1.100 - - [10/Jan/2023:14:23:01 +0000] "GET /api/v2/users?id=123&sort=name HTTP/1.1" 200 1245 "https://example.com/dashboard" "Mozilla/5.0..."
你想提取:IP、时间、HTTP方法、路径、状态码、响应大小。用 split() ?按空格切?但路径里有空格( /api/v2/users?id=123&sort=name ),Referer里也有空格( https://example.com/dashboard )。 split(' ') 会把路径切成5段,彻底乱套。而正则 r'^(\S+) \S+ \S+ \[([^\]]+)\] "(\w+) ([^"]+)" (\d+) (\d+)' 用 \S+ (非空白字符)匹配IP,用 \[([^\]]+)\] (方括号内任意非]字符)捕获时间,用 "(\w+) ([^"]+)" (引号内先取方法再取路径)——每一组捕获都基于语义边界,而非机械分割。
2.3 场景三:用户输入的“自由发挥式”电话号码
CRM系统里,销售手动录入的电话字段:
"138-1234-5678"
"+86 138 1234 5678"
"010-12345678"
"13812345678(手机)"
"138 1234 5678"
"138.1234.5678"
split('-') 只能处理第一种; replace() 要写七八个变体。而正则 r'1[3-9]\d{9}|0\d{2,3}-?\d{7,8}|\+\d{1,3}\s?\d{11}' (稍后详解)用 | (或)操作符并列三种主流格式,用 -? (问号表示0或1个横杠)、 \s? (0或1个空格)覆盖变体,用 1[3-9]\d{9} 精准锁定大陆手机号(1开头,第二位3-9,共11位),这才是工程级的鲁棒性。
提示:正则的核心价值不是“更短”,而是“更准”。
split()和replace()是手术刀,正则是CT扫描仪——它先看清数据的“解剖结构”,再决定哪里下刀。本文所有正则模式,都遵循“最小匹配原则”:只捕获必要部分,避免贪婪匹配导致跨字段污染。
3. 核心细节解析:从 re.findall() 到 re.sub() ,每个函数的实战定位
Python的 re 模块有十几个函数,但日常清洗中,真正高频、不可替代的只有四个: findall() 、 search() 、 sub() 、 compile() 。它们不是并列关系,而是有明确分工的“清洗流水线”。
3.1 re.findall(pattern, string) :批量提取的“收割机”
这是本文开篇提到的主力函数,也是新手最容易上手的。它的行为非常纯粹: 找到所有符合pattern的子串,返回list,不修改原字符串 。关键在于理解它的两个返回模式:
-
无捕获组时 :返回所有匹配的完整子串。
import re text = "the recipe calls for 10 strawberries and 1 banana" re.findall(r'\d+', text) # ['10', '1'] —— 完整匹配的数字字符串 -
有捕获组时 :只返回括号内的内容(即
()里的部分),这是精准提取的精髓。# 提取价格中的数字和单位(如"¥129.00" -> ('129.00', '¥')) re.findall(r'([0-9.]+)(¥|\$|USD)', "Price: ¥129.00, USD24.99") # [('129.00', '¥'), ('24.99', 'USD')]
实操心得:永远优先用捕获组!比如提取邮箱,
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'匹配整个邮箱,但如果你只想提取域名(@后面部分),写成r'@([A-Za-z0-9.-]+\.[A-Z|a-z]{2,})',findall就只返回域名列表,省去后续split('@')[1]的步骤,且避免@缺失时的索引错误。

525

被折叠的 条评论
为什么被折叠?



