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 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个速查地图。

数据解析之正则:知识体系 re 模块 re.findall() re.search() re.sub() 字符类与量词 边界与分组 转义与原始字符串 提取邮箱 提取电话号码 避坑:边界匹配、贪婪模式、性能优化 核心原则:先写小模式测试,再组合;多用 re.findall() 验证;不要追求完美匹配,够用就好

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 模块。