正则表达式:为什么它成了程序员的 “分水岭”?

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)

题目描述:给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

解题思路

  1. 先过滤掉字符串中的非字母和数字字符
  1. 将字符串转换为小写(或大写)
  1. 判断处理后的字符串是否是回文

使用正则表达式可以很方便地过滤非字母和数字字符:

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 函数)。

解题思路

  1. 忽略前导空格
  1. 检查正负号
  1. 提取数字部分
  1. 处理溢出情况

使用正则表达式可以一步提取出符合要求的数字部分:

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>
相关推荐
Mr_Chenph4 分钟前
Qdrant Filtering:must / should / must_not 全解析(含 Python 实操)
python·filter·qdrant
我今晚不熬夜16 分钟前
使用单调栈解决力扣第42题--接雨水
java·数据结构·算法·leetcode
今夕节度使24 分钟前
Axure 11
python
Python当打之年27 分钟前
工具分享05 | Python制作PDF合并拆分提取工具V1.0
python·pdf
程序员黄同学1 小时前
Python 的列表 list 和元组 tuple 有啥本质区别?啥时候用谁更合适?
windows·python·list
flashlight_hi1 小时前
LeetCode 分类刷题:209. 长度最小的子数组
javascript·算法·leetcode
万能程序员-传康Kk1 小时前
美团末端配送碳排放评估
python
展信佳_daydayup2 小时前
0-1 深度学习基础——文件读取
算法
高斯林.神犇2 小时前
冒泡排序实现以及优化
数据结构·算法·排序算法
Github项目推荐2 小时前
跨平台Web服务开发的新选择(5802)
算法·架构