Python re 模块详解 - 正则表达式

核心概念

模式字符串(Pattern) → 匹配 → 目标字符串(String)

常用元字符速查

符号 含义 示例
. 任意字符(除换行) a.c → abc
* 0个或多个 ab* → a, ab, abb
+ 1个或多个 ab+ → ab, abb
? 0个或1个 ab? → a, ab
^ 开头 ^hello
$ 结尾 world$
\d 数字 \d+ → 123
\w 字母/数字/下划线 \w+
\s 空白字符 \s+
[] 字符集合 [a-z]
() 分组捕获 (ab)+
{n,m} 重复次数 \d{3,5}

1.re.compile() - 编译正则表达式

功能: 将正则字符串编译成 Pattern对象,复用时性能更高

返回: re.Pattern对象

import re

# 直接用字符串(每次都重新编译,效率低)
re.findall(r'\d+', '价格123元')

# compile 预编译(推荐!大量使用时效率高)
pattern = re.compile(r'\d+')
print(type(pattern))  # <class 're.Pattern'>

# 编译后可复用
pattern.findall('价格123元')
pattern.findall('数量456个')
pattern.findall('编号789号')

# 常用 flags 参数
pattern = re.compile(r'hello', re.IGNORECASE)   # 忽略大小写
pattern = re.compile(r'hello', re.MULTILINE)    # 多行模式
pattern = re.compile(r'hello', re.DOTALL)       # . 匹配换行符

2. re.match() - 从开头匹配

功能: 只从字符串开头开始匹配,不是开头则失败

返回: 匹配成功 → ``Match对象,失败 → None

import re

# ✅ 从开头能匹配
result = re.match(r'\d+', '123abc')
print(result)           # <re.Match object; span=(0, 3), match='123'>
print(result.group())   # '123'       → 匹配到的字符串
print(result.span())    # (0, 3)      → 起止位置
print(result.start())   # 0
print(result.end())     # 3

# ❌ 不是从开头,返回 None
result = re.match(r'\d+', 'abc123')
print(result)  # None

# 安全写法(避免 None 报错)
if result := re.match(r'\d+', '123abc'):
    print(result.group())

3. re.search() - 全文搜索第一个

功能: 扫描整个字符串,找到第一个匹配项

返回: 匹配成功 → Match对象,失败 → None

import re

# 不要求从开头,全文搜索
result = re.search(r'\d+', 'abc123def456')
print(result.group())  # '123'  → 只返回第一个!
print(result.span())   # (3, 6)

# match vs search 区别
re.match(r'\d+', 'abc123')   # None(不是开头)
re.search(r'\d+', 'abc123')  # '123'(全文搜索到了)

# 分组捕获
result = re.search(r'(\w+)@(\w+)\.(\w+)', 'user@gmail.com')
print(result.group())   # 'user@gmail.com'  → 整体
print(result.group(1))  # 'user'            → 第1组
print(result.group(2))  # 'gmail'           → 第2组
print(result.group(3))  # 'com'             → 第3组
print(result.groups())  # ('user', 'gmail', 'com') → 所有分组

4. re.findall() - 找所有匹配项 ⭐ 最常用

功能: 返回所有匹配结果

返回: 列表 list(无匹配返回空列表)

import re

# 基础用法
result = re.findall(r'\d+', 'abc123def456ghi789')
print(result)  # ['123', '456', '789']

# 无匹配返回空列表
result = re.findall(r'\d+', 'abcdef')
print(result)  # []

# ⚠️ 有分组时,返回分组内容
text = '张三:18, 李四:25, 王五:30'

# 无分组 → 返回整体匹配
re.findall(r'\w+:\d+', text)
# ['张三:18', '李四:25', '王五:30']

# 有1个分组 → 返回分组内容
re.findall(r'\w+:(\d+)', text)
# ['18', '25', '30']

# 有多个分组 → 返回元组列表
re.findall(r'(\w+):(\d+)', text)
# [('张三', '18'), ('李四', '25'), ('王五', '30')]

5. re.finditer() - 找所有匹配项(迭代器版)

功能: 和 findall 类似,但返回迭代器,节省内存

返回: 迭代器,每个元素是 Match对象

import re

# 大数据量时用 finditer 更省内存!
text = 'abc123def456ghi789'
for match in re.finditer(r'\d+', text):
    print(match.group(), match.span())
# 123 (3, 6)
# 456 (9, 12)
# 789 (15, 18)

# findall vs finditer
re.findall(r'\d+', text)    # 一次性返回所有结果 → 占内存
re.finditer(r'\d+', text)   # 懒加载,逐个取 → 省内存 ✅

6. re.sub() - 替换 ⭐ 清洗数据必备

功能: 将匹配到的内容替换成指定字符串

返回: 替换后的新字符串

import re

# 基础替换
result = re.sub(r'\d+', 'NUM', 'abc123def456')
print(result)  # 'abcNUMdefNUM'

# 清洗 HTML 标签
html = '<p>你好<br/>世界</p>'
result = re.sub(r'<.*?>', '', html)
print(result)  # '你好世界'

# 清洗特殊字符(数据清洗常用)
text = 'Hello!!! World@@@ 2024###'
result = re.sub(r'[^\w\s]', '', text)  # 只保留字母数字空格
print(result)  # 'Hello World 2024'

# count 参数:限制替换次数
result = re.sub(r'\d+', 'NUM', 'a1b2c3', count=2)
print(result)  # 'aNUMbNUMc3'  → 只替换前2个

# 用函数做替换(动态替换)
def double(match):
    return str(int(match.group()) * 2)

result = re.sub(r'\d+', double, '价格10元, 数量5个')
print(result)  # '价格20元, 数量10个'

7. re.split() - 按正则分割

功能: 按匹配到的模式分割字符串

返回: 列表 list

import re

# 按多种分隔符分割(数据清洗超常用)
text = '苹果,香蕉;葡萄 西瓜|橙子'
result = re.split(r'[,;\s|]+', text)
print(result)  # ['苹果', '香蕉', '葡萄', '西瓜', '橙子']

# 普通 split 做不到多种分隔符!
'苹果,香蕉;葡萄'.split(',')  # ['苹果', '香蕉;葡萄'] ← 只分了逗号

# 保留分隔符(加括号分组)
result = re.split(r'([,;])', '苹果,香蕉;葡萄')
print(result)  # ['苹果', ',', '香蕉', ';', '葡萄']

8. re.fullmatch() - 完整匹配

功能: 整个字符串必须完全符合模式

返回: Match对象None

import re

# 表单验证常用场景
# 验证手机号
re.fullmatch(r'1[3-9]\d{9}', '13812345678')  # ✅ Match对象
re.fullmatch(r'1[3-9]\d{9}', '138123456789') # ❌ None(多一位)

# 验证邮箱
re.fullmatch(r'\w+@\w+\.\w+', 'user@gmail.com')  # ✅

# 验证日期格式
re.fullmatch(r'\d{4}-\d{2}-\d{2}', '2024-01-15')  # ✅

函数返回值汇总

函数 返回值 适用场景
compile() Pattern对象 预编译复用
match() Match对象 / None 验证开头格式
search() Match对象 / None 找第一个匹配
findall() 列表 提取所有匹配
finditer() 迭代器 大数据量提取
sub() 新字符串 替换清洗
split() 列表 多规则分割
fullmatch() Match对象 / None 表单验证

实战:数据清洗综合示例

import re

# 原始脏数据
raw_data = [
    "  张三  | 年龄:28 | 电话:138-1234-5678 | 邮箱:zhangsan@qq.com  ",
    "  李四  | 年龄:32 | 电话:139-8765-4321 | 邮箱:lisi@163.com  ",
]

pattern_phone = re.compile(r'(\d{3})-(\d{4})-(\d{4})')
pattern_email = re.compile(r'[\w.]+@[\w.]+')
pattern_age   = re.compile(r'年龄:(\d+)')

for row in raw_data:
    # 1. 去除首尾空格
    row = row.strip()
    
    # 2. 提取姓名(第一个字段)
    name = row.split('|')[0].strip()
    
    # 3. 提取年龄
    age = pattern_age.search(row).group(1)
    
    # 4. 提取并格式化电话
    phone = pattern_phone.sub(r'\1\2\3', row)  # 去掉横线
    phone = pattern_phone.search(row).group()
    
    # 5. 提取邮箱
    email = pattern_email.search(row).group()
    
    print(f"姓名:{name} 年龄:{age} 电话:{phone} 邮箱:{email}")

# 输出:
# 姓名:张三 年龄:28 电话:138-1234-5678 邮箱:zhangsan@qq.com
# 姓名:李四 年龄:32 电话:139-8765-4321 邮箱:lisi@163.com

正则表达式语法详解 & 高级用法

之前的部分

r'[,;\s|]+'
# []   → 字符集合,匹配里面任意一个字符
# ,    → 逗号
# ;    → 分号
# \s   → 任意空白(空格/Tab/换行)
# |    → 竖线(在[]内就是普通字符,不是"或")
# +    → 前面的集合出现 1次或多次
# 整体:匹配 一个或多个 [逗号/分号/空白/竖线]

r'([,;])'
# ()   → 捕获分组,把匹配内容"存起来"
# [,;] → 匹配逗号或分号
# 整体:匹配并捕获 逗号或分号

r'1[3-9]\d{9}'
# 1      → 字面量 1
# [3-9]  → 3到9中任意一个数字
# \d     → 任意数字 0-9
# {9}    → 前面的\d 重复精确9次
# 整体:1开头 + 第二位3-9 + 后面9位数字 = 11位手机号

r'\w+@\w+\.\w+'
# \w+  → 1个或多个 字母/数字/下划线
# @    → 字面量 @
# \w+  → 域名主体
# \.   → 字面量 .(必须转义,否则.代表任意字符)
# \w+  → 顶级域名 com/cn/org
# 整体:简单邮箱格式

r'\d{4}-\d{2}-\d{2}'
# \d{4} → 精确4位数字(年)
# -     → 字面量 -
# \d{2} → 精确2位数字(月)
# -     → 字面量 -
# \d{2} → 精确2位数字(日)
# 整体:yyyy-MM-dd 日期格式

r'(\w+)@(\w+)\.(\w+)'
# (\w+) → 第1组:用户名
# @     → 字面量 @
# (\w+) → 第2组:域名主体
# \.    → 字面量 .
# (\w+) → 第3组:顶级域名
# 整体:邮箱拆成3个部分分别捕获

核心语法完整速查

# ========== 量词 ==========
r'a*'      # 0个或多个 a
r'a+'      # 1个或多个 a
r'a?'      # 0个或1个 a(可选)
r'a{3}'    # 精确3个 a
r'a{3,5}'  # 3到5个 a
r'a{3,}'   # 3个或更多 a

# ========== 贪婪 vs 非贪婪 ==========
r'<.*>'    # 贪婪:尽可能多匹配
r'<.*?>'   # 非贪婪:尽可能少匹配(加?变非贪婪)

# ========== 字符集 ==========
r'[abc]'   # a或b或c
r'[a-z]'   # 小写字母
r'[A-Z]'   # 大写字母
r'[0-9]'   # 数字(等同\d)
r'[^abc]'  # 非a非b非c(^在[]内表示取反)
r'[a-zA-Z0-9]'  # 字母或数字

# ========== 预定义字符 ==========
r'\d'   # 数字          [0-9]
r'\D'   # 非数字        [^0-9]
r'\w'   # 字母数字下划线 [a-zA-Z0-9_]
r'\W'   # 非\w
r'\s'   # 空白符(空格/Tab/换行)
r'\S'   # 非空白
r'\b'   # 单词边界
r'\B'   # 非单词边界

# ========== 锚点 ==========
r'^hello'   # 以hello开头
r'world$'   # 以world结尾
r'^\d+$'    # 整个字符串全是数字

# ========== 分组 ==========
r'(ab)+'         # 捕获分组,可反向引用
r'(?:ab)+'       # 非捕获分组(只分组不存储,性能更好)
r'(?P<name>\w+)' # 命名分组

高级用法 1:命名分组 (?P<name>...)

import re

text = '张三 1990-05-20 13812345678'

pattern = re.compile(
    r'(?P<name>\w+)\s+'       # 命名分组:姓名
    r'(?P<birth>\d{4}-\d{2}-\d{2})\s+'  # 命名分组:生日
    r'(?P<phone>1[3-9]\d{9})'           # 命名分组:手机
)

m = pattern.search(text)

# 用名字取值,比 group(1) 更可读!
print(m.group('name'))   # '张三'
print(m.group('birth'))  # '1990-05-20'
print(m.group('phone'))  # '13812345678'

# 转成字典
print(m.groupdict())
# {'name': '张三', 'birth': '1990-05-20', 'phone': '13812345678'}

高级用法 2:贪婪 vs 非贪婪(爬虫必知)

import re

html = '<div>第一段</div><div>第二段</div>'

# 贪婪模式(默认):.*  尽可能多吃
result = re.findall(r'<div>(.*)</div>', html)
print(result)  # ['第一段</div><div>第二段']  ← 吃多了!

# 非贪婪模式:.*?  尽可能少吃
result = re.findall(r'<div>(.*?)</div>', html)
print(result)  # ['第一段', '第二段']  ← 正确!

# 规律:量词后加 ? 变非贪婪
# *?  +?  ??  {n,m}?

高级用法 3:前瞻与后顾(零宽断言)

import re

# ========== 正向前瞻 (?=...) ==========
# 匹配后面跟着某内容的位置,但不消耗字符

text = '100元 200美元 300日元'

# 提取 元 前面的数字
result = re.findall(r'\d+(?=元)', text)
print(result)  # ['100', '300']  ← 200美元不匹配

# ========== 负向前瞻 (?!...) ==========
# 匹配后面 不跟着 某内容的位置

result = re.findall(r'\d+(?!元|\d)', text)
print(result)  # ['200']  ← 只有美元前的200

# ========== 正向后顾 (?<=...) ==========
# 匹配前面是某内容的位置

text = '价格:99 数量:5 折扣:0.8'
result = re.findall(r'(?<=:)\d+\.?\d*', text)
print(result)  # ['99', '5', '0.8']  ← 提取冒号后面的数字

# ========== 负向后顾 (?<!...) ==========
text = '100 $200 300'
result = re.findall(r'(?<!\$)\d+', text)
print(result)  # ['100', '300']  ← 不提取带$的

高级用法 4:| 多模式选择

import re

# 匹配多种格式的电话
text = '电话:010-12345678 或 13812345678 或 (021)87654321'

pattern = re.compile(
    r'(\(?\d{3}\)?\-?\d{8})'   # 座机
    r'|'                         # 或
    r'(1[3-9]\d{9})'            # 手机
)

for m in pattern.finditer(text):
    print(m.group())
# 010-12345678
# 13812345678
# (021)87654321

高级用法 5:re.sub 高级替换

import re

# ===== 用分组反向引用 \1 \2 =====
# 日期格式转换 2024-01-15 → 15/01/2024
text = '今天是 2024-01-15,明天是 2024-01-16'
result = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\3/\2/\1', text)
print(result)  # '今天是 15/01/2024,明天是 16/01/2024'

# ===== 用命名分组 =====
result = re.sub(
    r'(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})',
    r'\g<d>/\g<m>/\g<y>',
    text
)
print(result)  # '今天是 15/01/2024,明天是 16/01/2024'

# ===== 用函数动态替换 =====
# 敏感词打码
sensitive = ['傻瓜', '白痴', '废物']
pattern = re.compile('|'.join(sensitive))

def mask(m):
    word = m.group()
    return word[0] + '*' * (len(word) - 1)  # 只保留第一个字

text = '你这个傻瓜和白痴'
print(pattern.sub(mask, text))  # '你这个傻*和白*'

高级用法 6:flags 标志位

import re

text = '''Hello World
hello python
HELLO REGEX'''

# re.IGNORECASE (re.I) → 忽略大小写
re.findall(r'hello', text, re.I)
# ['Hello', 'hello', 'HELLO']

# re.MULTILINE (re.M) → ^ $ 匹配每行首尾
re.findall(r'^\w+', text, re.M)
# ['Hello', 'hello', 'HELLO']  ← 每行开头

# re.DOTALL (re.S) → . 匹配换行符
re.findall(r'Hello.*HELLO', text, re.S)
# ['Hello World\nhello python\nHELLO']

# re.VERBOSE (re.X) → 可以写注释,提高可读性!
phone_pattern = re.compile(r'''
    (1)         # 第1位必须是1
    ([3-9])     # 第2位3-9
    (\d{9})     # 后9位任意数字
''', re.VERBOSE)

实战综合:脏数据清洗流水线

import re

dirty_data = [
    "  <张三>  |  age:28岁  |  TEL:138-1234-5678  ",
    "  【李四】  |  age:32岁  |  TEL:139-8765-4321  ",
]

# 全角转半角
def full2half(text):
    result = ''
    for char in text:
        code = ord(char)
        if 0xFF01 <= code <= 0xFF5E:      # 全角字符范围
            result += chr(code - 0xFEE0)   # 转半角
        elif code == 0x3000:               # 全角空格
            result += ' '
        else:
            result += char
    return result

patterns = {
    'name':  re.compile(r'[<【<]([\w]+)[>】>]'),
    'age':   re.compile(r'age[::]\s*(\d+)'),
    'phone': re.compile(r'TEL[::]\s*([\d-]+)'),
}

for row in dirty_data:
    row = full2half(row).strip()        # 1. 全角→半角,去空格
    row = re.sub(r'\s+', ' ', row)      # 2. 多个空格→单个

    name  = patterns['name'].search(row).group(1)
    age   = patterns['age'].search(row).group(1)
    phone = patterns['phone'].search(row).group(1)
    phone = re.sub(r'-', '', phone)     # 3. 去掉电话横线

    print(f"姓名:{name} | 年龄:{age} | 电话:{phone}")

# 输出:
# 姓名:张三 | 年龄:28 | 电话:13812345678
# 姓名:李四 | 年龄:32 | 电话:13987654321

总结

  • 找一个用 search
  • 找所有用 findall
  • 替换用 sub
  • 分割用 split
  • 验证用 fullmatch
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐