数据工程师每天打交道的东西,很大一部分是文本:
- 应用日志
- Nginx access log
- CSV 里混着脏数据的字段
- API 返回的 JSON 片段
- 配置文件
- 爬下来的 HTML
这些文本往往不是完全结构化的,或者格式五花八门,用传统的 split、replace 处理起来很累,写一堆条件判断也不一定覆盖全。
正则表达式就是专门解决这类问题的。
它本质上是一种描述文本模式的小型语言,不依赖复杂的解析库,Python 里直接 import re 就能用。
它的优势很明显:轻量、灵活、跨语言,而且一旦写好,提取、清洗、验证、替换都能搞定。
缺点也很明显,对于初学者来说,正则的写法晦涩难懂。
本文希望通过从数据工程师日常会碰到的场景中,一步一步演示怎么用正则把活干了。
1. 理解正则的思维:从左到右找模式
正则表达式的工作方式很简单:它从左到右扫描文本,在每一个位置检查是否匹配你定义的模式。
比如你要在日志里找 "error" 这个词,模式就是 r"error"。
这里前面的 r 表示原始字符串,防止 Python 把反斜杠当成转义符,写正则时最好都加上。
python
import re
log_line = "2024-01-15 10:23:45 ERROR User login failed"
if re.search(r"error", log_line, re.IGNORECASE):
print("发现错误")
# 发现错误
默认是区分大小写的,加上 re.IGNORECASE 就能忽略大小写。
这就是最基础的字面匹配。但实际工作中,文本不会这么整齐,所以我们需要更灵活的工具。
2. 字面匹配不够用?加入元字符
很多情况下你只知道一部分模式。比如日志里的日期,格式是 2024-01-15,但具体数字每天都在变。
这时候可以用 \d 表示数字,{4} 表示重复 4 次,于是 \d{4}-\d{2}-\d{2} 就能匹配日期。
再比如电话号码可能是 555-1234 也可能是 5551234,中间的连字符可有可无,写成 \b\d{3}-?\d{4}\b。
? 表示前面的字符出现 0 次或 1 次,\b 是单词边界,防止匹配到更长数字的一部分。
python
text = "联系方式:555-1234,备用:5555678"
phones = re.findall(r"\b\d{3}-?\d{4}\b", text)
print(phones)
# phones -> ['555-1234', '5555678']
除了 \d,还有 . 匹配任意单个字符(换行除外),[] 定义字符集合,
比如 [cb]at 匹配 cat 或 bat,[a-d] 匹配 a 到 d 的任意字母。
量词除了 ?,还有 *(0 次或多次)、+(1 次或多次)、{n,m}(n 到 m 次)。
这些组合起来,就能描述大部分文本模式。
3. 锚点:把匹配固定在开头或结尾
有时候你不想匹配文本中间的任何位置,而是要求从开头或结尾开始。
^ 匹配字符串开头,$ 匹配字符串结尾。
比如你要检查一个文件名是不是以 .csv 结尾:
python
filename = "sales_data_2024.csv"
if re.search(r"\.csv$", filename):
print("这是 CSV 文件")
# 这是 CSV 文件
注意 $ 只是匹配位置,不消耗字符。
锚点在验证格式时特别有用,比如检查一行日志是不是以某个时间戳开头,或者某个字段是不是纯数字。
4. 捕获组:把需要的数据单独拿出来
前面都是找,但数据工程师更常做的是"提取"。
比如日志格式是:
plain
2024-01-15 10:23:45 ERROR [main] User login failed
你想把日期、时间、日志级别、线程名、消息分别拿出来。
用括号 () 创建捕获组,re.findall() 会返回元组列表。
python
log = "2024-01-15 10:23:45 ERROR [main] User login failed"
pattern = r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) \[(.*?)\] (.*)"
match = re.search(pattern, log)
if match:
date, time, level, thread, message = match.groups()
print(date, time, level, thread, message)
# date='2024-01-15'
# time='10:23:45'
# level='ERROR'
# thread='main'
# message='User login failed'
这里 .*? 是非贪婪匹配,后面会讲。
捕获组让你能把非结构化文本直接变成结构化字段,这是日志解析里最常用的手段。
5. 命名组:让正则更好维护
捕获组多了以后,靠位置记住哪个组是什么很容易出错。
命名组语法是 (?P<name>...),访问时用 group('name')。
比如从邮箱地址里提取用户名和域名:
python
email = "john.doe@example.com"
pattern = r"(?P<username>[\w.]+)@(?P<domain>[\w.]+)"
match = re.search(pattern, email)
if match:
print(match.group('username')) # john.doe
print(match.group('domain')) # example.com
这样即使以后调整组的顺序,代码也不用改。
可读性也上来了,别人看你的正则就知道每个部分是干嘛的。
6. 实际场景:数据清洗、信息提取、验证
数据清洗最常见的需求之一是从格式混乱的编码里提取纯数字。
比如产品代码有 PROD-123、Product 456、prod_789、PR-101,你只想要数字部分:
python
codes = ["PROD-123", "Product 456", "prod_789", "PR-101"]
cleaned = []
for code in codes:
m = re.search(r"\d+", code)
if m:
cleaned.append(m.group())
print(cleaned)
# cleaned -> ['123', '456', '789', '101']
信息提取可以用命名组做得更清晰。
比如客服日志:
plain
ISSUE #1234 [2023-10-15] Customer reported app crash on iPhone 12, iOS 15.2
你想提取问题编号、日期、设备、系统版本:
python
log = "ISSUE #1234 [2023-10-15] Customer reported app crash on iPhone 12, iOS 15.2"
pattern = r"ISSUE #(?P<issue>\d+) \[(?P<date>\d{4}-\d{2}-\d{2})\].*?(?P<device>iPhone \d+).*?(?P<ios>iOS \d+\.\d+)"
m = re.search(pattern, log)
if m:
print(m.groupdict())
# {'issue': '1234', 'date': '2023-10-15', 'device': 'iPhone 12', 'ios': 'iOS 15.2'}
数据验证也是常事。
比如验证邮箱,不要只返回 True/False,最好能告诉调用方哪里不对:
python
def validate_email(email):
pattern = r"^[\w.%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
if not re.match(pattern, email):
if '@' not in email:
return False, "缺少 @ 符号"
username, domain = email.split('@', 1)
if not username:
return False, "用户名为空"
if '.' not in domain:
return False, "域名缺少顶级域"
return False, "邮箱格式不正确"
return True, "有效邮箱"
7. 高级技巧:前瞻和非贪婪
前瞻(lookahead)是一种零宽断言,它只检查某个模式是否存在,但不消耗字符。
比如检查密码强度:
python
password = "Password123"
has_upper = bool(re.search(r"(?=.*[A-Z])", password)) # True
has_lower = bool(re.search(r"(?=.*[a-z])", password)) # True
has_digit = bool(re.search(r"(?=.*\d)", password)) # True
非贪婪匹配也很实用。默认量词是贪婪的,会尽可能多匹配。
比如解析 HTML 片段:
python
html = "<div>First</div><div>Second</div>"
greedy = re.findall(r"<div>(.*)</div>", html) # ['First</div><div>Second']
non_greedy = re.findall(r"<div>(.*?)</div>", html) # ['First', 'Second']
在量词后面加 ? 就变成非贪婪,匹配到第一个满足条件的位置就停。
处理嵌套或重复结构时,这个区别很关键。
8. 如何学正则
正则不用死记硬背。
建议从字面匹配开始,然后慢慢加入字符类、量词、捕获组、锚点,最后再碰前瞻和非贪婪。
遇到复杂模式,先拆成小段单独测试,确认每一部分都符合预期再拼起来。
调试工具推荐 regex101.com,它能实时高亮匹配结果,还会解释每个部分的含义。
另外,用小规模但覆盖边界情况的数据来测试,比如空字符串、特殊字符、超长文本。
多练几次,常见的模式自然就记住了。
9. 总结
正则表达式是数据工程师处理文本数据的必备技能。
它能从非结构化文本里提取结构化信息,清洗格式不一致的数据,验证字段是否符合规则,还能做复杂的搜索替换。
掌握它的关键不是背元字符,而是理解"模式匹配"的思维方式,然后结合具体场景反复练习。
一旦熟练,很多原本需要写几十行代码的文本处理任务,几行正则就能搞定,效率提升非常直接。