第一部分:初识正则表达式 (Regular Expressions)
1.1 什么是正则表达式?
正则表达式,通常缩写为 "regex" 或 "regexp",本质上是一种微型的、高度专业化的编程语言,它被内嵌在Python语言中,并通过 re 模块来提供。它不是Python独有的,但Python的re模块为它提供了强大的实现。
简单来说,正则表达式是一套用于描述字符串模式的规则。你可以把它想象成一个超级通配符。Windows系统中的 *.txt 就是一个简单的模式,而正则表达式可以定义远比这复杂得多的模式。
1.2 为什么需要正则表达式?
正则表达式是处理文本数据的“瑞士军刀”。在数据清洗、文本提取、格式验证等场景中,它几乎是不可或缺的工具。你可以用它来:
- 验证:检查用户输入是否符合规则,如邮箱、手机号、密码强度。
- 查找:从大段文本中提取所有符合特定模式的信息,如所有网址、日期、IP地址。
- 替换:批量地将文本中符合模式的内容替换为其他内容,如屏蔽敏感词、格式化日期。
- 分割:根据复杂的分隔符模式来拆分字符串。
1.3 第一个正则表达式:匹配字符
最简单的正则表达式就是直接匹配自身。例如,正则表达式 test 会精确地在文本中查找字符串 test。但是,正则表达式的威力在于它拥有元字符 (metacharacters)。这些特殊字符不匹配自身,而是代表了特定的匹配规则。
所有元字符列表如下,我们将在后续部分逐一详解:
. ^ $ * + ? { } [ ] \\ | ( )
第二部分:正则表达式语法精要 (核心基础)
这是正则表达式的基石,必须熟练掌握。
2.1 普通字符与元字符
- 普通字符:字母、数字、下划线等,直接匹配自身,如 abc 匹配 "abc"。
- 元字符:具有特殊含义的字符,如 . 匹配任何单个字符(除换行符)。
2.2 字符类 (Character Classes) – […]
使用方括号 […] 定义一个字符集合,匹配其中任意一个字符。
- 示例:[abc] 匹配 a, b 或 c。
- 范围表示:[a-z] 匹配所有小写字母,[0-9] 匹配所有数字,[a-zA-Z] 匹配所有字母。
- 排除 (Negation):在开头使用 ^,如 [11](@ref) 匹配除 5 之外的任何字符。
- 元字符在字符类中:绝大多数元字符(如 $, *)在 [] 内部都会失去特殊含义,变为普通字符,例如 [akm$] 匹配 a, k, m 或 $。
2.3 预定义字符集 (Shorthand Character Classes)
这些是常用字符集的快捷写法,能很大程度上简化正则表达式。
| \\d | [0-9] | 匹配任意数字 | \\d{3} 匹配 "123" |
| \\D | [^0-9] | 匹配任意非数字 | \\D+ 匹配 "abc" |
| \\w | [a-zA-Z0-9_] | 匹配字母、数字、下划线 (ASCII模式) | \\w+ 匹配 "hello_123" |
| \\W | [^a-zA-Z0-9_] | 匹配非单词字符 (标点、空格等) | \\W 匹配 "@" |
| \\s | [ \\t\\n\\r\\f\\v] | 匹配任意空白字符 (空格、制表符、换行等) | \\s+ 匹配多个空格 |
| \\S | [^ \\t\\n\\r\\f\\v] | 匹配任意非空白字符 | \\S+ 匹配 "hello" |
| . | (除\\n) | 匹配除换行符外的任意单个字符 | a.c 匹配 "abc", "a1c" |
| \\b | – | 匹配单词边界 (单词与非单词字符的分界) | \\bcat\\b 精准匹配单词 "cat" |
| \\B | – | 匹配非单词边界 | \\Bcat\\B 匹配 "concatenate" 里的 "cat" |
重要提示:在Python 3中,\\w 默认会匹配Unicode中的字母字符(如中文字符),若只想匹配ASCII字符,需使用 re.ASCII 标志。
2.4 量词 (Quantifiers) – 控制重复次数
量词用于指定前一个字符或分组必须出现的次数。
| * | 匹配0次或更多次 | 贪婪 | a* 匹配 "", "a", "aaa" |
| + | 匹配1次或更多次 | 贪婪 | a+ 匹配 "a", "aaa" |
| ? | 匹配0次或1次 | 贪婪 | colou?r 匹配 "color", "colour" |
| {n} | 匹配恰好 n 次 | 固定 | \\d{4} 匹配 "2026" |
| {n,m} | 匹配 n 到 m 次 | 贪婪 | a{2,4} 匹配 "aa", "aaa", "aaaa" |
| {n,} | 匹配至少 n 次 | 贪婪 | a{2,} 匹配 "aa", "aaa"… |
| {,n} | 匹配最多 n 次 | 贪婪 | a{,3} 匹配 "", "a", "aa", "aaa" |
2.5 贪婪 (Greedy) 与 非贪婪 (Non-greedy) 匹配
这是一个极其重要的概念,也是初学者容易犯错的地方。
- 贪婪 (Greedy):默认情况下,量词 (*, +, ?, {n,m}) 都是贪婪的,它们会尽可能多地匹配字符。
- 非贪婪 (Non-greedy / Lazy):在量词后面加上一个 ?,就变成了非贪婪模式,它们会尽可能少地匹配字符。
示例:对字符串 "<div>内容1</div><span>内容2</span>"
- 贪婪 .+:会匹配 "<div>内容1</div><span>内容2</span>",因为它从第一个 < 开始,一直吞到最后一个 >。
- 非贪婪 .+?:会匹配 "<div>内容1</div>" 和 "<span>内容2</span>",因为它找到第一个 > 就停止了。
规则:*?, +?, ??, {n,m}? 都是非贪婪版本。
2.6 位置锚点 (Anchors)
锚点本身不匹配任何字符,它们匹配的是字符串中的位置。
- ^:匹配字符串的开头。在多行模式(re.M)下,也匹配每一行的开头。
- $:匹配字符串的结尾。在多行模式下,也匹配每一行的结尾。
- \\b:匹配单词边界,即 \\w 和 \\W 之间的位置。
- \\B:匹配非单词边界。
第三部分:分组、捕获与引用
分组是正则表达式提取结构化信息的核心能力。
3.1 捕获分组 (Capturing Groups) – (…)
使用圆括号 (…) 将模式的一部分包裹起来,可以将其作为一个整体进行操作,并且匹配到的内容会被“捕获”并存储起来,供后续使用。
import re
text = "生日: 1990-05-15"
pattern = r"(\\d{4})-(\\d{2})-(\\d{2})"
match = re.search(pattern, text)
if match:
print(f"完整匹配: {match.group(0)}") # 1990-05-15
print(f"年: {match.group(1)}") # 1990
print(f"月: {match.group(2)}") # 05
print(f"日: {match.group(3)}") # 15
print(f"所有分组: {match.groups()}") # ('1990', '05', '15')
match.group(0) 总是返回整个匹配项,而 group(1), group(2)… 返回从左到右的各个分组。
3.2 命名分组 (Named Groups) – (?P<name>…)
当分组很多时,用数字索引难以维护。命名分组允许我们为分组起一个名字。
pattern = r"(?P<year>\\d{4})-(?P<month>\\d{2})-(?P<day>\\d{2})"
match = re.search(pattern, text)
if match:
print(f"年: {match.group('year')}") # 1990
print(f"月: {match.group('month')}") # 05
print(f"日: {match.group('day')}") # 15
3.3 反向引用 (Backreferences)
反向引用允许你在同一个正则表达式内部引用之前已经捕获到的分组内容,确保前后的内容一致。
- 数字反向引用:\\1, \\2… 引用对应编号的分组。
- 命名反向引用:(?P=name) 引用对应的命名分组。
示例:查找重复的单词"the the"。
pattern = r"\\b(\\w+)\\s+\\1\\b"
re.search(pattern, "the the quick brown fox") # 匹配成功
示例:匹配HTML标签<tag>…</tag>。
pattern = r"<(?P<tag>\\w+)>.*?</(?P=tag)>"
re.search(pattern, "<b>bold text</b>") # 匹配成功
性能警告:反向引用会强制引擎进入回溯模式,在处理长字符串时可能导致性能灾难,甚至“灾难性回溯”。
3.4 非捕获分组 (Non-capturing Groups) – (?:…)
有时你只需要将一组模式作为一个整体应用量词,但不需要捕获其内容。此时应使用 (?:…),它可以节省资源并提高性能。
# 匹配IP地址,但不捕获每一段
pattern = r"(?:\\d{1,3}\\.){3}\\d{1,3}"
text = "My IP is 192.168.1.1"
print(re.findall(pattern, text)) # ['192.168.1.1']
第四部分:re 模块核心函数实战
掌握了语法,让我们看看如何在Python中使用它们。re 模块提供了一整套函数。
4.1 re.match(pattern, string, flags=0)
从字符串的起始位置开始匹配。如果起始位置不匹配,则返回 None。
print(re.match(r'www', 'www.runoob.com')) # 匹配成功,返回Match对象
print(re.match(r'com', 'www.runoob.com')) # 匹配失败,返回None
场景:验证字符串是否以某种模式开头,如验证手机号格式 ^1[3-9]\\d{9}$。
4.2 re.search(pattern, string, flags=0)
扫描整个字符串,返回第一个成功的匹配。如果没找到,返回 None。
print(re.search(r'com', 'www.runoob.com')) # 匹配成功
区别:match 只从开头找,search 从任何位置找。这是两者最核心的区别。
4.3 re.findall(pattern, string, flags=0)
扫描整个字符串,返回所有不重叠的匹配项的列表。如果没有匹配,返回空列表。这是最常用的函数之一。
text = "电话: 138-1234-5678, 139-8765-4321"
phones = re.findall(r'1[3-9]\\d-\\d{4}-\\d{4}', text)
print(phones) # ['138-1234-5678', '139-8765-4321']
4.4 re.finditer(pattern, string, flags=0)
和 findall 类似,但它返回的是一个迭代器 (iterator),迭代器中的每个元素是一个 Match 对象。这在处理大量匹配项时可以节省内存。
for match in re.finditer(r'\\d+', 'a1b2c3d4'):
print(f"匹配值: {match.group()}, 位置: {match.span()}")
4.5 re.sub(pattern, repl, string, count=0, flags=0)
用于替换字符串中所有匹配正则表达式的子串。
text = "2024-10-01 是国庆节"
# 将日期格式从 – 改为 /
new_text = re.sub(r'(\\d{4})-(\\d{2})-(\\d{2})', r'\\1/\\2/\\3', text)
print(new_text) # 2024/10/01 是国庆节
- repl 参数:可以是字符串(用 \\1, \\2 或 \\g<name> 引用分组),也可以是一个函数,该函数接收一个 Match 对象并返回替换字符串,功能非常强大。
def double(matched):
value = int(matched.group('value'))
return str(value * 2)
s = 'A23G4HFD567'
print(re.sub('(?P<value>\\d+)', double, s)) # A46G8HFD1134
4.6 re.split(pattern, string, maxsplit=0, flags=0)
根据匹配的子串来分割字符串,返回一个列表。比字符串的 split() 方法更强大,因为它可以按复杂模式分割。
text = 'apple,banana;orange|grape'
fruits = re.split(r'[,;|]', text)
print(fruits) # ['apple', 'banana', 'orange', 'grape']
4.7 re.compile(pattern, flags=0)
预编译正则表达式,生成一个 Pattern 对象。对于需要多次使用的同一正则表达式,预编译可以显著提升性能,因为它避免了每次调用 match/search 等函数时都重新编译正则。
# 不推荐:每次循环都编译
for text in large_texts:
result = re.search(r'\\d+', text)
# 推荐:预编译,只编译一次
pattern = re.compile(r'\\d+')
for text in large_texts:
result = pattern.search(text)
第五部分:编译标志 (Flags)
编译标志用于修改正则表达式的行为。它们可以作为参数传递给 re.compile() 或 re.search() 等函数,也可以通过按位或 (|) 组合使用。
| re.IGNORECASE | re.I | 使匹配不区分大小写。如 re.findall(r"hello", text, re.I) 可匹配 "Hello", "HELLO"。 |
| re.MULTILINE | re.M | 改变 ^ 和 $ 的行为,使它们匹配每一行的开头和结尾,而不仅是整个字符串。对日志文件等按行分割的文本非常有用。 |
| re.DOTALL | re.S | 使 . 元字符可以匹配换行符 \\n。默认情况下 . 是不能匹配换行的,这在跨行匹配时经常用到。 |
| re.ASCII | re.A | 使 \\w, \\d, \\s 等预定义字符集仅匹配 ASCII 字符,而不是Unicode字符。例如,\\w 将不再匹配中文字符。 |
| re.VERBOSE | re.X | 允许在正则表达式中添加注释和空白,使复杂的正则更具可读性。引擎会忽略模式中的空格和以 # 开头的注释。 |
| re.DEBUG | 无 | 显示编译后的正则表达式模式,用于调试和性能分析。 |
re.VERBOSE 实战:处理复杂正则时,这个标志是救星。
phone_pattern = re.compile(r"""
(\\d{3}) # 区号,3位数字
[-.\\s]? # 可选分隔符
(\\d{3}) # 中间三位
[-.\\s]? # 可选分隔符
(\\d{4}) # 最后四位
""", re.VERBOSE)
内联标志:你也可以在正则表达式模式内部启用标志,影响整个或部分模式。例如 (?i) 启用忽略大小写。
re.search(r'(?i)python', 'Python') # 匹配成功
第六部分:进阶与实战技巧
6.1 原子分组与占有量词 (Possessive Quantifiers)
Python 标准库的 re 模块不支持原子分组 (?>…) 和占有量词(如 *+, ++),但第三方 regex 模块支持。占有量词与贪婪量词类似,但一旦匹配,绝不会交还已经匹配的字符给后续模式尝试,这可以极大地防止“灾难性回溯”。
6.2 条件匹配
同样,re 模块不支持,但 regex 模块支持。可以根据前面的分组是否成功匹配来决定后续的模式。
# regex模块示例:匹配 (123) 456-7890 或 123-456-7890
pattern = r'(()?\\d{3}(?(1))|-)\\d{3}-\\d{4}'
6.3 正则表达式的性能陷阱:灾难性回溯 (Catastrophic Backtracking)
这是正则表达式领域最著名的性能杀手。当正则表达式包含嵌套的量词(如 (a+)+b)匹配一个无法匹配的字符串(如 "aaaa")时,引擎会进行指数级的尝试,导致程序“假死”。
如何避免:
6.4 匹配中文
在Python 3中,\\w 可以匹配中文字符。若要精确匹配中文字符,可以使用Unicode范围 [\\u4e00-\\u9fff]。若要涵盖更多生僻字和中文标点,可以扩宽范围,如 [\\u4e00-\\u9fff\\u3000-\\u303f\\uff00-\\uffef]。
6.5 常用实战模式
- 邮箱验证:r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
- 手机号 (中国大陆):r'^1[3-9]\\d{9}$'
- URL:r'https?://[^\\s/$.?#].[^\\s]*'
- IP地址:r'(?:\\d{1,3}\\.){3}\\d{1,3}'

