65_Python正则表达式入门

Python正则表达式入门:从模式匹配到数据提取实战

文章目录

前言

正则表达式(Regular Expression)是处理文本的瑞士军刀。无论是验证用户输入、提取网页数据、清洗日志文件,还是批量替换文本,正则表达式都能用几行代码完成看似复杂的任务。Python内置的 re 模块提供了完整的正则表达式支持。

学习正则表达式的现实意义:在面试中,正则表达式是常见考点;在爬虫开发中,它是数据提取的核心工具;在日志分析中,它是错误排查的利器。然而,正则表达式也有一个"名声"------它看起来像天书,容易把简单问题复杂化。本文的目标是让你掌握正则的核心套路,学会"用合适的工具做合适的事",避免陷入过度依赖正则的陷阱。本文将从零带你掌握正则表达式的核心用法。

一、re 模块快速上手

1.1 第一个正则示例

正则表达式最简单的入门方式就是从一个具体的例子开始。下面这个例子演示了如何从一段文本中提取手机号码------这是正则表达式最经典的应用场景之一。

python 复制代码
import re

text = "我的手机号是13812345678,请尽快联系我"

# 匹配中国大陆手机号
pattern = r"1[3-9]\d{9}"
match = re.search(pattern, text)

if match:
    print(f"找到手机号: {match.group()}")  # 找到手机号: 13812345678
    print(f"起始位置: {match.start()}")     # 7
    print(f"结束位置: {match.end()}")       # 18

关键解析re.search() 在整个字符串中搜索第一个匹配,返回一个 Match 对象。如果没找到匹配,它返回 None------所以在生产代码中务必先检查返回值是否为 None ,否则直接调用 .group() 会抛出 AttributeError

1.2 原生字符串 r"..."

正则表达式中大量使用反斜杠,使用原生字符串 r"..." 可以避免转义混乱:

python 复制代码
# 繁琐的普通字符串
pattern1 = "\\d+\\.\\d+"

# 简洁的原生字符串(推荐)
pattern2 = r"\d+\.\d+"

print(pattern1 == pattern2)  # True

二、核心匹配函数

re 模块提供了多个匹配函数,各自有不同的语义和使用场景。理解它们的区别是正则入门的关键。常见面试题 :"match()search() 有什么区别?"------答案很简单,match() 只从字符串开头匹配,search() 搜索整个字符串。

2.1 search():搜索第一个匹配

python 复制代码
text = "价格:¥19.9,折扣价:¥9.9"
pattern = r"¥(\d+\.\d+)"

match = re.search(pattern, text)
print(match.group())   # ¥19.9
print(match.group(1))  # 19.9 (第一个捕获组)

2.2 match():从字符串开头匹配

python 复制代码
# match() 只匹配开头
print(re.match(r"\d+", "123abc"))   # <re.Match object>
print(re.match(r"\d+", "abc123"))   # None (不在开头)

# search() 搜索任意位置
print(re.search(r"\d+", "abc123"))  # <re.Match object>

2.3 findall():查找所有匹配

python 复制代码
text = "我有苹果5个,橘子12个,香蕉8个"
pattern = r"(\w+)(\d+)个"

# 有捕获组时返回元组列表
result = re.findall(pattern, text)
print(result)
# [('苹果', '5'), ('橘子', '12'), ('香蕉', '8')]

for fruit, count in result:
    print(f"{fruit}: {count}个")

2.4 finditer():返回迭代器(推荐大文本使用)

python 复制代码
text = "Python 3.9, Python 3.10, Python 3.11"
pattern = r"Python (\d+\.\d+)"

for match in re.finditer(pattern, text):
    print(f"版本 {match.group(1)} 位于位置 {match.start()}")

2.5 sub():替换匹配内容

python 复制代码
# 脱敏手机号
text = "联系人: 张三 13812345678, 李四 13987654321"
masked = re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", text)
print(masked)  # 联系人: 张三 138****5678, 李四 139****4321

# 使用函数进行替换
def censor(match):
    word = match.group()
    return word[0] + "*" * (len(word) - 1)

text = "这个项目简直太棒了,效率非常高"
result = re.sub(r"[了得]", censor, text)
print(result)  # 这个项目简直太棒*,效率非常高

三、常用正则模式速查

正则表达式的元字符是构建匹配模式的"积木"。这些符号初看可能很抽象,但每个都有明确的使用场景。学习技巧 :不要试图一次记住所有元字符------先熟练 \d\w.*+? 这几个最常用的,剩下的需要时再查即可。

3.1 字符类

python 复制代码
patterns = {
    r"\d":     "匹配任意数字 [0-9]",
    r"\D":     "匹配任意非数字",
    r"\w":     "匹配字母数字下划线 [a-zA-Z0-9_]",
    r"\W":     "匹配非字母数字下划线",
    r"\s":     "匹配空白字符(空格、制表符、换行等)",
    r"\S":     "匹配非空白字符",
    r".":      "匹配任意字符(除换行符)",
}

test = "A 1 _ 中 @"
print(re.findall(r"\d", test))  # ['1']
print(re.findall(r"\w", test))  # ['A', '1', '_', '中']
print(re.findall(r"\s", test))  # [' ', ' ', ' ', ' ']

3.2 量词

python 复制代码
test_cases = {
    r"a*":    "a出现0次或多次",
    r"a+":    "a出现1次或多次",
    r"a?":    "a出现0次或1次",
    r"a{3}":   "a恰好出现3次",
    r"a{2,4}": "a出现2到4次",
    r"a{2,}":  "a出现至少2次",
}

print(re.findall(r"a*", "baaac"))    # ['', 'aaa', '', '']
print(re.findall(r"a+", "baaac"))    # ['aaa']
print(re.findall(r"a{2,3}", "baaaaac"))  # ['aaa']
print(re.findall(r"\d{3,4}", "电话: 010-1234567"))  # ['010', '1234']

3.3 边界与锚点

边界匹配是正则表达式中的重要概念,它不消耗字符,只匹配"位置"。^ 匹配行首,$ 匹配行尾,\b 匹配单词边界。理解边界匹配对于精确匹配非常重要------比如你想匹配独立的单词 "cat" 而不是 "category" 中的 "cat",就需要用到 \bcat\bre.MULTILINE 标志让 ^$ 匹配每行的开头和结尾(而非整个字符串的开头和结尾)。

python 复制代码
test = """第一行
第二行
第三行"""

# ^ 匹配行首,$ 匹配行尾
print(re.findall(r"^第.", test, re.MULTILINE))
# ['第一', '第二', '第三']

# \b 匹配单词边界
text = "cat category scatter cat"
print(re.findall(r"\bcat\b", text))  # ['cat', 'cat']

3.4 贪婪与非贪婪

这是正则表达式中最经典的"坑"之一。贪婪匹配 是正则的默认行为------量词(*, +, ?, {})会尽可能多地匹配字符。这在很多场景下会导致意料之外的结果,比如在HTML中使用 .* 匹配时可能跨过多个标签。非贪婪匹配 通过在量词后加 ? 实现(如 *?, +?, ??),它会尽可能少地匹配。一个实用的经验法则:在HTML/XML解析、引号内容提取等场景中,几乎总是应该使用非贪婪匹配。

python 复制代码
html = "<div>内容1</div><div>内容2</div>"

# 贪婪匹配(默认):匹配尽可能多的内容
greedy = re.findall(r"<div>.*</div>", html)
print(greedy)  # ['<div>内容1</div><div>内容2</div>']

# 非贪婪匹配:加 ? 匹配尽可能少的内容
lazy = re.findall(r"<div>.*?</div>", html)
print(lazy)  # ['<div>内容1</div>', '<div>内容2</div>']

四、捕获组与高级用法

4.1 分组捕获

python 复制代码
# 提取日期
text = "今天是2024-01-15,明天是2024-01-16"
pattern = r"(\d{4})-(\d{2})-(\d{2})"

for match in re.finditer(pattern, text):
    year, month, day = match.groups()
    print(f"{year}年{month}月{day}日")

4.2 命名分组

python 复制代码
# 使用 (?P<name>...) 给分组命名
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"

text = "出生日期: 1995-08-22"
match = re.search(pattern, text)
if match:
    print(match.group("year"))    # 1995
    print(match.group("month"))   # 08
    print(match.group("day"))     # 22
    print(match.groupdict())
    # {'year': '1995', 'month': '08', 'day': '22'}

4.3 前瞻与后顾

python 复制代码
# 正向肯定前瞻 (?=...):后面必须跟着...
text = "100元 200美元 300元 400欧元"
# 匹配后面跟着"元"的数字
result = re.findall(r"\d+(?=元)", text)
print(result)  # ['100', '300']

# 正向肯定后顾 (?<=...):前面必须是...
# 匹配前面是"$"的数字
text = "商品: $199, $299, ¥599"
result = re.findall(r"(?<=\$)\d+", text)
print(result)  # ['199', '299']

# 负向前瞻 (?!...):后面不能跟着...
text = "Python3 Python2 Python"
result = re.findall(r"Python(?!\d)", text)
print(result)  # ['Python']  (不匹配后面有数字的)

4.4 常用模式速查表

模式 含义 示例
\d{3,4} 3-4位数字 区号
[\u4e00-\u9fa5] 中文字符 匹配中文
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} Email地址 基本邮箱匹配
1[3-9]\d{9} 中国大陆手机号 13812345678
\d{17}[\dXx] 18位身份证 身份证号
https?://[^\s]+ URL 网址

五、编译正则:提升性能

频繁使用的正则表达式应该预编译re.compile() 将正则模式编译为一个正则对象,后续可以反复使用,避免每次调用 re.match() 时重复解析和编译。在大批量文本处理中,预编译能带来显著的性能提升------在循环内重复使用同一个正则表达式时,编译版本比未编译版本快30%-50%。最佳实践:将编译后的正则对象定义为模块级别的常量,在整个程序中复用。

python 复制代码
import time

# 预编译
email_pattern = re.compile(
    r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)

emails = [
    "user@example.com",
    "invalid-email",
    "test@company.co.uk",
    "no@domain",
    "good@email.org",
]

for email in emails:
    is_valid = bool(email_pattern.match(email))
    status = "✓" if is_valid else "✗"
    print(f"{status} {email}")

# 性能测试
test_text = "hello@world.com " * 10000

start = time.time()
for _ in range(1000):
    re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', "test@example.com")
print(f"未编译耗时: {time.time() - start:.3f}s")

start = time.time()
for _ in range(1000):
    email_pattern.match("test@example.com")
print(f"编译后耗时: {time.time() - start:.3f}s")

六、实战案例:简易网页爬虫辅助工具

下面的实战案例将前面学到的知识整合成几个实用的函数------邮箱提取、URL提取、HTML表格解析、敏感信息脱敏。这些函数都是实际爬虫和数据清洗项目中可以直接复用的工具代码。

python 复制代码
import re


def extract_emails(text):
    """从文本中提取所有邮箱地址"""
    pattern = re.compile(
        r'\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b'
    )
    return pattern.findall(text)


def extract_urls(text):
    """从文本中提取所有URL"""
    pattern = re.compile(
        r'https?://[^\s<>"\']+|www\.[^\s<>"\']+'
    )
    return pattern.findall(text)


def parse_html_table(html):
    """解析简易HTML表格"""
    # 提取所有行
    rows = re.findall(r'<tr>(.*?)</tr>', html, re.DOTALL)

    table_data = []
    for row in rows:
        # 提取每行中的单元格
        cells = re.findall(r'<t[dh]>(.*?)</t[dh]>', row, re.DOTALL)
        # 清理HTML标签
        clean_cells = [re.sub(r'<.*?>', '', cell).strip() for cell in cells]
        table_data.append(clean_cells)

    return table_data


def mask_sensitive_info(text):
    """脱敏处理:隐藏身份证号、手机号、银行卡号"""
    text = re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', text)
    text = re.sub(r'(\d{6})\d{8}(\d{4})', r'\1********\2', text)
    text = re.sub(r'(\d{4})\s?(\d{4})\s?(\d{4})\s?(\d{4})',
                  r'\1 **** **** \4', text)
    return text


# 测试
sample = """
联系人: 张三, 邮箱: zhang@test.com
官网: https://www.example.com
手机: 13812345678
身份证: 320102199001011234

<table>
<tr><th>姓名</th><th>分数</th></tr>
<tr><td>张三</td><td>95</td></tr>
<tr><td>李四</td><td>88</td></tr>
</table>
"""

print("提取邮箱:", extract_emails(sample))
print("提取URL:", extract_urls(sample))
print("解析表格:", parse_html_table(sample))
print("脱敏后:", mask_sensitive_info(sample))

总结

正则表达式是文本处理的利器,Python re 模块的核心API:

  • search() 查找第一个匹配,findall() 查找所有匹配
  • sub() 替换匹配内容,split() 按模式分割字符串
  • 使用 r"..." 原生字符串避免转义困扰
  • 频繁使用的正则用 re.compile() 预编译提升性能
  • 非贪婪匹配 *?+? 在HTML解析等场景中非常实用

正则表达式初看可能有些晦涩,但一旦掌握,处理文本的效率和优雅程度都会显著提升。建议收藏本文用作速查参考。下一篇我们将进入多线程与并发编程的世界。

✅ 亮点总结

  • 系统讲解 re 模块五大核心函数(search/findall/sub/split/match),附带速查表
  • 元字符、量词、分组、零宽断言循序渐进,从基础模式到复杂匹配
  • 预编译 re.compile() 提升性能,非贪婪匹配解决 HTML 解析中的常见坑
  • 实战案例:邮箱/URL 提取、敏感信息脱敏、HTML 表格解析,即学即用

适用场景

  • 数据清洗:从日志/爬虫结果中提取结构化字段(日期、IP、金额等)
  • 表单验证:校验用户输入的手机号、邮箱、身份证号等格式合法性
  • 代码重构:批量替换项目中的旧 API 调用、统一代码风格

扩展方向

  • 学习正则表达式的性能优化,了解回溯陷阱和原子组
  • 结合 re 的 flags 参数掌握多行匹配、忽略大小写等高级模式
  • 探索 regex 第三方库,支持更丰富的正则特性(如递归匹配、模糊匹配)