Python 正则表达式:从入门到实战的进阶指南

正则表达式这东西,在程序员圈子里总带着点神秘色彩。有人把它当成处理字符串的瑞士军刀,几行代码就能搞定别人几十行才能完成的文本处理;也有人对着那些夹杂着星号、反斜杠的字符组合望而却步,每次遇到都得靠搜索引擎抄现成的代码。
正则表达式的本质:一种字符串模式描述语言
我们每天处理的字符串里藏着很多规律:邮箱地址里一定有 @符号,手机号通常是 11 位数字,URL 开头总会是 http 或者 https。正则表达式的作用,就是用一套标准化的符号系统来描述这些规律。
比如要检查一个字符串是不是有效的手机号,不用正则的话可能需要写这样的代码:
python
def is_phone_number(s):
if len(s) != 11:
return False
for c in s:
if not c.isdigit():
return False
return True
而用正则表达式,一行代码就能解决:
python
import re
def is_phone_number(s):
return bool(re.match(r'^\d{11}$', s))
这里的^\d{11} <math xmlns="http://www.w3.org/1998/Math/MathML"> 就是一个正则模式,它的意思是:从字符串开头 ( ) 到结尾( 就是一个正则模式,它的意思是:从字符串开头(^)到结尾( </math>就是一个正则模式,它的意思是:从字符串开头()到结尾()必须是 11 个({11})数字(\d)。这种描述方式比传统代码更直接,也更高效。
正则表达式的基础语法:构建模式的积木
正则表达式的语法看起来复杂,其实是由一系列基础元素组合而成的,就像用乐高积木搭建复杂模型一样。
普通字符与转义字符
大部分字符在正则表达式中表示它们自己,比如abc就匹配 "abc" 这个字符串。但有些字符有特殊含义,比如.、*、?等,这些被称为元字符。如果需要匹配这些元字符本身,就需要用反斜杠\进行转义。
比如要匹配字符串中的句号,就需要写成.:
python
# 匹配包含句号的句子
text = "Hello world. This is a test."
pattern = r'.' # 注意使用原始字符串r""避免转义问题
matches = re.findall(pattern, text)
print(matches) # 输出: ['.', '.']
字符集:匹配多种可能的字符
用方括号[]可以定义一个字符集,表示匹配其中任意一个字符。比如[abc]匹配 a、b 或 c 中的任意一个,[0-9]匹配任意数字。
字符集里还可以用短横线-表示范围,[a-z]匹配任意小写字母,[A-Za-z]匹配任意大小写字母。如果在字符集开头加上^,则表示匹配除了字符集中的字符之外的任意字符,比如[^0-9]匹配非数字字符。
python
# 提取字符串中的所有字母
text = "abc123def456ghi"
pattern = r'[A-Za-z]'
letters = re.findall(pattern, text)
print(''.join(letters)) # 输出: abcdefghi
# 提取非数字字符
non_digits = re.findall(r'[^0-9]', text)
print(''.join(non_digits)) # 输出: abcdefghi
预定义字符集:常用模式的缩写
正则表达式定义了一些预定义的字符集缩写,让模式更简洁:
- \d:匹配任意数字(等价于[0-9])
- \D:匹配任意非数字(等价于[^0-9])
- \w:匹配字母、数字或下划线(等价于[a-zA-Z0-9_])
- \W:匹配非字母、数字和下划线
- \s:匹配空白字符(空格、制表符\t、换行符\n等)
- \S:匹配非空白字符
这些缩写在实际使用中非常频繁,比如提取字符串中的所有单词:
python
text = "Hello, world! This is a test."
# 匹配由字母组成的单词
words = re.findall(r'\w+', text)
print(words) # 输出: ['Hello', 'world', 'This', 'is', 'a', 'test']
量词:控制匹配次数
量词用来指定前面的元素需要匹配多少次,这是正则表达式强大功能的核心之一:
- *:匹配前面的元素 0 次或多次
- +:匹配前面的元素 1 次或多次
- ?:匹配前面的元素 0 次或 1 次
- {n}:匹配前面的元素恰好 n 次
- {n,}:匹配前面的元素至少 n 次
- {n,m}:匹配前面的元素至少 n 次,至多 m 次
比如验证密码强度,要求密码长度在 8-16 位之间,包含字母和数字:
python
def is_strong_password(password):
# 至少8位,至多16位,包含至少一个字母和一个数字
pattern = r'^(?=.*[A-Za-z])(?=.*\d).{8,16}$'
return bool(re.match(pattern, password))
print(is_strong_password("1234567")) # False(长度不足)
print(is_strong_password("abcdefgh")) # False(没有数字)
print(is_strong_password("abc12345")) # True(符合要求)
这里的(?=.[A-Za-z])是一种正向预查(lookahead),表示 "后面必须有至少一个字母",类似的(?=.\d)表示 "后面必须有至少一个数字"。
贪婪匹配与非贪婪匹配
正则表达式默认是贪婪的,也就是会尽可能匹配最长的字符串。在量词后面加上?可以启用非贪婪模式,即尽可能匹配最短的字符串。
python
text = "<div>内容1</div><div>内容2</div>"
# 贪婪匹配:会匹配从第一个<div>到最后一个</div>的整个字符串
greedy_match = re.findall(r'<div>.*</div>', text)
print(greedy_match) # 输出: ['<div>内容1</div><div>内容2</div>']
# 非贪婪匹配:在*后面加?,只匹配到第一个</div>
non_greedy_match = re.findall(r'<div>.*?</div>', text)
print(non_greedy_match) # 输出: ['<div>内容1</div>', '<div>内容2</div>']
这个特性在处理 HTML 标签、XML 元素等成对出现的结构时特别有用。
位置匹配:定位字符串中的特定位置
有时我们需要匹配字符串中的特定位置而不是字符本身,比如单词的开头或结尾:
- ^:匹配字符串的开头
- $:匹配字符串的结尾
- \b:匹配单词边界(单词和非单词字符之间的位置)
- \B:匹配非单词边界
python
text = "cat category concatenate"
# 匹配单独的"cat"单词,不包含在其他单词中
pattern = r'\bcat\b'
matches = re.findall(pattern, text)
print(matches) # 输出: ['cat']
# 匹配包含"cat"的单词
pattern = r'\w*cat\w*'
matches = re.findall(pattern, text)
print(matches) # 输出: ['cat', 'category', 'concatenate']
分组与捕获:提取匹配的特定部分
用圆括号()可以将正则表达式的一部分括起来形成分组,这样我们可以单独提取匹配结果中的特定部分。
python
text = "张三:13812345678,李四:13987654321"
# 匹配姓名和电话,分成两个分组
pattern = r'(\w+):(\d{11})'
matches = re.findall(pattern, text)
for name, phone in matches:
print(f"{name}的电话是{phone}")
# 输出:
# 张三的电话是13812345678
# 李四的电话是13987654321
分组还可以配合|实现 "或" 逻辑,比如(a|b)匹配 a 或者 b。
Python 的 re 模块:正则表达式的实现工具
Python 通过内置的re模块提供了正则表达式支持,掌握这个模块的常用函数是实际应用的关键。
re.match () 与 re.search ():查找匹配
re.match()从字符串的开头开始匹配,如果开头不匹配就返回 None;re.search()则在整个字符串中查找第一个匹配项。
python
text = "abc123def456"
# 从开头匹配字母
match = re.match(r'[A-Za-z]+', text)
print(match.group()) # 输出: abc
# 在整个字符串中查找数字
search = re.search(r'\d+', text)
print(search.group()) # 输出: 123
# 尝试从开头匹配数字(失败)
no_match = re.match(r'\d+', text)
print(no_match) # 输出: None
这两个函数返回的是Match对象,通过group()方法可以获取匹配的内容,start()和end()方法可以获取匹配的位置。
re.findall () 与 re.finditer ():查找所有匹配
re.findall()返回所有匹配项组成的列表;re.finditer()返回一个迭代器,每次迭代返回一个Match对象,适合处理大量匹配结果。
python
text = "价格: 99元, 折扣: 8折, 最终: 79.2元"
# 提取所有数字(包括整数和小数)
numbers = re.findall(r'\d+.?\d*', text)
print(numbers) # 输出: ['99', '8', '79.2']
# 使用finditer处理
for match in re.finditer(r'(\d+.?\d*)(元|折)', text):
value = match.group(1)
unit = match.group(2)
print(f"数值: {value}, 单位: {unit}")
# 输出:
# 数值: 99, 单位: 元
# 数值: 8, 单位: 折
# 数值: 79.2, 单位: 元
re.sub () 与 re.subn ():替换匹配内容
re.sub()用于替换匹配的内容,返回替换后的字符串;re.subn()则返回一个元组(替换后的字符串, 替换次数)。
python
text = "Python是一门很棒的语言,Python非常流行"
# 替换Python为Python3
new_text = re.sub(r'Python', 'Python3', text)
print(new_text) # 输出: Python3是一门很棒的语言,Python3非常流行
# 只替换第一个匹配
new_text = re.sub(r'Python', 'Python3', text, count=1)
print(new_text) # 输出: Python3是一门很棒的语言,Python非常流行
# 获取替换次数
new_text, count = re.subn(r'Python', 'Python3', text)
print(f"替换了{count}次,结果: {new_text}")
# 输出: 替换了2次,结果: Python3是一门很棒的语言,Python3非常流行
re.sub()的第二个参数还可以是一个函数,根据匹配内容动态生成替换值:
python
# 将数字乘以2
def double_number(match):
return str(float(match.group()) * 2)
text = "a:10, b:20, c:30"
new_text = re.sub(r'\d+', double_number, text)
print(new_text) # 输出: a:20.0, b:40.0, c:60.0
re.split ():根据匹配分割字符串
re.split()使用正则表达式匹配的内容作为分隔符分割字符串,比普通的str.split()更灵活。
python
text = "apple, banana; orange| grape"
# 用逗号、分号或竖线作为分隔符
fruits = re.split(r'[,;|]', text)
# 去除空格
fruits = [f.strip() for f in fruits]
print(fruits) # 输出: ['apple', 'banana', 'orange', 'grape']
编译正则表达式:提高效率的技巧
如果需要多次使用同一个正则表达式,建议用re.compile()编译成一个Pattern对象,这样可以提高执行效率,还能使用更多参数配置匹配方式。
python
# 编译一个不区分大小写的正则表达式
pattern = re.compile(r'hello', re.IGNORECASE)
print(pattern.match("Hello")) # 匹配成功
print(pattern.match("HELLO")) # 匹配成功
print(pattern.match("hello")) # 匹配成功
re.compile()支持的常用标志包括:
- re.IGNORECASE(re.I):不区分大小写
- re.DOTALL(re.S):让.匹配包括换行符在内的所有字符
- re.MULTILINE(re.M):让^和$匹配每行的开头和结尾
- re.VERBOSE(re.X):允许编写多行正则表达式并添加注释
python
# 使用VERBOSE模式编写更易读的正则表达式
pattern = re.compile(r'''
\d{3} # 区号的前三位
- # 连接符
\d{4} # 中间四位
- # 连接符
\d{4} # 最后四位
''', re.VERBOSE)
print(pattern.match("138-1234-5678")) # 匹配成功
结合力扣题目理解正则表达式
题目一:验证回文串(LeetCode 125)
题目描述:给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
解题思路:
- 先过滤掉字符串中的非字母和数字字符
- 将字符串转换为小写(或大写)
- 判断处理后的字符串是否是回文
使用正则表达式可以很方便地过滤非字母和数字字符:
python
import re
def isPalindrome(s: str) -> bool:
# 过滤非字母和数字字符,转换为小写
s_clean = re.sub(r'[^a-zA-Z0-9]', '', s).lower()
# 判断回文
return s_clean == s_clean[::-1]
# 测试
print(isPalindrome("A man, a plan, a canal: Panama")) # True
print(isPalindrome("race a car")) # False
这里的[^a-zA-Z0-9]表示匹配所有非字母和非数字的字符,用空字符串替换后就得到了只包含字母和数字的字符串。
题目二:字符串转换整数 (atoi)(LeetCode 8)
题目描述:请你来实现一个 myAtoi (string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。
解题思路:
- 忽略前导空格
- 检查正负号
- 提取数字部分
- 处理溢出情况
使用正则表达式可以一步提取出符合要求的数字部分:
python
import re
def myAtoi(s: str) -> int:
# 正则匹配:可选的正负号开头,后面跟数字
pattern = r'^[+-]?\d+'
# 查找匹配
match = re.match(pattern, s.lstrip()) # lstrip()去除前导空格
if not match:
return 0
# 转换为整数
num = int(match.group())
# 处理溢出
INT_MAX = 2**31 - 1
INT_MIN = -2**31
return min(max(num, INT_MIN), INT_MAX)
# 测试
print(myAtoi("42")) # 42
print(myAtoi("</doubaocanvas>