目录

Python 正则表达式

学习目标

  • 掌握正则表达式的基本语法
  • 学会使用 re 模块进行匹配、搜索和替换
  • 理解贪婪与非贪婪匹配
  • 掌握常用正则模式和实际应用

1. re 模块基础

1.1 匹配与搜索

import re

# re.match - 从字符串开头匹配
result = re.match(r'hello', 'hello world')
print(result)          # <re.Match object; span=(0, 5), match='hello'>
print(result.group())  # hello

# re.search - 搜索整个字符串
result = re.search(r'world', 'hello world')
print(result.group())  # world

# re.findall - 查找所有匹配
result = re.findall(r'\d+', 'abc 123 def 456')
print(result)  # ['123', '456']

# re.finditer - 返回迭代器
for match in re.finditer(r'\d+', 'abc 123 def 456'):
    print(f"找到: {match.group()} 在位置 {match.span()}")

1.2 替换与分割

import re

# re.sub - 替换
result = re.sub(r'\d+', '#', 'abc 123 def 456')
print(result)  # abc # def #

# 替换并限制次数
result = re.sub(r'\d+', '#', '1 2 3 4', count=2)
print(result)  # # # 3 4

# re.subn - 替换并返回次数
result = re.subn(r'\d+', '#', 'abc 123 def 456')
print(result)  # ('abc # def #', 2)

# re.split - 分割
result = re.split(r'\s+', 'hello   world  python')
print(result)  # ['hello', 'world', 'python']

2. 正则表达式语法

2.1 元字符

import re

# . 匹配任意字符(除换行)
print(re.findall(r'a.c', 'abc aac acc axc'))  # ['abc', 'aac', 'acc', 'axc']

# ^ 匹配开头  $ 匹配结尾
print(bool(re.match(r'^hello', 'hello world')))   # True
print(bool(re.search(r'world$', 'hello world')))  # True

# * 零次或多次  + 一次或多次  ? 零次或一次
print(re.findall(r'ab*c', 'ac abc abbc abbbc'))  # ['ac', 'abc', 'abbc', 'abbbc']
print(re.findall(r'ab+c', 'ac abc abbc'))         # ['abc', 'abbc']
print(re.findall(r'ab?c', 'ac abc abbc'))         # ['ac', 'abc']

# {n} 恰好n次  {n,} 至少n次  {n,m} n到m次
print(re.findall(r'a{3}', 'aa aaa aaaa'))         # ['aaa', 'aaa']
print(re.findall(r'a{2,}', 'a aa aaa'))           # ['aa', 'aaa']
print(re.findall(r'a{2,3}', 'a aa aaa aaaa'))     # ['aa', 'aaa', 'aaa']

2.2 字符类

import re

# [...] 字符集
print(re.findall(r'[aeiou]', 'hello world'))  # ['e', 'o', 'o']

# [^...] 否定字符集
print(re.findall(r'[^aeiou]', 'hello'))  # ['h', 'l', 'l']

# \d 数字  \D 非数字
print(re.findall(r'\d+', 'age: 25, score: 90'))  # ['25', '90']

# \w 单词字符 [a-zA-Z0-9_]  \W 非单词字符
print(re.findall(r'\w+', 'hello world!'))  # ['hello', 'world']

# \s 空白字符  \S 非空白字符
print(re.findall(r'\S+', 'hello   world'))  # ['hello', 'world']

# 预定义字符集
# \d = [0-9]
# \w = [a-zA-Z0-9_]
# \s = [ \t\n\r\f\v]

2.3 分组与引用

import re

# (...) 捕获分组
result = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-01-15')
print(result.group(0))  # 2024-01-15(完整匹配)
print(result.group(1))  # 2024(第一组)
print(result.group(2))  # 01(第二组)
print(result.group(3))  # 15(第三组)
print(result.groups())  # ('2024', '01', '15')

# 命名分组 (?P<name>...)
result = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', '2024-01')
print(result.group('year'))   # 2024
print(result.group('month'))  # 01

# (?:...) 非捕获分组
result = re.findall(r'(?:https?://)(\w+\.\w+)', 'https://google.com http://test.org')
print(result)  # ['google.com', 'test.org']

# \1 \2 反向引用
print(re.findall(r'(\w+) \1', 'hello hello world world'))  # ['hello', 'world']

3. 贪婪与非贪婪

import re

text = '<div>content1</div><div>content2</div>'

# 贪婪匹配(默认)- 匹配尽可能长的字符串
print(re.findall(r'<div>.*</div>', text))
# ['<div>content1</div><div>content2</div>']

# 非贪婪匹配 - 匹配尽可能短的字符串
print(re.findall(r'<div>.*?</div>', text))
# ['<div>content1</div>', '<div>content2</div>']

# 其他非贪婪量词
# *?  +?  ??  {n,m}?
text = '"hello" "world"'
print(re.findall(r'".*?"', text))  # ['"hello"', '"world"']

4. 编译与标志

4.1 编译正则

import re

# 编译正则(提高效率,适合重复使用)
pattern = re.compile(r'\d{4}-\d{2}-\d{2}')

# 使用编译后的对象
print(pattern.match('2024-01-15'))
print(pattern.search('date: 2024-01-15'))
print(pattern.findall('2024-01-15 2024-02-20'))

4.2 标志位

import re

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

# re.DOTALL / re.S - . 匹配换行
print(re.findall(r'a.b', 'a\nb', re.S))  # ['a\nb']

# re.MULTILINE / re.M - ^$ 匹配每行开头结尾
text = 'line1\nline2\nline3'
print(re.findall(r'^line\d', text, re.M))  # ['line1', 'line2', 'line3']

# 多个标志组合
print(re.findall(r'hello.world', 'HELLO\nWORLD', re.I | re.S))

5. 实际应用

5.1 邮箱验证

import re

def validate_email(email):
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

# 测试
print(validate_email('test@example.com'))      # True
print(validate_email('invalid.email'))          # False
print(validate_email('user.name+tag@domain.co')) # True

5.2 手机号提取

import re

def extract_phones(text):
    # 中国手机号
    pattern = r'1[3-9]\d{9}'
    return re.findall(pattern, text)

text = "联系我:13800138000 或 15912345678"
print(extract_phones(text))  # ['13800138000', '15912345678']

5.3 URL 解析

import re

def parse_url(url):
    pattern = r'(?P<protocol>https?)://(?P<host>[^:/\s]+)(?::(?P<port>\d+))?(?P<path>/[^\s]*)?'
    match = re.match(pattern, url)
    if match:
        return match.groupdict()
    return None

url = "https://example.com:8080/path/to/page?query=1"
print(parse_url(url))
# {'protocol': 'https', 'host': 'example.com', 'port': '8080', 'path': '/path/to/page?query=1'}

5.4 日志解析

import re
from datetime import datetime

def parse_log_line(line):
    """解析 Nginx 日志"""
    pattern = r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<size>\d+)'
    match = re.match(pattern, line)
    return match.groupdict() if match else None

log = '192.168.1.1 - - [15/Jan/2024:14:30:00 +0800] "GET /index.html HTTP/1.1" 200 1234'
result = parse_log_line(log)
print(result)

6. 正则最佳实践

import re

# 1. 使用原始字符串
# 推荐
pattern = r'\d+\w+'
# 不推荐
pattern = '\\d+\\w+'

# 2. 复杂正则应编译
EMAIL_PATTERN = re.compile(r'^[\w.-]+@[\w.-]+\.\w{2,}$')

# 3. 使用注释模式(VERBOSE)
PHONE_PATTERN = re.compile(r'''
    ^
    (\+?86)?           # 可选的国家代码
    [-\s]?             # 可选的分隔符
    1[3-9]\d{9}        # 手机号
    $
''', re.VERBOSE)

# 4. 处理匹配失败
match = re.search(r'\d+', 'no numbers')
if match:
    print(match.group())
else:
    print("未找到匹配")

本节小结

  • 匹配函数match(开头)、search(全局)、findallfinditer
  • 修改函数sub(替换)、split(分割)
  • 元字符. ^ $ * + ? {} [] \ | ()
  • 分组:捕获分组 ()、命名分组 (?P<name>...)、非捕获 (?:...)
  • 贪婪* + ? 默认贪婪,加 ? 变非贪婪
  • 编译:频繁使用的正则应编译,支持标志位

练习

  1. 编写正则验证中国大陆身份证号(18位)
  2. 使用正则从 HTML 中提取所有链接的 href 和文本
  3. 编写函数,将驼峰命名转换为下划线命名(camelCase -> snake_case)
  4. 使用正则解析 Markdown 中的标题、链接和图片