4、数据解析之正则:re模块基础、常用匹配模式、实战提取邮箱与电话
正则表达式,说白了就是一套「按规则找东西」的语法。我刚开始接触数据清洗那会儿,面对一堆乱七八糟的文本,第一反应是写几十行 if-else 去判断。后来被同事看了一眼代码,他丢下一句「用正则啊」,就走了。嗯,从那以后,我再也没写过那种笨办法。
这一章,咱们就聊聊 Python 的 re 模块,以及怎么用它从文本里精准捞出邮箱和电话号码。你想想看,数据分类的第一步,不就是把有用的字段从原始文本里「抠」出来吗?
4.1 re 模块基础:三个核心函数
Python 的 re 模块,我平时用得最多的就三个函数:re.findall()、re.search() 和 re.sub()。记住这三个,基本能覆盖 90% 的场景。
| 函数 | 作用 | 返回类型 | 常用场景 |
|---|---|---|---|
re.findall() |
查找所有匹配项 | 列表 | 提取所有邮箱、电话 |
re.search() |
查找第一个匹配项 | Match 对象 | 判断文本是否包含某模式 |
re.sub() |
替换匹配项 | 字符串 | 脱敏、清洗脏数据 |
我的习惯: 写正则之前,一定先拿 re.findall() 试一把。看到返回的列表,心里就有底了。如果返回空列表,说明模式写错了,赶紧调。
举个最简单的例子:
import re
text = "我的邮箱是 alice@example.com,你也可以联系 bob@test.org"
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
print(emails)
# 输出:['alice@example.com', 'bob@test.org']
你看,一行代码就把两个邮箱全拎出来了。这就是正则的威力。
4.2 常用匹配模式:从字符到边界
正则的语法其实不多,但组合起来千变万化。我把它拆成几个核心模块,你记牢了就行。
4.2.1 字符类与量词
| 模式 | 含义 | 示例 |
|---|---|---|
. |
匹配任意单个字符(除换行) | a.b 匹配 aab、a1b |
\d |
匹配数字(0-9) | \d{3} 匹配三位数字 |
\w |
匹配字母、数字、下划线 | \w+ 匹配一个单词 |
\s |
匹配空白字符(空格、制表符等) | \s+ 匹配连续空白 |
* |
匹配前一个字符 0 次或多次 | ab*c 匹配 ac、abc |
+ |
匹配前一个字符 1 次或多次 | ab+c 匹配 abc、abbc |
? |
匹配前一个字符 0 次或 1 次 | ab?c 匹配 ac、abc |
{n,m} |
匹配前一个字符 n 到 m 次 | \d{3,5} 匹配 3-5 位数字 |
避坑指南: 我曾经在匹配手机号时用了 \d{11},结果把一段 11 位的数字串全抓出来了,里面混着座机号。后来加上了位置边界 \b,才精准匹配到独立的手机号。
4.2.2 边界与分组
边界匹配,说白了就是告诉正则「从哪儿开始找,到哪儿结束」。分组则是把匹配到的内容拆成小块,方便后续处理。
^:匹配字符串开头$:匹配字符串结尾\b:匹配单词边界(空格、标点、字符串首尾)():分组,提取子串(?:):非捕获分组,只匹配不提取
举个例子,提取邮箱的用户名和域名:
text = "alice@example.com"
match = re.search(r'(\w+)@(\w+\.\w+)', text)
if match:
print("用户名:", match.group(1)) # alice
print("域名:", match.group(2)) # example.com
分组的好处是,你不仅能找到邮箱,还能把邮箱拆成两部分,存到数据库的不同字段里。
4.3 实战:提取邮箱
邮箱的格式其实挺复杂的。理论上,邮箱可以包含 +、-、. 等特殊字符。但实际业务中,我们遇到的绝大多数邮箱都是标准格式。
我常用的邮箱正则:
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
这个模式能覆盖 99% 的场景。但要注意,有些邮箱域名是 .com.cn 这种两级后缀,上面的模式也能匹配,因为 [a-zA-Z]{2,} 只要求最后一部分至少两个字母。
注意: 不要试图写一个「完美」的邮箱正则。RFC 5322 标准定义的邮箱格式极其复杂,包含注释、引号等奇葩写法。实际项目中,够用就行。我曾经花了一整天去适配所有 RFC 标准,最后发现业务数据里根本用不上。
实战代码:从一段文本中提取所有邮箱,并去重:
import re
text = """
联系方式:
张三:zhangsan@company.com
李四:lisi@test.org.cn
王五:wangwu@company.com(重复)
备用:zhangsan@company.com
"""
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
unique_emails = list(set(emails))
print(unique_emails)
# 输出:['zhangsan@company.com', 'lisi@test.org.cn', 'wangwu@company.com']
4.4 实战:提取电话号码
电话号码比邮箱麻烦。中国的手机号是 11 位,以 1 开头,第二位通常是 3-9。座机号则带区号,比如 010-12345678。
我一般分两种情况处理:
4.4.1 手机号
phone_pattern = r'1[3-9]\d{9}'
这个模式匹配以 1 开头,第二位是 3-9,后面跟 9 位数字,总共 11 位。
4.4.2 座机号(含区号)
landline_pattern = r'0\d{2,3}-?\d{7,8}'
这个模式匹配以 0 开头,区号 2-3 位,中间可能有连字符,后面 7-8 位号码。
实战代码:从文本中提取所有电话:
text = """
联系电话:13800138000
座机:010-12345678
另一个手机:15912345678
国际格式:+86 13800138000
"""
# 手机号
phones = re.findall(r'1[3-9]\d{9}', text)
print("手机号:", phones)
# 输出:['13800138000', '15912345678']
# 座机号
landlines = re.findall(r'0\d{2,3}-?\d{7,8}', text)
print("座机号:", landlines)
# 输出:['010-12345678']
我的经验: 实际数据里,手机号可能连着其他数字,比如「手机13800138000备用」。这时候 \b 边界就派上用场了。加上 \b 可以避免匹配到一段长数字中的子串。
4.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个速查地图。
4.6 避坑与技巧
正则写多了,总会遇到一些坑。我把自己踩过的几个分享给你:
- 贪婪匹配 vs 非贪婪匹配: 默认情况下,
*和+是贪婪的,会尽可能多地匹配。如果你只想匹配最少的内容,加个?,比如.*?。 - 原始字符串
r'': 写正则时一定要用r'',否则反斜杠会被 Python 解释器转义。我刚开始就吃过这个亏,写\d结果匹配不到任何数字。 - 性能问题: 如果要对大量文本做正则匹配,建议先用
re.compile()编译模式,能提升不少速度。
# 编译模式,提升性能
pattern = re.compile(r'1[3-9]\d{9}')
text = "手机号:13800138000,另一个:15912345678"
matches = pattern.findall(text)
print(matches)
重要提醒: 正则不是万能的。遇到嵌套结构(比如 HTML、JSON),别用正则去解析,那是自找麻烦。老老实实用专门的解析库,比如 BeautifulSoup 或 json 模块。