Python 正则表达式详解:提升你的文本处理能力
正则表达式,通常简称为“regex”或“regexp”,是处理字符串的强大工具。它们提供了一种简洁而灵活的方式来识别文本中的模式、提取信息以及执行搜索和替换操作。Python 的内置 re 模块提供了对正则表达式的全面支持。
本教程将引导您了解 Python 中正则表达式的基础知识和高级功能。
1. 什么是正则表达式?
核心而言,正则表达式是定义搜索模式的字符序列。当您在文本中搜索特定模式时,可以使用这些模式来描述您正在寻找的内容。可以把它们想象成文本编辑器中“查找”功能的高度高级版本。
为什么使用它们?
* 验证: 检查电子邮件地址或电话号码是否为有效格式。
* 解析: 从日志文件或网页中提取特定数据(例如,日期、URL、价格)。
* 文本处理: 替换特定模式,根据复杂分隔符拆分字符串。
* 数据清洗: 删除不需要的字符或标准化数据格式。
2. Python 中的 re 模块
Python 的 re 模块提供了所有必要的功能来处理正则表达式。要使用它,只需导入它:
python
import re
原始字符串
强烈建议在 Python 中使用原始字符串来表示正则表达式。原始字符串以 r 为前缀(例如,r"pattern")。这会告诉 Python 将反斜杠 (\) 视为字面字符,而不是转义序列。这非常重要,因为正则表达式大量使用反斜杠表示特殊序列(例如,\d 表示数字),如果没有原始字符串,您将不得不双重转义它们(例如,\\d)。
“`python
没有原始字符串(需要双反斜杠用于正则表达式特殊序列)
pattern = “\d+”
使用原始字符串(推荐)
pattern = r”\d+”
“`
3. 基本匹配函数
re 模块提供了几个用于查找模式的函数:
re.match(pattern, string, flags=0)
此函数尝试仅在 string 的开头匹配 pattern。如果找到匹配项,它返回一个 Match 对象;否则,它返回 None。
“`python
import re
text = “Hello, world!”
pattern = r”Hello”
match_obj = re.match(pattern, text)
if match_obj:
print(f”Match found: {match_obj.group()}”) # 输出: Match found: Hello
else:
print(“No match”)
text2 = “world, Hello!”
match_obj2 = re.match(pattern, text2)
if match_obj2:
print(f”Match found: {match_obj2.group()}”)
else:
print(“No match”) # 输出: No match (因为 ‘Hello’ 不在开头)
“`
re.search(pattern, string, flags=0)
此函数扫描 string,查找 pattern 产生匹配的第一个位置。如果在字符串中的任何位置找到匹配项,它返回一个 Match 对象,否则返回 None。
“`python
import re
text = “The quick brown fox jumps over the lazy dog.”
pattern = r”fox”
match_obj = re.search(pattern, text)
if match_obj:
print(f”Match found: {match_obj.group()}”) # 输出: Match found: fox
print(f”Start index: {match_obj.start()}”) # 输出: Start index: 16
print(f”End index: {match_obj.end()}”) # 输出: End index: 19
print(f”Span: {match_obj.span()}”) # 输出: Span: (16, 19)
else:
print(“No match”)
text2 = “Hello, world!”
pattern2 = r”world”
match_obj2 = re.search(pattern2, text2)
if match_obj2:
print(f”Match found: {match_obj2.group()}”) # 输出: Match found: world
“`
re.findall(pattern, string, flags=0)
此函数返回 string 中 pattern 的所有非重叠匹配项的列表。如果模式中存在组,它将返回一个元组列表。
“`python
import re
text = “The rain in Spain falls mainly on the plain.”
pattern = r”ai”
matches = re.findall(pattern, text)
print(matches) # 输出: [‘ai’, ‘ai’, ‘ai’, ‘ai’]
text2 = “Emails: [email protected], [email protected]”
pattern2 = r”(\w+)@(\w+.\w+)” # 捕获用户名和域名的组
emails = re.findall(pattern2, text2)
print(emails) # 输出: [(‘test’, ‘example.com’), (‘user’, ‘domain.org’)]
“`
re.finditer(pattern, string, flags=0)
此函数返回一个迭代器,它为 string 中 pattern 的所有非重叠匹配项生成 Match 对象。对于大型文本,这通常比 findall 更节省内存,因为它不会一次性在内存中构建整个列表。
“`python
import re
text = “The rain in Spain falls mainly on the plain.”
pattern = r”ai”
for match_obj in re.finditer(pattern, text):
print(f”Match found: {match_obj.group()} at {match_obj.span()}”)
输出:
Match found: ai at (5, 7)
Match found: ai at (14, 16)
Match found: ai at (29, 31)
Match found: ai at (40, 42)
“`
4. 元字符(特殊字符)
元字符是正则表达式中具有特殊含义的字符。
-
.(点): 匹配任何字符(默认情况下除了换行符)。
python
print(re.search(r"a.b", "acb").group()) # acb
print(re.search(r"a.b", "a-b").group()) # a-b
print(re.search(r"a.b", "a\nb")) # None (默认情况下) -
^(脱字号): 匹配字符串的开头。
python
print(re.search(r"^Hello", "Hello World").group()) # Hello
print(re.search(r"^World", "Hello World")) # None -
$(美元符号): 匹配字符串的结尾。
python
print(re.search(r"World$", "Hello World").group()) # World
print(re.search(r"Hello$", "Hello World")) # None -
*(星号): 匹配前面字符或组的零次或多次出现。
python
print(re.search(r"ab*c", "ac").group()) # ac (b 出现 0 次)
print(re.search(r"ab*c", "abc").group()) # abc (b 出现 1 次)
print(re.search(r"ab*c", "abbbc").group()) # abbbc (b 出现 3 次) -
+(加号): 匹配前面字符或组的一次或多次出现。
python
print(re.search(r"ab+c", "ac")) # None (b 出现 0 次)
print(re.search(r"ab+c", "abc").group()) # abc
print(re.search(r"ab+c", "abbbc").group()) # abbbc -
?(问号): 匹配前面字符或组的零次或一次出现。也用于非贪婪匹配(见下文)。
python
print(re.search(r"ab?c", "ac").group()) # ac
print(re.search(r"ab?c", "abc").group()) # abc
print(re.search(r"ab?c", "abbc")) # None (b 出现多于 1 次) -
{m,n}(花括号): 匹配前面字符或组的m到n次出现。{m}: 恰好m次出现。{m,}:m或更多次出现。{,n}: 最多n次出现(与{0,n}相同)。
python
print(re.search(r"a{2}b", "aab").group()) # aab
print(re.search(r"a{2,4}b", "aaab").group()) # aaab
print(re.search(r"a{2,}b", "aaaaab").group()) # aaaaab
-
[](方括号): 定义一个字符集。匹配集合中的任何单个字符。[abc]: 匹配 ‘a’、’b’ 或 ‘c’。[a-z]: 匹配任何小写字母。[A-Z]: 匹配任何大写字母。[0-9]: 匹配任何数字。[a-zA-Z0-9]: 匹配任何字母数字字符。[^abc]: 匹配任何不是 ‘a’、’b’ 或 ‘c’ 的字符(否定)。
python
print(re.search(r"[aeiou]", "hello").group()) # e
print(re.search(r"[0-9]+", "The year is 2023.").group()) # 2023
print(re.search(r"[^aeiou]", "apple").group()) # p
-
|(竖线): 作为 OR 运算符。匹配竖线前或竖线后的表达式。
python
print(re.search(r"cat|dog", "I have a cat.").group()) # cat
print(re.search(r"cat|dog", "I have a dog.").group()) # dog -
()(圆括号): 创建一个捕获组。用于分组模式的一部分以及提取匹配的子字符串。
python
print(re.search(r"(ab)+", "ababab").group()) # ababab -
\(反斜杠): 转义特殊字符。如果您想匹配字面元字符(例如,字面点号.、星号*或反斜杠\),您必须用反斜杠对其进行转义。
python
print(re.search(r"a\.b", "a.b").group()) # a.b
print(re.search(r"a\*b", "a*b").group()) # a*b
print(re.search(r"C:\\Users", "C:\Users").group()) # C:\Users (使用原始字符串)
5. 特殊序列
特殊序列是预定义的字符集,通常由反斜杠后跟一个字符表示。
-
\d: 匹配任何数字(0-9)。等同于[0-9]。
python
print(re.findall(r"\d", "Phone: 123-456-7890")) # ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'] -
\D: 匹配任何非数字字符。等同于[^0-9]。
python
print(re.findall(r"\D", "Phone: 123-456-7890")) # ['P', 'h', 'o', 'n', 'e', ':', ' ', '-', '-'] -
\w: 匹配任何单词字符(字母数字 + 下划线)。等同于[a-zA-Z0-9_]。
python
print(re.findall(r"\w", "Hello_World 123!")) # ['H', 'e', 'l', 'l', 'o', '_', 'W', 'o', 'r', 'l', 'd', '1', '2', '3'] -
\W: 匹配任何非单词字符。等同于[^a-zA-Z0-9_]。
python
print(re.findall(r"\W", "Hello_World 123!")) # [' ', '!'] -
\s: 匹配任何空白字符(空格、制表符、换行符等)。
python
print(re.findall(r"\s", "Hello World\tPython\nRegex")) # [' ', '\t', '\n'] -
\S: 匹配任何非空白字符。
python
print(re.findall(r"\S", "Hello World")) # ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] -
\b: 匹配单词边界。单词边界是单词字符 (\w) 和非单词字符 (\W) 之间的位置,或者如果其后/前是单词字符,则位于字符串的开头/结尾。
python
print(re.findall(r"\bcat\b", "The cat sat on the mat.")) # ['cat']
print(re.findall(r"\bcat\b", "category")) # []
print(re.findall(r"\bcat", "category").group()) # cat (匹配 'category' 的开头) -
\B: 匹配非单词边界。
python
print(re.findall(r"\Bcat\B", "The wildcat sat.")) # ['cat'] (匹配 'wildcat' 中的 'cat')
print(re.findall(r"\Bcat\B", "catamaran")) # []
6. 贪婪与非贪婪量词
默认情况下,量词 (*、+、?、{m,n}) 是贪婪的。这意味着它们会尝试匹配尽可能多的文本。
“`python
text = “
Title
“
贪婪: 从第一个 < 匹配到最后一个 >
print(re.search(r”<.*>”, text).group()) #
Title
“`
要使量词非贪婪(或惰性),在其后附加一个 ?。非贪婪量词会匹配尽可能少的文本。
“`python
text = “
Title
“
非贪婪: 从第一个 < 匹配到下一个 >
print(re.search(r”<.*?>”, text).group()) #
“`
这在处理 HTML 或 XML 等结构化文本时至关重要,因为您希望匹配单个标签而不是整个块。
7. 组
圆括号 () 用于在正则表达式中创建组。
捕获组
当您将正则表达式的一部分括在圆括号中时,它就成为一个捕获组。然后,您可以提取这些组的匹配内容。
match_obj.group(0)或match_obj.group(): 返回整个匹配。match_obj.group(1): 返回第一个捕获组的内容。match_obj.group(N): 返回第 N 个捕获组的内容。match_obj.groups(): 返回包含所有捕获组的元组。
“`python
import re
text = “Name: John Doe, Age: 30″
pattern = r”Name: (\w+ \w+), Age: (\d+)”
match_obj = re.search(pattern, text)
if match_obj:
print(f”Full match: {match_obj.group(0)}”) # Name: John Doe, Age: 30
print(f”Name: {match_obj.group(1)}”) # John Doe
print(f”Age: {match_obj.group(2)}”) # 30
print(f”All groups: {match_obj.groups()}”) # (‘John Doe’, ’30’)
“`
命名组
您可以使用 (?P<name>...) 为捕获组命名。这会使您的代码更具可读性,并允许您按名称访问组。
match_obj.group('name'): 返回命名组的内容。match_obj.groupdict(): 返回所有命名组的字典。
“`python
import re
text = “Date: 2023-10-26”
pattern = r”Date: (?P
match_obj = re.search(pattern, text)
if match_obj:
print(f”Year: {match_obj.group(‘year’)}”) # 2023
print(f”Month: {match_obj.group(‘month’)}”) # 10
print(f”Day: {match_obj.group(‘day’)}”) # 26
print(f”All named groups: {match_obj.groupdict()}”)
# 输出: {‘year’: ‘2023’, ‘month’: ’10’, ‘day’: ’26’}
“`
非捕获组
有时您需要对模式的一部分进行分组以应用量词或备选项,但您不想捕获它们的内容。您可以使用 (?:...) 进行非捕获组。
“`python
import re
text = “apple orange banana”
(?:apple|orange) 对备选项进行分组而不捕获它们
这允许 + 量词应用于整个组
print(re.findall(r”(?:apple|orange)+”, text)) # [‘apple’, ‘orange’]
与捕获组比较:
print(re.findall(r”(apple|orange)+”, text)) # [‘apple’, ‘orange’] (仍然有效,但会捕获)
“`
8. 前瞻和后瞻断言
前瞻和后瞻断言是零宽度断言。它们不消耗字符串中的字符,但断言当前位置之后或之前存在(或不存在)模式。
正向前瞻 (?=...)
断言 (...) 中的模式必须紧随当前位置,但不包含在匹配中。
“`python
import re
text = “apple pie, banana split, cherry tart”
查找后跟 ‘ pie’ 的单词
print(re.findall(r”\w+(?= pie)”, text)) # [‘apple’]
查找后跟 ‘USD’ 的数字
print(re.findall(r”\d+(?=USD)”, “Price: 100USD, 50EUR”)) # [‘100’]
“`
负向前瞻 (?!...)
断言 (...) 中的模式必须不紧随当前位置。
“`python
import re
text = “apple pie, banana split, cherry tart”
查找不后跟 ‘ pie’ 的单词
print(re.findall(r”\b\w+\b(?! pie)”, text)) # [‘banana’, ‘split’, ‘cherry’, ‘tart’]
查找不是 ‘apple’ 的单词
print(re.findall(r”\b(?!apple)\w+\b”, text)) # [‘pie’, ‘banana’, ‘split’, ‘cherry’, ‘tart’]
“`
正向后瞻 (?<=...)
断言 (...) 中的模式必须位于当前位置之前,但不包含在匹配中。在 Python 的 re 模块中,(?<=...) 中的模式必须具有固定长度。
“`python
import re
text = “Price: $100, Cost: $50”
查找前面有 ‘$’ 的数字
print(re.findall(r”(?<=\$)\d+”, text)) # [‘100′, ’50’]
“`
负向后瞻 (?<!...)
断言 (...) 中的模式必须不位于当前位置之前。(?<!...) 中的模式也必须具有固定长度。
“`python
import re
text = “USD100, EUR50, JPY200”
查找前面没有 ‘USD’ 的数字
print(re.findall(r”(?<!USD)\d+”, text)) # [’50’, ‘200’]
“`
9. re.sub(pattern, repl, string, count=0, flags=0)
此函数将 string 中 pattern 的出现替换为 repl。
* repl 可以是字符串或函数。
* count 是要替换的模式出现的最大次数。
“`python
import re
text = “The quick brown fox.”
将 ‘fox’ 替换为 ‘dog’
new_text = re.sub(r”fox”, “dog”, text)
print(new_text) # The quick brown dog.
text2 = “123-456-7890”
将数字替换为 ‘#’
new_text2 = re.sub(r”\d”, “#”, text2)
print(new_text2) # ###-###-####
使用函数作为 repl (例如,将数字加倍)
def double_number(match):
return str(int(match.group(0)) * 2)
text3 = “Numbers: 10, 20, 30″
new_text3 = re.sub(r”\d+”, double_number, text3)
print(new_text3) # Numbers: 20, 40, 60
在 repl 字符串中使用反向引用
text4 = “Name: Doe, John”
new_text4 = re.sub(r”(\w+), (\w+)”, r”\2 \1″, text4)
print(new_text4) # Name: John Doe
“`
re.subn(pattern, repl, string, count=0, flags=0)
与 re.sub() 类似,但返回一个元组 (new_string, number_of_substitutions_made)。
“`python
import re
text = “one two one three”
new_text, count = re.subn(r”one”, “single”, text)
print(f”New text: {new_text}, Replacements: {count}”)
输出: New text: single two single three, Replacements: 2
“`
10. re.split(pattern, string, maxsplit=0, flags=0)
此函数根据 pattern 的出现来拆分 string。
* maxsplit 是要执行的最大拆分次数。
“`python
import re
text = “apple,banana;cherry orange”
按逗号、分号或空格拆分
parts = re.split(r”[,;\s]”, text)
print(parts) # [‘apple’, ‘banana’, ‘cherry’, ‘orange’]
text2 = “one,two,three,four”
只拆分一次
parts2 = re.split(r”,”, text2, maxsplit=1)
print(parts2) # [‘one’, ‘two,three,four’]
“`
11. 编译 (re.compile())
如果您要在代码中多次使用相同的正则表达式模式,使用 re.compile() 编译它会更高效。这会将正则表达式编译成一个正则表达式对象,然后可以用于匹配。
“`python
import re
一次编译模式
email_pattern = re.compile(r”(\w+)@(\w+.\w+)”)
text1 = “Contact us at [email protected]”
text2 = “My email is [email protected]”
match1 = email_pattern.search(text1)
if match1:
print(f”Email 1: {match1.group()}”)
match2 = email_pattern.search(text2)
if match2:
print(f”Email 2: {match2.group()}”)
“`
12. 标志
标志修改正则表达式匹配的行为。它们可以作为可选参数传递给 re 函数或 re.compile()。
-
re.IGNORECASE或re.I: 执行不区分大小写的匹配。
python
print(re.search(r"hello", "Hello World", re.I).group()) # Hello -
re.MULTILINE或re.M: 当指定此标志时,^和$元字符匹配字符串中每行的开头和结尾,而不仅仅是整个字符串的开头和结尾。
python
text = "Line 1\nLine 2\nLine 3"
print(re.findall(r"^Line", text)) # ['Line'] (没有 re.M,只匹配第一行)
print(re.findall(r"^Line", text, re.M)) # ['Line', 'Line', 'Line'] -
re.DOTALL或re.S: 使.元字符匹配任何字符,包括换行符。
python
text = "Hello\nWorld"
print(re.search(r"Hello.World", text)) # None (没有 re.S,. 不匹配 \n)
print(re.search(r"Hello.World", text, re.S).group()) # Hello\nWorld -
re.VERBOSE或re.X: 允许您通过忽略空白和允许模式中注释来编写更具可读性的正则表达式。
“`python
pattern = re.compile(r”””
^(\d{3}) # 区号 (3 位数字)
\s* # 可选空白
(\d{3}) # 前 3 位数字
-? # 可选连字符
(\d{4})$ # 后 4 位数字
“””, re.VERBOSE)print(pattern.search(“123 456-7890”).groups()) # (‘123’, ‘456’, ‘7890’)
“`
您可以使用按位或运算符 | 组合标志:
python
print(re.search(r"hello", "Hello\nworld", re.I | re.S).group()) # Hello
13. 最佳实践和技巧
- 使用原始字符串 (
r"..."): 始终为正则表达式模式使用原始字符串,以避免反斜杠转义问题。 - 测试您的正则表达式: 使用在线正则表达式测试工具(例如,regex101.com、pythex.org)交互式地构建和测试您的模式。
- 从简单开始: 逐步构建复杂的模式。从核心匹配开始,然后添加更多约束。
- 具体: 您的模式越具体,意外匹配文本的可能性就越小。
- 重复使用时使用
re.compile(): 提高性能和可读性。 - 使用命名组: 使数据提取比依赖数字索引清晰得多。
- 对于复杂模式考虑
re.VERBOSE: 显著提高可读性。 - 避免过度依赖: 尽管功能强大,但正则表达式并非总是最佳工具。对于简单的字符串操作(例如,检查字符串是否以某个前缀开头),
startswith()、endswith()、find()、replace()等字符串方法通常更高效且更具可读性。 - 错误处理: 始终检查
re.search()或re.match()是否返回Match对象,然后再尝试在其上调用.group(),因为如果没有找到匹配项,它们会返回None。
结论
正则表达式是任何处理文本数据的 Python 开发人员不可或缺的技能。通过掌握元字符、特殊序列、组和标志,您可以有效地解决各种字符串操作和模式匹配任务。实践是熟练的关键,因此请尝试不同的模式和文本以巩固您的理解。