mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1953 字
10 分钟
正 则 表 达 式 一 定 要 会

什么是正则表达式?#

正则表达式(Regular Expression)是一种用于描述字符串匹配规则的”语言”。几乎所有涉及文本处理的场景都离不开它——日志分析、数据清洗、表单校验、爬虫提取……

TIP

编写正则时,推荐使用原始字符串 r"..." 来避免反斜杠转义带来的困扰。例如 r"\d+""\\d+" 更清晰。


正则表达式语法速查#

常用元字符#

字符含义
.匹配除换行符外的任意字符
^匹配字符串开头
$匹配字符串末尾
*前一个字符重复 0 ~ ∞ 次
+前一个字符重复 1 ~ ∞ 次
?前一个字符重复 0 或 1 次
{m,n}前一个字符重复 m ~ n 次
|或,匹配左边或右边
[]字符集合,如 [a-z]
()捕获分组
\转义特殊字符

常用预定义序列#

序列含义等价写法(ASCII 模式)
\d数字[0-9]
\D非数字[^0-9]
\w单词字符[a-zA-Z0-9_]
\W非单词字符[^a-zA-Z0-9_]
\s空白字符[ \t\n\r\f\v]
\S非空白字符[^ \t\n\r\f\v]
\b单词边界

贪婪 vs 懒惰#

默认情况下,*+? 都是贪婪的,会尽可能多地匹配字符。在量词后加 ? 变为懒惰模式:

import re
text = "<h1>Title</h1>"
re.findall(r'<.*>', text) # 贪婪:['<h1>Title</h1>']
re.findall(r'<.*?>', text) # 懒惰:['<h1>', '</h1>']

核心函数详解#

re.search() — 扫描整个字符串#

在整个字符串中搜索第一个匹配,返回 Match 对象,未匹配返回 None

import re
m = re.search(r'\d+', 'hello 42 world 100')
if m:
print(m.group()) # '42'
print(m.span()) # (6, 8)

re.match() — 从开头匹配#

只在字符串开头尝试匹配。注意:即使在 MULTILINE 模式下,match() 也只匹配字符串开头,不匹配每行开头。

re.match(r'\d+', '123abc') # 匹配,返回 Match 对象
re.match(r'\d+', 'abc123') # 不匹配,返回 None
NOTE

match()search() 的区别:match() 只看开头,search() 扫描全文。大多数场景下 search() 更实用。

re.fullmatch() — 完全匹配#

要求整个字符串必须完全符合模式,常用于表单校验。

re.fullmatch(r'\d{11}', '13800138000') # 匹配
re.fullmatch(r'\d{11}', '1380013800x') # None

re.findall() — 找出所有匹配#

返回所有非重叠匹配的列表。如果模式中有捕获组,返回的是捕获组的内容。

# 无捕获组 → 返回字符串列表
re.findall(r'\d+', 'a1 b22 c333')
# ['1', '22', '333']
# 单个捕获组 → 返回字符串列表(组内容)
re.findall(r'(\d+)px', 'width:20px; height:30px')
# ['20', '30']
# 多个捕获组 → 返回元组列表
re.findall(r'(\w+)=(\d+)', 'width=20 height=30')
# [('width', '20'), ('height', '30')]

re.finditer() — 迭代所有匹配#

findall() 类似,但返回 Match 对象的迭代器,可以获取更多信息(位置、分组等)。

for m in re.finditer(r'\w+ly\b', 'He ran quickly and carefully'):
print(f'{m.group()} at position {m.span()}')
# quickly at position (7, 14)
# carefully at position (19, 28)

re.sub() — 替换匹配内容#

替换所有匹配的子串,repl 可以是字符串或函数。

# 基本替换
re.sub(r'\s+', '-', 'hello world python')
# 'hello-world-python'
# 使用反向引用交换两个单词
re.sub(r'(\w+) (\w+)', r'\2 \1', 'hello world')
# 'world hello'
# 使用函数作为替换逻辑
def double(m):
return str(int(m.group()) * 2)
re.sub(r'\d+', double, 'price: 10, tax: 3')
# 'price: 20, tax: 6'

re.split() — 按模式分割#

str.split() 更强大,支持正则模式分割。

re.split(r'[,;\s]+', 'apple, banana;cherry date')
# ['apple', 'banana', 'cherry', 'date']
# 带捕获组时,分隔符也会保留在结果中
re.split(r'(\W+)', 'one-two-three')
# ['one', '-', 'two', '-', 'three']

re.compile() — 预编译模式#

当同一个正则需要多次使用时,预编译可以提升性能。

pattern = re.compile(r'\b[A-Z][a-z]+\b')
pattern.findall('Hello World Python') # ['Hello', 'World', 'Python']
pattern.search('say Hello') # <re.Match object; span=(4, 9), match='Hello'>

Match 对象常用方法#

search()match() 等函数匹配成功时,返回一个 Match 对象:

m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '今天是 2026-02-11 星期三')
m.group() # '2026-02-11' 完整匹配
m.group(1) # '2026' 第 1 个捕获组
m.group('year') # '2026' 命名捕获组
m.groups() # ('2026', '02', '11')
m.groupdict() # {'year': '2026', 'month': '02', 'day': '11'}
m.start() # 4 匹配起始位置
m.end() # 14 匹配结束位置
m.span() # (4, 14)
TIP

Match 对象的布尔值始终为 True,所以可以直接用在 if 判断中:

if m := re.search(pattern, text):
process(m)

常用编译标志#

通过 flags 参数改变正则的匹配行为,多个标志用 | 组合:

标志缩写作用
re.IGNORECASEre.I忽略大小写
re.MULTILINEre.M^ / $ 匹配每行的开头和末尾
re.DOTALLre.S. 匹配包括换行符在内的所有字符
re.VERBOSEre.X允许在模式中添加注释和空白,提高可读性
re.ASCIIre.A\w \d \s 仅匹配 ASCII 字符
# VERBOSE 模式:写出可读性更好的正则
pattern = re.compile(r"""
(?P<protocol>https?) # 协议
:// # 分隔符
(?P<domain>[\w.-]+) # 域名
(?P<path>/\S*)? # 路径(可选)
""", re.VERBOSE)
m = pattern.search('访问 https://docs.python.org/zh-cn/3/ 查看文档')
m.groupdict()
# {'protocol': 'https', 'domain': 'docs.python.org', 'path': '/zh-cn/3/'}

实战 Demo:日志分析器#

以下是一个小 Demo 来串联以上知识点——解析 Nginx 访问日志,提取关键信息并统计。

import re
from collections import Counter
# 模拟的 Nginx 访问日志
log_data = """
192.168.1.1 - - [11/Feb/2026:10:00:01 +0800] "GET /index.html HTTP/1.1" 200 1024
10.0.0.5 - - [11/Feb/2026:10:00:02 +0800] "POST /api/login HTTP/1.1" 200 512
192.168.1.1 - - [11/Feb/2026:10:00:03 +0800] "GET /images/logo.png HTTP/1.1" 304 0
172.16.0.10 - - [11/Feb/2026:10:00:04 +0800] "GET /about HTTP/1.1" 200 2048
10.0.0.5 - - [11/Feb/2026:10:00:05 +0800] "GET /api/users HTTP/1.1" 403 128
192.168.1.1 - - [11/Feb/2026:10:00:06 +0800] "DELETE /api/users/3 HTTP/1.1" 500 64
172.16.0.10 - - [11/Feb/2026:10:00:07 +0800] "GET /index.html HTTP/1.1" 200 1024
10.0.0.5 - - [11/Feb/2026:10:00:08 +0800] "PUT /api/users/1 HTTP/1.1" 200 256
""".strip()
# 使用 VERBOSE 模式编写可读的日志解析正则
log_pattern = re.compile(r"""
(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # IP 地址
\s-\s-\s
\[(?P<time>[^\]]+)\] # 时间戳
\s
"(?P<method>\w+) # HTTP 方法
\s(?P<path>\S+) # 请求路径
\s(?P<protocol>[^"]+)" # 协议版本
\s(?P<status>\d{3}) # 状态码
\s(?P<size>\d+) # 响应大小
""", re.VERBOSE)
# ========== 1. 解析所有日志条目 ==========
print("=" * 50)
print("日志解析结果")
print("=" * 50)
entries = []
for m in log_pattern.finditer(log_data):
entry = m.groupdict()
entry['status'] = int(entry['status'])
entry['size'] = int(entry['size'])
entries.append(entry)
print(f" {entry['ip']:>15} | {entry['method']:<6} | {entry['path']:<20} | {entry['status']}")
# ========== 2. 统计各 IP 的访问次数 ==========
print(f"\n{'=' * 50}")
print("IP 访问次数统计")
print("=" * 50)
ip_list = re.findall(r'\d{1,3}(?:\.\d{1,3}){3}', log_data)
for ip, count in Counter(ip_list).most_common():
print(f" {ip:<20}{count} 次")
# ========== 3. 筛选异常请求(状态码 >= 400) ==========
print(f"\n{'=' * 50}")
print("异常请求(4xx / 5xx)")
print("=" * 50)
for entry in entries:
if entry['status'] >= 400:
print(f" [{entry['status']}] {entry['method']} {entry['path']}{entry['ip']}")
# ========== 4. 用 re.sub 脱敏 IP 地址 ==========
print(f"\n{'=' * 50}")
print(" IP 脱敏处理")
print("=" * 50)
masked = re.sub(
r'(\d{1,3}\.\d{1,3}\.)\d{1,3}\.\d{1,3}',
r'\1*.*',
log_data
)
# 只展示前 3 行
for line in masked.strip().split('\n')[:3]:
print(f" {line}")
print(" ...")

运行输出:

==================================================
📋 日志解析结果
==================================================
192.168.1.1 | GET | /index.html | 200
10.0.0.5 | POST | /api/login | 200
192.168.1.1 | GET | /images/logo.png | 304
172.16.0.10 | GET | /about | 200
10.0.0.5 | GET | /api/users | 403
192.168.1.1 | DELETE | /api/users/3 | 500
172.16.0.10 | GET | /index.html | 200
10.0.0.5 | PUT | /api/users/1 | 200
==================================================
🔢 IP 访问次数统计
==================================================
192.168.1.1 → 3 次
10.0.0.5 → 3 次
172.16.0.10 → 2 次
==================================================
⚠️ 异常请求(4xx / 5xx)
==================================================
[403] GET /api/users ← 10.0.0.5
[500] DELETE /api/users/3 ← 192.168.1.1
==================================================
🔒 IP 脱敏处理
==================================================
192.168.*.* - - [11/Feb/2026:10:00:01 +0800] "GET /index.html HTTP/1.1" 200 1024
10.0.*.* - - [11/Feb/2026:10:00:02 +0800] "POST /api/login HTTP/1.1" 200 512
192.168.*.* - - [11/Feb/2026:10:00:03 +0800] "GET /images/logo.png HTTP/1.1" 304 0
...
NOTE

Q: 为什么 \[(?P<time>[^\]]+)\] 这里,之前明明加过了 r 表示原始字符串了,这里依旧要使用 \[ 来表示呢?

A: 因为 r"" 只是表示在 Python 字符串阶段,忽略 \ 本身的转义,但是在正则解析的阶段,由于 [] 有特殊含义,因此需要一个 \[ 表示输出 [这个中括号本身


常见踩坑提醒#

  1. match()search()match() 只匹配开头,想搜索全文请用 search()
  2. findall() 有捕获组时,返回的是组内容而非完整匹配——如果只想分组但不影响返回值,用非捕获组 (?:...)
  3. 贪婪匹配陷阱:r'<.*>' 会匹配从第一个 < 到最后一个 > 的所有内容,用 r'<.*?>' 改为懒惰
  4. 别忘了 r 前缀:\b 在普通字符串中是退格符,在 r"\b" 中才是单词边界
  5. re.compile() 不是必须的:Python 内部会缓存最近使用的模式,但频繁使用同一模式时预编译更清晰高效

小结#

需求推荐函数
检查字符串是否包含某模式re.search()
从开头匹配re.match()
完整校验(如手机号格式)re.fullmatch()
提取所有匹配re.findall() / re.finditer()
替换文本re.sub()
按模式分割re.split()
多次复用同一模式re.compile()
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

正 则 表 达 式 一 定 要 会
https://l1ngg.info/posts/tech/python-re/
作者
L1ngg
发布于
2026-02-11
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00