Python数据分析中正则表达式匹配字符串与常用函数示例详解

前言

先澄清标题里的一个措辞:正则表达式(regular expression,简称 regex 或 RE)不是数据分析专有的东西 ,它是 Python 标准库 re 模块提供的通用文本处理能力。在数据分析场景里之所以频繁出现,是因为原始数据往往是一堆没洗过的文本------日志、爬下来的网页、表格里的脏字段------需要用模式而不是固定子串去抽取信息。数据分析库(比如 pandas 的 Series.str 系列方法)在需要正则时会调用这一套底层能力,参数细节以对应库的官方文档为准。本文只讲标准库 re,它是这一切的基础。

初学者最集中的误解是分不清 re.match 和 re.search:match 只从字符串开头 尝试匹配,search 才扫描整个字符串 找第一个匹配位置。所以 re.match(r"\d+", "abc123") 返回 None,而 re.search(r"\d+", "abc123") 能匹配到 123。这个区别让无数人在「为什么明明有数字却匹配不到」上卡了很久。

第二个误解是把正则当成字符串的一部分来写。如果模式里含 \d、\s 这类转义,必须用原始字符串 r"..." ,否则反斜杠会被 Python 的字符串转义先吃掉一层。本文把 re 的常用函数、匹配对象和常见陷阱讲清楚。

一、五个匹配函数的分工

re 模块在顶层提供了几个一次性函数,内部会把模式编译并(带缓存地)复用:

函数 从哪里开始匹配 成功返回 失败返回

|------------------|----------|----------|--------|
| re.match(p, s) | 只从开头 | Match 对象 | None |

|-------------------|----------------|----------|--------|
| re.search(p, s) | 扫描整个串,找第一处 | Match 对象 | None |

|----------------------|--------------|----------|--------|
| re.fullmatch(p, s) | 要求整串完全匹配 | Match 对象 | None |

|--------------------|-------------------|----|-----|
| re.findall(p, s) | 扫描全串,取所有非重叠匹配 | 列表 | 空列表 |

|---------------------|----------|-----|------|
| re.finditer(p, s) | 同上,但惰性产出 | 迭代器 | 空迭代器 |

python 复制代码
# 适用于 Python 3.8+

import re



print(re.match(r"\d+", "abc123"))      # None ------ 开头不是数字

print(re.search(r"\d+", "abc123"))     # <re.Match object; span=(3, 6), match='123'>

print(re.fullmatch(r"\d+", "123"))     # <re.Match ...> ------ 整串都是数字才匹配

print(re.fullmatch(r"\d+", "123a"))    # None

print(re.findall(r"\d+", "a1b22c333")) # ['1', '22', '333']

re.fullmatch 从 Python 3.4 起才有。要判断「整个字符串是否符合某格式」,用它比用 match 再补 $ 更清晰。

findall 的返回形状取决于模式里有几个捕获组(capturing group):

python 复制代码
# 适用于 Python 3.8+

print(re.findall(r"\d+", "a1b22"))            # ['1', '22']  无分组 -> 字符串列表

print(re.findall(r"(\d+)", "a1b22"))          # ['1', '22']  一个分组 -> 取分组内容

print(re.findall(r"(\w+)=(\d+)", "w=20 h=10"))  # [('w', '20'), ('h', '10')]  多分组 -> 元组列表

多分组时返回元组列表 这一点,是 findall 最常见的意外来源:本以为拿到的是整段匹配,结果拿到的是分组切片。

二、Match 对象怎么取值

search / match / fullmatch 成功时返回 Match 对象,取值靠这几个方法:

python 复制代码
# 适用于 Python 3.8+

import re



m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "今天日期 2026-10-07 天气晴")

if m:

    print(m.group(0))    # 2026-10-07   整个匹配

    print(m.group(1))    # 2026         第一个分组

    print(m.groups())    # ('2026', '10', '07')

    print(m.start(), m.end())   # 5 15   匹配在原串中的起止下标

用命名分组 可读性更好,语法是 (?P<名称>...),用名字取值:

python 复制代码
# 适用于 Python 3.8+

import re



m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-10")

if m:

    print(m.group("year"))    # 2026

    print(m["month"])         # 10      ------ 3.6+ 支持下标取分组

    print(m.groupdict())      # {'year': '2026', 'month': '10'}

分组可能不参与 匹配(比如整组是可选的),此时 groups() 里对应位置是 None,可以给默认值:m.groups("0")。

三、替换与切分

re.sub(pattern, repl, string, count=0, flags=0) 把匹配到的部分替换掉;count=0 表示不限次数。repl 既可以是字符串(支持 \1 这类反向引用,用原始字符串写),也可以是函数,函数接收 Match 对象并返回替换文本:

python 复制代码
# 适用于 Python 3.8+

import re



print(re.sub(r"\d+", "#", "a1b22c333"))          # 'a#b#c#'

print(re.sub(r"\d+", "#", "a1b22c333", count=1)) # 'a#b22c333'



# 用函数做「把价格上调 10%」这类动态替换

def bump(match):

    return str(int(match.group()) * 2)



print(re.sub(r"\d+", bump, "价 100 元"))          # '价 200 元'

re.split(pattern, string, maxsplit=0, flags=0) 用模式切分,和 str.split 不同的是它能按一类分隔符切:

python 复制代码
# 适用于 Python 3.8+

import re



print(re.split(r"\W+", "a, b; c"))     # ['a', 'b', 'c']

print(re.split(r"[,;]\s*", "a, b; c")) # ['a', 'b', 'c']

maxsplit 和 flags 从 Python 3.13 起不建议再按位置传,将来会变成仅关键字参数,新代码写成 re.split(p, s, maxsplit=1) 更稳。

四、编译模式与常用标志

模式会被 re 模块内部缓存(同一模式重复使用不会反复编译),但显式编译能省掉每次的查表开销,也更适合把模式定义在模块级常量:

python 复制代码
# 适用于 Python 3.8+

import re



DATE_RE = re.compile(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})")



def find_dates(text):

    return [m.groupdict() for m in DATE_RE.finditer(text)]



print(find_dates("2026-10-07 和 2025-01-02"))

# [{'y': '2026', 'm': '10', 'd': '07'}, {'y': '2025', 'm': '01', 'd': '02'}]

flags 用按位或组合,常用的有:

标志 作用

|-------------------------|-------|
| re.IGNORECASE(re.I) | 忽略大小写 |

|------------------------|-------------------------|
| re.MULTILINE(re.M) | 让 ^ $ 匹配每行首尾,而非整串首尾 |

|---------------------|--------------|
| re.DOTALL(re.S) | 让 . 能匹配换行符 |

|--------------------|------------------------------|
| re.ASCII(re.A) | 让 \d \w \s 只按 ASCII 解释 |

|----------------------|--------------|
| re.VERBOSE(re.X) | 允许在模式里换行、写注释 |

re.VERBOSE 对写复杂正则特别有用:

python 复制代码
# 适用于 Python 3.8+

import re



PHONE_RE = re.compile(r"""

    (?P<area>\d{3})    # 区号

    -                   # 横线

    (?P<num>\d{8})      # 号码

""", re.VERBOSE)



m = PHONE_RE.fullmatch("010-12345678")

print(m.group("area"))   # 010

五、实战:从脏文本里提取结构化字段

python 复制代码
# 适用于 Python 3.8+

import re



LOG_LINE = '2026-10-07 08:30:15 [ERROR] user=alice ip=192.168.1.7 msg="timeout"'



LINE_RE = re.compile(

    r"^(?P<date>\d{4}-\d{2}-\d{2})\s+"

    r"(?P<time>\d{2}:\d{2}:\d{2})\s+"

    r"\[(?P<level>[A-Z]+)\]\s+"

    r"user=(?P<user>\w+)\s+"

    r"ip=(?P<ip>[\d.]+)"

)



def parse_line(line):

    m = LINE_RE.match(line)

    if m is None:

        return None

    return m.groupdict()



print(parse_line(LOG_LINE))

# {'date': '2026-10-07', 'time': '08:30:15', 'level': 'ERROR',

#  'user': 'alice', 'ip': '192.168.1.7'}

print(parse_line("这不是日志"))   # None

几个要点:用命名分组让结果自带字段名;匹配失败返回 None 而不是抛异常,交给调用方决定怎么办;IP 这类「够用就好」的模式不用追求完全严谨(真正校验 IP 要按 0-255 逐段判断)。

如果只是要判断 子串在不在,别动正则------直接 in 或 str.startswith 就够,正则只在需要「模式」时才用。真要转义一个字面量 去匹配,用 re.escape(text),它会把里面的特殊字符全部转义,避免自己手写出错。

常见坑点

  1. match 与 search 混用

❌ re.match(r"\d+", "id=42") 得到 None,以为模式错了

✅ 不要求从开头匹配就用 re.search;要整串匹配用 re.fullmatch

  1. 模式不用原始字符串

❌ re.search("\d+", s) 里 \d 先被 Python 当成字符串转义处理,可能触发 SyntaxWarning 或行为异常

✅ 一律写 r"\d+"

  1. findall 有分组时返回形状变了

❌ 模式里写了 (\d+) 却期待 findall 返回整段匹配文本

✅ 记住:有 1 个分组返回分组内容列表,有多个分组返回元组列表;不想取分组就用非捕获组 (?:...)

  1. 忘了判 Match 是否为 None

❌ re.search(p, s).group() 没匹配到就 AttributeError: 'NoneType' object has no attribute 'group'

✅ 先 if m: 再取 m.group()

  1. . 匹配不到换行

❌ 用 r"<div>.*</div>" 抓跨行内容,什么都没抓到

✅ 加 re.DOTALL,或改用非贪婪 .*? 配合 re.DOTALL

  1. 贪婪匹配吃掉太多

❌ re.search(r"<b>.*</b>", "<b>a</b><b>b</b>") 匹配到 <b>a</b><b>b</b>

✅ 用非贪婪 r"<b>.*?</b>" 得到 <b>a</b>

  1. 以为 ^ $ 自动按行

❌ 对多行文本用 ^ERROR 想匹配每一行开头的 ERROR,只匹配到第一行

✅ 加 re.MULTILINE(注意 re.match 即使在 MULTILINE 下也只从整串开头试)

  1. 在循环里反复 compile 同一模式

❌ 循环体内每次都 re.compile(p),白白重复工作

✅ 把 re.compile() 提到循环外或模块级,复用编译结果

总结

需求 用法

|--------|----------------------|
| 整串完全匹配 | re.fullmatch(p, s) |

|-------|------------------|
| 从开头匹配 | re.match(p, s) |

|----------|-------------------|
| 任意位置找第一个 | re.search(p, s) |

|-------|------------------------------------------|
| 取所有匹配 | re.findall(p, s) / re.finditer(p, s) |

|----|-------------------------------|
| 替换 | re.sub(p, repl, s, count=0) |

|-------|------------------------------|
| 按模式切分 | re.split(p, s, maxsplit=0) |

|-----|------------------------|
| 预编译 | re.compile(p, flags) |

|-------|-------------------|
| 转义字面量 | re.escape(text) |

用 re 的心法只有三条:写模式一律用原始字符串 、先想清楚是 match 还是 search 、拿到 Match 前先判 None 。再记住 findall 在分组存在时返回形状会变、. 默认不匹配换行、.* 默认贪婪这几个细节,配合 re.VERBOSE 把复杂模式写成分行带注释的形式,正则在数据清洗里就会是一个非常可靠的工具,而不是「写完不敢改」的黑盒。

相关推荐
中屹指纹浏览器svip1 小时前
打开账号环境网页报错,常见原因有哪些?
服务器·开发语言·php·中屹指纹浏览器
许墨の小蝴蝶1 小时前
Pillow 替换图片文字后字号偏小的排查
python·pillow
朝朝辞暮i1 小时前
第 01 讲:Python 变量、对象、数据类型与内存引用
开发语言·python
外收内放1 小时前
Python基础语法练习题(69)
开发语言·python
lojox2 小时前
深入理解 C++ STL:vector 底层实现、迭代器失效与 list 特性
开发语言·c++
TomEval2 小时前
【测AI】第11篇:深度学习基础 —— 神经网络是怎么工作的
人工智能·python·深度学习·神经网络·机器学习·aigc
iotxiaohu2 小时前
用 cflow 查看 C 函数调用关系
c语言·开发语言·cflow
zhang-ge2 小时前
python包和项目管理器uv在ubuntu下的安装与使用
开发语言·python·uv
for_ever_love__2 小时前
LangChain 降级、重试与超时:让服务在模型抽风时还能用
python·自然语言处理·langchain·agent·超时·重试·with_retry