Python 正则表达式深度指南:从入门到高级实践
引言
在现代数据处理和软件开发中,我们经常需要处理大量的文本数据。无论是从日志文件中提取特定信息,验证用户输入格式,还是对复杂文本进行高效的搜索和替换,都离不开强大的文本模式匹配工具——正则表达式 (Regular Expressions, Regex)。
正则表达式是一种强大的字符串处理语言,它允许我们通过简洁的语法描述复杂的文本模式。Python 通过其内置的 re 模块提供了对正则表达式的全面支持。掌握正则表达式不仅能显著提高您的文本处理效率,更是数据清洗、爬虫开发、日志分析和自然语言处理(NLP)等领域的必备技能。
本教程将带您深入探索 Python 正则表达式的世界,从最基本的匹配规则开始,逐步掌握元字符、特殊序列、集合、量词等核心概念,并通过 re 模块的各种函数(match, search, findall, sub, compile)进行实战。我们将通过丰富的代码示例和详细解释,确保您能够融会贯通,并将其应用于实际项目中。
目录
- 环境准备
- 什么是正则表达式?
- Python
re模块的核心函数 - 正则表达式语法:基础构建块
- 正则表达式标志 (Flags)
- 高级主题与最佳实践
- 结语与进一步学习
1. 环境准备
Python 的 re 模块是标准库的一部分,这意味着您无需安装任何额外的包,只需安装 Python 即可。
- 安装 Python:
如果您尚未安装 Python,请访问 Python 官方网站 下载并安装最新版本。 - 验证安装:
打开终端或命令提示符,输入python --version(或python3 --version),确认 Python 已正确安装。
准备就绪,让我们开始学习正则表达式!
2. 什么是正则表达式?
正则表达式,通常缩写为 regex、regexp 或 RE,是一种用于描述字符串模式的强大工具。它通过一系列字符和特殊符号,定义了一个搜索模式,这个模式可以用于:
- 搜索 (Search):在文本中查找某个模式是否存在。
- 匹配 (Match):检查整个字符串或字符串的某个部分是否符合特定模式。
- 提取 (Extract):从符合模式的文本中提取出感兴趣的部分。
- 替换 (Replace):将符合模式的文本替换为其他内容。
- 分割 (Split):根据模式来分割字符串。
- 验证 (Validate):检查输入文本是否符合预设的格式要求(如邮箱、电话号码、密码强度)。
2.1. 正则表达式的用途
- 数据清洗:移除文本中的 HTML 标签、URL、特殊符号等噪声。
- 数据提取:从非结构化或半结构化数据中(如日志文件、网页内容)提取日期、电话号码、电子邮件地址等特定信息。
- 数据验证:在表单提交前验证电子邮件格式、密码强度、手机号等用户输入。
- 文本分析:在自然语言处理中,用于模式识别、词形归一化等。
3. Python re 模块的核心函数
Python 通过内置的 re 模块来支持正则表达式操作。在使用之前,您只需导入它:
import re
re 模块提供了多个函数,用于执行不同的正则表达式操作。理解它们的区别至关重要。
3.1. re.match():从字符串开头匹配
- “为什么”:当您只需要检查字符串开头是否符合某个模式时,
re.match()是理想选择。 - “是什么”:尝试从字符串的起始位置匹配一个模式。如果匹配成功,返回一个
Match对象;否则返回None。 - “怎么做”:
re.match(pattern, string, flags=0)
import re
text = "Hello World"
pattern = r"Hello" # 匹配 "Hello"
match_obj_1 = re.match(pattern, text)
print(f"匹配对象 1: {match_obj_1}")
# 输出: 匹配对象 1: <re.Match object; span=(0, 5), match='Hello'>
if match_obj_1:
print(f"匹配到的内容: {match_obj_1.group()}") # group() 返回匹配到的字符串
print(f"匹配的起始和结束位置: {match_obj_1.span()}")
pattern_fail = r"World" # 匹配 "World"
match_obj_2 = re.match(pattern_fail, text)
print(f"匹配对象 2: {match_obj_2}")
# 输出: 匹配对象 2: None (因为 "World" 不在字符串开头)
Match 对象的常用方法:
group(0)或group():返回整个匹配的字符串。group(n):返回第n个捕获组的匹配内容(稍后介绍)。start():返回匹配的起始索引。end():返回匹配的结束索引(不包含)。span():返回一个元组(start, end)。
3.2. re.search():搜索整个字符串
- “为什么”:当您需要检查整个字符串中是否存在某个模式的任意一个匹配时,
re.search()是首选。 - “是什么”:扫描整个字符串,查找第一个匹配模式的位置。如果找到,返回一个
Match对象;否则返回None。 - “怎么做”:
re.search(pattern, string, flags=0)
import re
text = "Hello World"
pattern = r"World" # 匹配 "World"
search_obj = re.search(pattern, text)
print(f"搜索对象: {search_obj}")
# 输出: 搜索对象: <re.Match object; span=(6, 11), match='World'>
if search_obj:
print(f"匹配到的内容: {search_obj.group()}")
print(f"匹配的起始和结束位置: {search_obj.span()}")
pattern_fail = r"Python" # 匹配 "Python"
search_obj_fail = re.search(pattern_fail, text)
print(f"搜索对象 (失败): {search_obj_fail}")
# 输出: 搜索对象 (失败): None
re.match() 与 re.search() 的关键区别:
re.match()只检查字符串的开头。re.search()会扫描整个字符串,找到第一个匹配项就停止。
3.3. re.findall():查找所有非重叠匹配
- “为什么”:当您需要从字符串中提取所有符合某个模式的非重叠子串时,
re.findall()是最方便的选择。 - “是什么”:在字符串中查找所有匹配模式的非重叠子串,并以列表形式返回。如果模式中包含捕获组,则返回一个元组列表。
- “怎么做”:
re.findall(pattern, string, flags=0)
import re
text = "apple banana apple cherry"
pattern = r"apple" # 匹配 "apple"
all_matches = re.findall(pattern, text)
print(f"所有匹配项: {all_matches}")
# 输出: 所有匹配项: ['apple', 'apple']
text_nums = "Price: $12.34, Discount: 50%, Total: $6.17"
pattern_nums = r"\d+\.?\d*" # 匹配数字 (整数或浮点数)
# \d+ 匹配一个或多个数字
# \.? 匹配零个或一个点号
# \d* 匹配零个或多个数字
all_numbers = re.findall(pattern_nums, text_nums)
print(f"所有数字: {all_numbers}")
# 输出: 所有数字: ['12.34', '50', '6.17']
3.4. re.sub():替换匹配项
- “为什么”:当您需要将字符串中所有或部分符合某个模式的子串替换为其他内容时,
re.sub()是理想工具。 - “是什么”:在字符串中找到所有匹配模式的子串,并将它们替换为指定的
repl字符串(或通过函数生成)。 - “怎么做”:
re.sub(pattern, repl, string, count=0, flags=0)pattern:要匹配的正则表达式。repl:替换字符串,可以是一个字符串,也可以是一个函数。string:要进行替换操作的原始字符串。count:可选参数,指定最多替换多少次。默认为 0,表示替换所有匹配项。
import re
text = "The quick brown fox jumps over the lazy dog."
# 替换所有空格为下划线
new_text_1 = re.sub(r"\s", "_", text)
print(f"替换空格: {new_text_1}")
# 输出: 替换空格: The_quick_brown_fox_jumps_over_the_lazy_dog.
# 移除所有非字母字符
text_dirty = "Hello World! This is a test. 123."
new_text_2 = re.sub(r"[^a-zA-Z\s]", "", text_dirty)
print(f"移除标点和数字: {new_text_2}")
# 输出: 移除标点和数字: Hello World This is a test
# 替换前两个匹配项
text_many_words = "apple banana cherry date apple fig"
new_text_3 = re.sub(r"apple", "fruit", text_many_words, count=2)
print(f"替换前两个 'apple': {new_text_3}")
# 输出: 替换前两个 'apple': fruit banana cherry date fruit fig
3.5. re.split():分割字符串
- “为什么”:当您需要根据复杂的模式来分割字符串,而不是简单的固定分隔符时(如
str.split()),re.split()会非常有用。 - “是什么”:根据正则表达式模式将字符串分割成一个列表。
- “怎么做”:
re.split(pattern, string, maxsplit=0, flags=0)maxsplit:可选参数,指定最大分割次数。
import re
text = "apple,banana;cherry orange"
# 使用逗号、分号或空格作为分隔符
parts = re.split(r"[,;\s]", text)
print(f"分割结果: {parts}")
# 输出: 分割结果: ['apple', 'banana', 'cherry', 'orange']
text_logs = "ERROR: File not found. WARNING: Disk full. INFO: Operation successful."
# 使用 "ERROR:", "WARNING:", "INFO:" 作为分隔符,并保留内容
parts_logs = re.split(r"(ERROR:|WARNING:|INFO:)", text_logs)
# 注意:如果模式包含捕获组,则匹配到的分隔符也会作为结果的一部分返回
print(f"日志分割结果: {parts_logs}")
# 输出: 日志分割结果: ['', 'ERROR:', ' File not found. ', 'WARNING:', ' Disk full. ', 'INFO:', ' Operation successful.']
3.6. re.compile():编译正则表达式
- “为什么”:当您需要多次使用同一个正则表达式模式时,编译它可以显著提高性能。编译后的模式对象可以重复使用。
- “是什么”:将正则表达式模式编译成一个
RegexObject对象。 - “怎么做”:
re.compile(pattern, flags=0)
import re
# 未编译的模式
text_data = ["hello world", "World is good", "Hello Python"]
pattern_uncompiled = r"world"
print("--- 未编译模式 ---")
for text in text_data:
match = re.search(pattern_uncompiled, text, re.IGNORECASE)
if match:
print(f"'{text}' 匹配到 '{match.group()}'")
# 编译模式
compiled_pattern = re.compile(r"world", re.IGNORECASE)
print("\n--- 编译模式 ---")
for text in text_data:
match = compiled_pattern.search(text) # 直接调用模式对象的方法
if match:
print(f"'{text}' 匹配到 '{match.group()}'")
# 输出:
# --- 未编译模式 ---
# 'hello world' 匹配到 'world'
# 'World is good' 匹配到 'World'
# 'Hello Python' 匹配到 'Hello' (此处应无匹配,因为是world。实际上输出: 'Hello Python' 匹配到 'Python' -- 示例模式有误,应为"world")
# 更正后的预期输出 (如果 pattern_uncompiled 也是 r"world"):
# 'hello world' 匹配到 'world'
# 'World is good' 匹配到 'World'
# 编译模式的正确输出
# --- 编译模式 ---
# 'hello world' 匹配到 'world'
# 'World is good' 匹配到 'World'
更正说明: 上述未编译模式的示例中,re.search(pattern_uncompiled, text, re.IGNORECASE) 如果 pattern_uncompiled 是 r"world",则应该只匹配到 world 和 World。原示例的输出有误导性,已更正。
性能优势:当正则表达式被多次使用时,re.compile() 会避免每次都重新解析模式,从而节省时间。对于单次匹配,其优势不明显。
4. 正则表达式语法:基础构建块
正则表达式的强大之处在于其丰富的模式匹配语法。
4.1. 普通字符 (Literal Characters)
大多数字符都直接匹配它们自己。例如,a 匹配字符 a,hello 匹配字符串 hello。
import re
text = "apple pie"
match = re.search(r"apple", text)
print(match.group() if match else "无匹配") # 输出: apple
4.2. 元字符 (Metacharacters)
元字符是具有特殊含义的字符,它们不匹配自身,而是匹配某种模式。
4.2.1. . (点号):匹配任意字符(换行符除外)
- “是什么”:匹配除换行符
\n之外的任意单个字符。 - “怎么做”:直接在模式中使用
.。
import re
text = "cat, bat, mat, hat, fat"
# 匹配任意字符 + 'at'
matches = re.findall(r".at", text)
print(f"匹配 '.at': {matches}")
# 输出: 匹配 '.at': ['cat', 'bat', 'mat', 'hat', 'fat']
text_newline = "hello\nworld"
match_dot = re.search(r".", text_newline) # 匹配 'h'
print(f"匹配 '.' (不含换行符): {match_dot.group()}") # 输出: h
注意:要让 . 也匹配换行符,可以使用 re.DOTALL 标志(稍后介绍)。
4.2.2. ^ (脱字符):匹配字符串开头
- “是什么”:匹配字符串的起始位置。
- “怎么做”:将
^放在模式的最前面。
import re
text = "Python is great"
match_start_1 = re.search(r"^Python", text)
print(f"开头匹配 'Python': {match_start_1.group() if match_start_1 else '无匹配'}") # 输出: Python
match_start_2 = re.search(r"^is", text)
print(f"开头匹配 'is': {match_start_2.group() if match_start_2 else '无匹配'}") # 输出: 无匹配
注意:在 re.MULTILINE 模式下,^ 也会匹配每一行的开头。
4.2.3. $ (美元符号):匹配字符串结尾
- “是什么”:匹配字符串的结束位置。
- “怎么做”:将
$放在模式的最后面。
import re
text = "Python is great"
match_end_1 = re.search(r"great$", text)
print(f"结尾匹配 'great': {match_end_1.group() if match_end_1 else '无匹配'}") # 输出: great
match_end_2 = re.search(r"Python$", text)
print(f"结尾匹配 'Python': {match_end_2.group() if match_end_2 else '无匹配'}") # 输出: 无匹配
注意:在 re.MULTILINE 模式下,$ 也会匹配每一行的结尾。
4.2.4. * (星号):匹配零个或多个
- “是什么”:匹配前一个字符(或字符组)零次或多次。
- “怎么做”:放在需要重复的字符或组后面。
import re
text = "ab, abb, abbb, ac"
matches = re.findall(r"ab*", text) # 匹配 'a' 后跟零个或多个 'b'
print(f"匹配 'ab*': {matches}") # 输出: 匹配 'ab*': ['ab', 'abb', 'abbb', 'a'] ('a' 因为 'b' 出现了零次)
matches_2 = re.findall(r"a[bc]*", "axbxxbcxabc") # 匹配 'a' 后跟零个或多个 'b' 或 'c'
print(f"匹配 'a[bc]*': {matches_2}") # 输出: ['a', 'b', 'bc', 'abc'] -- 注意这里只匹配到 'a','b' 'bc' 'abc'是单独的
# 实际上,这里输出应该是:['a', 'abc']
# 更正:`re.findall` 查找的是非重叠匹配。
# 对于 "axbxxbcxabc",模式 `a[bc]*`:
# - 找到 "a"
# - 继续从 "xbxxbcxabc" 找,找不到以 "a" 开头的。
# 示例有误,应更正为:
matches_2 = re.findall(r"ab*c", "ac, abc, abbc") # 匹配 'a' 后跟零个或多个 'b',再跟一个 'c'
print(f"匹配 'ab*c': {matches_2}")
# 输出: 匹配 'ab*c': ['ac', 'abc', 'abbc']
4.2.5. + (加号):匹配一个或多个
- “是什么”:匹配前一个字符(或字符组)一次或多次。
- “怎么做”:放在需要重复的字符或组后面。
import re
text = "ab, abb, abbb, ac"
matches = re.findall(r"ab+", text) # 匹配 'a' 后跟一个或多个 'b'
print(f"匹配 'ab+': {matches}") # 输出: 匹配 'ab+': ['ab', 'abb', 'abbb'] ('a' 不匹配,因为需要至少一个 'b')
4.2.6. ? (问号):匹配零个或一个
- “是什么”:匹配前一个字符(或字符组)零次或一次。
- “怎么做”:放在需要重复的字符或组后面。
import re
text = "color, colour"
matches = re.findall(r"colou?r", text) # 匹配 'colo' 后跟零个或一个 'u',再跟 'r'
print(f"匹配 'colou?r': {matches}") # 输出: 匹配 'colou?r': ['color', 'colour']
4.2.7. {m,n} (花括号):匹配指定数量
- “是什么”:
{m}:精确匹配前一个字符m次。{m,}:匹配前一个字符至少m次。{,n}:匹配前一个字符最多n次。{m,n}:匹配前一个字符至少m次,最多n次。
- “怎么做”:放在需要重复的字符或组后面。
import re
text = "123, 1234, 12345, 12"
matches_exact = re.findall(r"\d{3}", text) # 精确匹配 3 个数字
print(f"匹配 '\\d{{3}}': {matches_exact}") # 输出: 匹配 '\d{3}': ['123', '123', '123']
matches_at_least = re.findall(r"\d{3,}", text) # 匹配至少 3 个数字
print(f"匹配 '\\d{{3,}}': {matches_at_least}") # 输出: 匹配 '\d{3,}': ['123', '1234', '12345']
matches_range = re.findall(r"\d{2,4}", text) # 匹配 2 到 4 个数字
print(f"匹配 '\\d{{2,4}}': {matches_range}") # 输出: 匹配 '\d{2,4}': ['12', '123', '1234', '1234']
4.2.8. [] (方括号):字符集
- “是什么”:匹配方括号内定义的任意一个字符。
[abc]:匹配a、b或c中的任意一个。[a-z]:匹配任意小写字母。[A-Z]:匹配任意大写字母。[0-9]:匹配任意数字。[a-zA-Z0-9]:匹配任意字母或数字。[^abc]:匹配除了a、b、c之外的任意字符(^在字符集开头表示“非”)。
- “怎么做”:将需要匹配的字符或范围放在
[]中。
import re
text = "The cat sat on the mat."
matches_vowels = re.findall(r"[aeiou]", text) # 匹配任意元音字母
print(f"匹配元音字母: {matches_vowels}") # 输出: 匹配元音字母: ['e', 'a', 'a', 'o', 'e', 'a']
matches_consonants = re.findall(r"[^aeiou\s.]", text) # 匹配非元音、非空白、非点号的字符
print(f"匹配非元音非空白: {matches_consonants}") # 输出: 匹配非元音非空白: ['T', 'h', 'c', 't', 's', 't', 'n', 't', 'h', 'm', 't']
4.2.9. \ (反斜杠):转义字符
- “为什么”:当您需要匹配一个具有特殊含义的元字符本身时(如
.、*、?等),需要对其进行转义。 - “是什么”:将后续字符视为普通字符(取消其特殊含义),或引入特殊序列(如
\d)。 - “怎么做”:在元字符前加上
\。
import re
text = "Price: $10.99"
# 匹配字面量 '$' 和 '.'
matches = re.search(r"\$\d+\.\d+", text)
print(f"匹配价格: {matches.group() if matches else '无匹配'}") # 输出: 匹配价格: $10.99
强烈推荐使用原始字符串 (Raw Strings) r"...":在 Python 中,反斜杠本身也是一个转义字符。为了避免与 Python 字符串的转义规则冲突(例如 \n 表示换行),建议在定义正则表达式模式时使用原始字符串。例如,r"\d" 比 "\d" 更好。
4.2.10. | (竖线):或操作
- “是什么”:匹配竖线左边或右边的任意一个模式。
- “怎么做”:将两个或多个模式用
|分隔。
import re
text = "cat dog bird fish"
matches = re.findall(r"cat|dog", text) # 匹配 'cat' 或 'dog'
print(f"匹配 'cat|dog': {matches}") # 输出: 匹配 'cat|dog': ['cat', 'dog']
4.2.11. () (圆括号):捕获组与分组
- “为什么”:圆括号有两个主要用途:
- 分组 (Grouping):将多个字符或模式组合成一个逻辑单元,以便对它们应用量词(如
(ab)+)或|操作(如(cat|dog))。 - 捕获 (Capturing):捕获匹配到的子字符串,以便后续提取。
- 分组 (Grouping):将多个字符或模式组合成一个逻辑单元,以便对它们应用量词(如
- “是什么”:创建子表达式,其匹配的内容可以被单独提取。
- “怎么做”:将要分组或捕获的模式放在
()中。
import re
text = "The date is 2023-10-26. Another date is 2024-01-15."
# 捕获年、月、日
pattern = r"(\d{4})-(\d{2})-(\d{2})"
matches = re.findall(pattern, text)
print(f"捕获所有日期: {matches}")
# 输出: 捕获所有日期: [('2023', '10', '26'), ('2024', '01', '15')]
match_obj = re.search(pattern, text)
if match_obj:
print(f"完整匹配: {match_obj.group(0)}") # 或 match_obj.group()
print(f"年份: {match_obj.group(1)}")
print(f"月份: {match_obj.group(2)}")
print(f"日期: {match_obj.group(3)}")
# 输出:
# 完整匹配: 2023-10-26
# 年份: 2023
# 月份: 10
# 日期: 26
4.3. 特殊序列 (Special Sequences)
特殊序列是反斜杠 \ 后跟一个特殊字符,用于匹配预定义的字符集。
4.3.1. \d, \D:数字与非数字
\d:匹配任意数字 (0-9),等同于[0-9]。\D:匹配任意非数字字符,等同于[^0-9]。
import re
text = "Item A: 123, Item B: 45.6, Item C"
numbers = re.findall(r"\d+", text) # 匹配一个或多个数字
print(f"所有数字: {numbers}") # 输出: 所有数字: ['123', '45', '6']
non_numbers = re.findall(r"\D+", text) # 匹配一个或多个非数字字符
print(f"所有非数字序列: {non_numbers}") # 输出: 所有非数字序列: ['Item A: ', ', Item B: ', '.', ', Item C']
4.3.2. \s, \S:空白字符与非空白字符
\s:匹配任意空白字符(空格、制表符\t、换行符\n、回车符\r等)。\S:匹配任意非空白字符。
import re
text = "Hello\tWorld\nPython"
spaces = re.findall(r"\s+", text) # 匹配一个或多个空白字符
print(f"所有空白字符序列: {spaces}") # 输出: 所有空白字符序列: ['\t', '\n']
non_spaces = re.findall(r"\S+", text) # 匹配一个或多个非空白字符 (即单词)
print(f"所有非空白字符序列: {non_spaces}") # 输出: 所有非空白字符序列: ['Hello', 'World', 'Python']
4.3.3. \w, \W:单词字符与非单词字符
\w:匹配任意单词字符(字母、数字、下划线_),等同于[a-zA-Z0-9_]。\W:匹配任意非单词字符,等同于[^a-zA-Z0-9_]。
import re
text = "Python_1.0 is great!"
words = re.findall(r"\w+", text) # 匹配一个或多个单词字符
print(f"所有单词字符序列: {words}") # 输出: 所有单词字符序列: ['Python_1', '0', 'is', 'great']
non_words = re.findall(r"\W+", text) # 匹配一个或多个非单词字符
print(f"所有非单词字符序列: {non_words}") # 输出: 所有非单词字符序列: ['.', ' ', '!']
4.3.4. \b, \B:单词边界与非单词边界
\b:匹配单词边界。单词边界是单词字符和非单词字符之间的位置,或者是字符串的开头或结尾。\B:匹配非单词边界。
import re
text = "cat scattered in the category"
# 匹配独立的单词 'cat'
matches_boundary = re.findall(r"\bcat\b", text)
print(f"匹配单词边界的 'cat': {matches_boundary}") # 输出: 匹配单词边界的 'cat': ['cat']
# 匹配 'cat' 但不是独立单词,例如 'scattered' 和 'category' 中的 'cat'
matches_non_boundary = re.findall(r"\Bcat", text)
print(f"匹配非单词边界的 'cat': {matches_non_boundary}") # 输出: 匹配非单词边界的 'cat': ['cat', 'cat'] (来自 'scattered' 和 'category')
4.4. 量词的贪婪与非贪婪模式 (*?, +?, ??)
默认情况下,量词(*, +, ?, {m,n})是贪婪的 (greedy),它们会尽可能多地匹配字符。
- “为什么”:贪婪模式在某些情况下可能不是我们想要的。例如,从
<h1>Title</h1>中提取标签内容,<h1>.*</h1>会匹配整个字符串,而我们可能只想匹配Title。 - “是什么”:非贪婪模式 (non-greedy),也称为懒惰模式 (lazy),通过在量词后添加
?来启用,它会尽可能少地匹配字符。 - “怎么做”:在量词后面加上
?,例如*?,+?,??,{m,n}?。
import re
text = "<h1>Title 1</h1> <p>Paragraph</p> <h1>Title 2</h1>"
# 贪婪模式:匹配从第一个 <h1> 到最后一个 </h1>
greedy_pattern = r"<h1>.*</h1>"
greedy_match = re.findall(greedy_pattern, text)
print(f"贪婪模式匹配: {greedy_match}")
# 输出: 贪婪模式匹配: ['<h1>Title 1</h1> <p>Paragraph</p> <h1>Title 2</h1>']
# 非贪婪模式:匹配最短的 <h1>...</h1> 片段
nongreedy_pattern = r"<h1>.*?</h1>"
nongreedy_match = re.findall(nongreedy_pattern, text)
print(f"非贪婪模式匹配: {nongreedy_match}")
# 输出: 非贪婪模式匹配: ['<h1>Title 1</h1>', '<h1>Title 2</h1>']
5. 正则表达式标志 (Flags)
标志 (Flags) 可以修改正则表达式的行为,影响匹配的执行方式。它们作为 re 模块函数的第三个参数传递。
5.1. re.IGNORECASE (或 re.I):忽略大小写
- “是什么”:使匹配忽略大小写。
import re
text = "Hello World"
match_case_sensitive = re.search(r"hello", text)
print(f"大小写敏感匹配 'hello': {match_case_sensitive}") # 输出: None
match_case_insensitive = re.search(r"hello", text, re.IGNORECASE)
print(f"忽略大小写匹配 'hello': {match_case_insensitive.group() if match_case_insensitive else '无匹配'}") # 输出: Hello
5.2. re.MULTILINE (或 re.M):多行模式
- “是什么”:使
^和$不仅匹配整个字符串的开始和结束,也匹配每一行的开始和结束(由换行符\n界定)。
import re
text = "Line 1\nLine 2\nLine 3"
# 默认模式下,^ 和 $ 只匹配字符串的开头和结尾
match_default_start = re.findall(r"^Line", text)
print(f"默认模式匹配 '^Line': {match_default_start}") # 输出: ['Line']
# 多行模式下,^ 匹配每行的开头
match_multiline_start = re.findall(r"^Line", text, re.MULTILINE)
print(f"多行模式匹配 '^Line': {match_multiline_start}") # 输出: ['Line', 'Line', 'Line']
# 默认模式下,$ 只匹配字符串的结尾
match_default_end = re.findall(r"3$", text)
print(f"默认模式匹配 '3$': {match_default_end}") # 输出: ['3']
# 多行模式下,$ 匹配每行的结尾
match_multiline_end = re.findall(r"\d$", text, re.MULTILINE)
print(f"多行模式匹配 '\\d$': {match_multiline_end}") # 输出: ['1', '2', '3']
5.3. re.DOTALL (或 re.S):点号匹配换行符
- “是什么”:使元字符
.(点号)能够匹配包括换行符\n在内的所有字符。
import re
text = "Hello\nWorld"
# 默认模式下,. 不匹配换行符
match_dot_default = re.search(r"Hello.World", text)
print(f"默认模式匹配 'Hello.World': {match_dot_default}") # 输出: None
# DOTALL 模式下,. 匹配换行符
match_dot_all = re.search(r"Hello.World", text, re.DOTALL)
print(f"DOTALL 模式匹配 'Hello.World': {match_dot_all.group() if match_dot_all else '无匹配'}") # 输出: Hello
# World
6. 高级主题与最佳实践
6.1. 非捕获组 (?:...)
- “为什么”:有时候您需要使用圆括号进行分组,但又不想捕获其匹配的内容(即不希望它出现在
group()或findall()的结果中),这时可以使用非捕获组。 - “是什么”:通过在
(后加上?:来创建非捕获组。它只用于分组,不创建捕获组。 - “怎么做”:
r"(?:pattern)"
import re
text = "cat dog bird"
# 捕获组示例
matches_capture = re.findall(r"(cat|dog) bird", text)
print(f"捕获组匹配: {matches_capture}") # 输出: ['cat'] (只捕获了组内的内容)
# 非捕获组示例
matches_non_capture = re.findall(r"(?:cat|dog) bird", text)
print(f"非捕获组匹配: {matches_non_capture}") # 输出: ['cat bird'] (捕获了整个模式匹配到的内容)
# 进一步演示 `group()` 行为
match_obj_capture = re.search(r"(cat|dog) bird", text)
if match_obj_capture:
print(f"捕获组 (group(0)): {match_obj_capture.group(0)}") # 整个匹配
print(f"捕获组 (group(1)): {match_obj_capture.group(1)}") # 第一个捕获组
match_obj_non_capture = re.search(r"(?:cat|dog) bird", text)
if match_obj_non_capture:
print(f"非捕获组 (group(0)): {match_obj_non_capture.group(0)}") # 整个匹配
# print(match_obj_non_capture.group(1)) # 会报错,因为没有捕获组1
6.2. 零宽断言 (Lookaheads & Lookbehinds)
零宽断言是一种不消耗字符,只进行判断(断言)的模式。它们检查当前位置的文本是否符合某个条件,但不把这些文本包含在最终匹配结果中。
- 正向先行断言 (Positive Lookahead):
(?=...)- 表达式...必须匹配,但不会被包含在匹配结果中。 - 负向先行断言 (Negative Lookahead):
(?!...)- 表达式...不能匹配。 - 正向后行断言 (Positive Lookbehind):
(?<=...)- 表达式...必须匹配在当前位置之前。 - 负向后行断言 (Negative Lookbehind):
(?<!...)- 表达式...不能匹配在当前位置之前。
import re
text = "apple pie, pineapple juice"
# 匹配 'apple',但后面必须跟着 'pie'
matches_lookahead = re.findall(r"apple(?=\spie)", text)
print(f"正向先行断言: {matches_lookahead}") # 输出: ['apple']
# 匹配 'apple',但后面不能跟着 'pie'
matches_neg_lookahead = re.findall(r"apple(?!\spie)", text)
print(f"负向先行断言: {matches_neg_lookahead}") # 输出: ['apple'] (来自 pineapple 中的 apple)
text_prices = "The price is $10.99. Old price was $8.50."
# 匹配数字,但前面必须跟着 '$'
matches_lookbehind = re.findall(r"(?<=\$)\d+\.\d+", text_prices)
print(f"正向后行断言: {matches_lookbehind}") # 输出: ['10.99', '8.50']
6.3. 使用原始字符串 (Raw Strings) r"..."
- “为什么”:在 Python 中,反斜杠
\既是正则表达式的特殊字符(如\d),也是 Python 字符串的转义字符(如\n换行符,\t制表符)。这会导致混淆和错误。 - “是什么”:原始字符串通过在字符串前加上
r或R来定义,它会告诉 Python 解释器不要对字符串中的反斜杠进行转义处理。 - “怎么做”:
r"your\regex\pattern"
# 普通字符串,需要双反斜杠来表示一个字面量反斜杠
print("C:\\Users\\Desktop") # 输出: C:\Users\Desktop
print("This is a new\nline") # 输出: This is a new\nline (实际上是两行)
# 原始字符串
print(r"C:\Users\Desktop") # 输出: C:\Users\Desktop
print(r"This is a new\nline") # 输出: This is a new\nline (字面量显示 '\n')
# 正则表达式中使用原始字符串可以避免混淆
# 例如,匹配字面量 '\':
regex_pattern = r"\\" # 匹配一个反斜杠
# 如果不用原始字符串,需要写成 "\\\\"
print(re.findall(regex_pattern, r"C:\Users\Desktop")) # 输出: ['\\', '\\']
强烈推荐:在编写所有正则表达式模式时,始终使用原始字符串 r"..."。
6.4. 性能考量:re.compile()
如前所述,当一个正则表达式模式需要被多次使用时,使用 re.compile() 预编译它能够显著提高执行效率,因为它避免了每次调用时都重新解析模式的开销。
6.5. 调试正则表达式
- 在线工具:使用 regex101.com、regexr.com 等在线工具,它们提供实时匹配、解释和测试功能,非常有助于调试复杂的正则表达式。
- 逐步构建:从简单的模式开始,逐步添加更复杂的元素,每次添加后都进行测试。
- 分段测试:对于很长的文本,可以截取小段进行测试。
6.6. 复杂示例:提取电子邮件和电话号码
假设我们有一段文本,需要从中提取电子邮件地址和电话号码。
import re
long_text = """
Contact us at support@example.com or call 123-456-7890.
You can also reach out to info@mycompany.org.
My mobile is +1-555-123-4567. Another email: test.user@domain.co.uk.
Fax: (01) 2345 6789.
"""
# 匹配电子邮件地址的模式
# [a-zA-Z0-9._%+-]+:匹配用户名部分,允许字母、数字、点、下划线、百分号、加号、减号
# @:匹配 '@' 符号
# [a-zA-Z0-9.-]+:匹配域名部分,允许字母、数字、点、减号
# \.[a-zA-Z]{2,}:匹配顶级域名(至少两个字母)
email_pattern = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
emails = email_pattern.findall(long_text)
print(f"提取到的电子邮件地址: {emails}")
# 输出: ['support@example.com', 'info@mycompany.org', 'test.user@domain.co.uk']
# 匹配电话号码的模式
# (\+\d{1,3})?:可选的国际区号,如 +1,+86
# [\s.-]?:可选的分隔符(空格、点、减号)
# \(?\d{2,4}\)?:可选的括号括起来的区号
# [\s.-]?\d{3,4}[\s.-]?\d{3,4}:数字和可选的分隔符
phone_pattern = re.compile(r"(\+\d{1,3}[-\s.]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}")
phones = phone_pattern.findall(long_text)
# 注意:findall 如果有捕获组,会返回捕获组内容。
# 这里第一个捕获组是国际区号,所以返回的是区号部分的列表。
# 如果想获取整个匹配,可以改为非捕获组 (?:...) 或使用 re.finditer()
print(f"提取到的电话号码 (原始捕获组): {phones}")
# 输出: [('', '', '123-456-7890'), ('+1-', '', '555-123-4567'), ('', '01', '2345 6789')]
# (如果捕获组只返回一个,则返回字符串,如果多个捕获组则返回元组)
# 改进电话号码提取:获取完整匹配
# 使用 finditer 并从 Match 对象中获取 group(0)
phone_matches_iter = phone_pattern.finditer(long_text)
extracted_phones = [match.group(0) for match in phone_matches_iter]
print(f"提取到的电话号码 (完整匹配): {extracted_phones}")
# 输出: ['123-456-7890', '+1-555-123-4567', '(01) 2345 6789']
7. 结语与进一步学习
恭喜您!通过本教程,您已经系统地学习了 Python 正则表达式的核心概念、常用函数、语法规则和高级技巧。正则表达式无疑是一个强大而灵活的工具,能够极大地提升您在文本处理任务中的效率和能力。
然而,正则表达式的海洋广阔而深邃,仍有许多进阶概念和技巧等待您探索。作为基于Gemini2.5架构开发的先进人工智能助手,我建议您:
- 持续实践:解决实际问题是掌握正则表达式的最佳方式。尝试从网页、日志文件、API 响应中提取信息,或进行数据清理。
- 查阅官方文档:Python
re模块官方文档 是最权威、最全面的参考资料。 - 学习更多高级特性:
- 命名捕获组
(?P<name>...):通过名称而非索引来访问捕获组,提高可读性。 - 条件匹配
(?(id/name)yes-pattern|no-pattern):根据某个捕获组是否存在来决定匹配模式。 - 原子组
(?>...):禁止回溯,提高匹配效率,但也可能降低灵活性。
- 命名捕获组
- 避免过度使用:虽然正则表达式很强大,但对于简单的字符串操作,Python 内置的字符串方法(如
str.split(),str.replace(),str.find(),str.startswith()等)可能更简洁、更易读、性能更好。
熟能生巧,通过不断地练习和学习,您将能够游刃有余地驾驭正则表达式,成为文本处理的高手!
更多推荐
所有评论(0)