Python 正则表达式完全指南:从入门到精通
正则表达式(Regular Expression,简称 RegEx 或 RegExp)是一种强大的字符串处理工具,它使用一套特殊的字符组合来定义模式,从而实现对字符串的搜索、匹配、查找、替换和分割等操作。无论是在数据清洗、文本分析、日志处理、网络爬虫还是其他需要处理大量文本数据的场景中,正则表达式都是一个不可或缺的利器。
Python 通过其内置的 re
模块提供了对正则表达式的完整支持。本文将详细介绍如何在 Python 中使用 re
模块,从基础概念到高级用法,带你全面掌握 Python 正则表达式。
1. 正则表达式基础:它是什么以及为什么使用它?
想象一下你需要从一篇长文本中找出所有的电话号码,或者验证一个用户的邮箱地址格式是否正确,再或者替换掉文本中所有连续的空格为一个空格。如果仅使用 Python 字符串的内置方法(如 find()
, replace()
, split()
),这些任务可能会变得异常繁琐且代码冗长易错。
正则表达式提供了一种简洁而强大的方式来描述和匹配字符串的模式。它不仅仅是简单的文本查找,更是一种模式匹配语言。通过学习正则表达式的语法,你可以创建高度灵活和精确的模式来应对各种复杂的文本处理需求。
Python 的 re
模块是实现这一切的接口。它提供了一系列函数,允许你在 Python 代码中编译、搜索、匹配、查找、替换和分割字符串,所有这些都基于强大的正则表达式模式。
2. Python re
模块核心函数
使用 Python 的 re
模块前,首先需要导入它:
python
import re
re
模块提供了许多函数,其中最常用和核心的包括:
re.match()
: 尝试从字符串的起始位置匹配一个模式。如果匹配成功,返回一个匹配对象(Match Object);否则返回None
。re.search()
: 扫描整个字符串,查找模式的第一个匹配项。如果找到,返回一个匹配对象;否则返回None
。re.findall()
: 在字符串中查找模式的所有非重叠匹配项,并将它们作为列表返回。re.finditer()
: 在字符串中查找模式的所有非重叠匹配项,并返回一个迭代器,迭代器中的每个元素都是一个匹配对象。re.sub()
: 在字符串中查找模式的所有匹配项,并将它们替换为指定的字符串。返回替换后的新字符串。re.split()
: 根据模式在字符串中进行分割,返回分割后的子字符串列表。re.compile()
: 将一个正则表达式模式编译成一个正则表达式对象。编译后的对象可以重复使用,通常能提高性能,特别是在循环中或多次使用同一个模式时。
接下来,我们将通过示例详细介绍这些函数。
2.1 re.match()
与 re.search()
的区别
理解 match()
和 search()
的区别是初学者常遇到的问题。
re.match(pattern, string, flags=0)
只会尝试从 string
的开头匹配 pattern
。
“`python
import re
text = “hello world”
pattern = “hello”
match_result = re.match(pattern, text)
if match_result:
print(f”match() 匹配成功: {match_result.group()}”) # match() 匹配成功: hello
else:
print(“match() 匹配失败”)
pattern_fail = “world”
match_result_fail = re.match(pattern_fail, text)
if match_result_fail:
print(f”match() 匹配成功: {match_result_fail.group()}”)
else:
print(“match() 匹配失败”) # match() 匹配失败
“`
re.search(pattern, string, flags=0)
会扫描整个 string
,直到找到第一个与 pattern
匹配的位置。
“`python
import re
text = “hello world”
pattern = “world”
search_result = re.search(pattern, text)
if search_result:
print(f”search() 匹配成功: {search_result.group()}”) # search() 匹配成功: world
else:
print(“search() 匹配失败”)
pattern_also_matches = “hello”
search_result_also = re.search(pattern_also_matches, text)
if search_result_also:
print(f”search() 也匹配开头: {search_result_also.group()}”) # search() 也匹配开头: hello
“`
总结:如果你只关心字符串的开头是否匹配特定模式,使用 match()
。如果你想在整个字符串中查找模式的第一个出现,使用 search()
。
2.2 re.findall()
与 re.finditer()
re.findall(pattern, string, flags=0)
返回所有匹配模式的非重叠子字符串的列表。
“`python
import re
text = “hello world hello python hello regex”
pattern = “hello”
all_matches = re.findall(pattern, text)
print(f”findall() 结果: {all_matches}”) # findall() 结果: [‘hello’, ‘hello’, ‘hello’]
查找所有数字
text_digits = “price is 10 dollars, quantity is 50 units.”
pattern_digits = r”\d+” # \d+ 匹配一个或多个数字
all_digits = re.findall(pattern_digits, text_digits)
print(f”查找所有数字: {all_digits}”) # 查找所有数字: [’10’, ’50’]
“`
re.finditer(pattern, string, flags=0)
返回一个迭代器,其元素是匹配对象。这在需要获取匹配的详细信息(如位置、分组捕获)时非常有用,且对于大型文本来说,迭代器通常比一次性加载所有结果到列表中更节省内存。
“`python
import re
text = “hello world hello python hello regex”
pattern = “hello”
for match in re.finditer(pattern, text):
print(f”finditer() 找到匹配: {match.group()} 在位置 {match.span()}”)
# finditer() 找到匹配: hello 在位置 (0, 5)
# finditer() 找到匹配: hello 在位置 (12, 17)
# finditer() 找到匹配: hello 在位置 (25, 30)
“`
2.3 re.sub()
– 替换
re.sub(pattern, repl, string, count=0, flags=0)
找到 string
中所有匹配 pattern
的子字符串,并用 repl
替换它们。count
指定最多替换的次数,默认为 0 (替换所有匹配项)。repl
可以是字符串或函数。
“`python
import re
text = “This has too many spaces.”
pattern = r”\s+” # \s+ 匹配一个或多个空白字符
replacement = ” “
new_text = re.sub(pattern, replacement, text)
print(f”替换空格后: {new_text}”) # 替换空格后: This has too many spaces.
替换前两个匹配
text_multiple = “apple, banana, orange, apple, grape”
pattern_fruit = “apple”
replacement_new = “pear”
new_text_limited = re.sub(pattern_fruit, replacement_new, text_multiple, count=2)
print(f”限定替换次数: {new_text_limited}”) # 限定替换次数: pear, banana, orange, pear, grape
“`
repl
也可以是一个函数,该函数接收一个匹配对象作为参数,并返回用于替换的字符串。这在需要根据匹配内容动态生成替换文本时非常有用。
“`python
import re
text = “Increment numbers: 10, 25, 5″
pattern = r”\d+”
def increment(match):
number = int(match.group(0))
return str(number + 1)
new_text_incremented = re.sub(pattern, increment, text)
print(f”数字递增后: {new_text_incremented}”) # 数字递增后: Increment numbers: 11, 26, 6
“`
2.4 re.split()
– 分割
re.split(pattern, string, maxsplit=0, flags=0)
根据 pattern
匹配的子字符串来分割 string
。maxsplit
指定最多分割的次数,默认为 0 (不限制)。
“`python
import re
text = “apple,banana;cherry orange”
根据逗号、分号或空格分割
pattern = r”[;, ]”
parts = re.split(pattern, text)
print(f”分割结果: {parts}”) # 分割结果: [‘apple’, ‘banana’, ‘cherry’, ‘orange’]
限定分割次数
text_limit = “A,B,C,D,E”
pattern_comma = “,”
parts_limited = re.split(pattern_comma, text_limit, maxsplit=2)
print(f”限定分割次数结果: {parts_limited}”) # 限定分割次数结果: [‘A’, ‘B’, ‘C,D,E’]
“`
2.5 re.compile()
– 编译正则表达式
如果一个正则表达式模式会被多次使用,使用 re.compile(pattern, flags=0)
将其编译成一个正则表达式对象可以提高效率。编译后的对象具有与 re
模块函数同名的方法(match
, search
, findall
, finditer
, sub
, split
)。
“`python
import re
import time
不编译的情况
start_time = time.time()
for _ in range(10000):
re.search(r”\d+”, “abc123xyz”)
end_time = time.time()
print(f”不编译耗时: {end_time – start_time:.6f}秒”)
编译的情况
compiled_pattern = re.compile(r”\d+”)
start_time = time.time()
for _ in range(10000):
compiled_pattern.search(“abc123xyz”)
end_time = time.time()
print(f”编译后耗时: {end_time – start_time:.6f}秒”)
编译后的对象使用方法
text_compiled = “hello world, version 1.0″
compiled_match = compiled_pattern.search(text_compiled)
if compiled_match:
print(f”编译对象搜索结果: {compiled_match.group()}”) # 编译对象搜索结果: 123 (来自上面的搜索)
# 如果用text_compiled搜索,则匹配 1
compiled_pattern_version = re.compile(r”\d+.\d+”)
version_match = compiled_pattern_version.search(text_compiled)
if version_match:
print(f”编译对象搜索结果 (版本): {version_match.group()}”) # 编译对象搜索结果 (版本): 1.0
“`
对于简单的脚本或只使用一次的模式,编译的性能提升可能不明显。但在处理大量文本或在循环中重复使用同一模式时,编译的优势会体现出来。
3. 正则表达式语法核心元素
正则表达式的强大之处在于其丰富的语法。掌握这些语法元素是使用正则表达式的关键。
3.1 普通字符与元字符
- 普通字符: 大多数字符(如字母、数字、标点符号)都匹配它们自身。例如,
abc
就匹配字符串 “abc”。 - 元字符 (Metacharacters): 具有特殊含义的字符。包括:
. ^ $ * + ? { } [ ] \ | ( )
。要匹配这些元字符本身,需要使用反斜杠\
进行转义。
3.2 反斜杠 \
与原始字符串 r"..."
在 Python 中,反斜杠 \
既用于转义正则表达式中的元字符(如 \.
匹配点号),也用于 Python 字符串自身的转义(如 \n
表示换行)。这就可能导致混淆。例如,正则表达式 \n
在 Python 字符串中需要写成 "\\n"
。
为了避免这种双重转义的麻烦,强烈推荐使用 Python 的原始字符串(Raw String),以 r
开头。在原始字符串中,反斜杠 \
只会被解释为字面意思,不会用于 Python 自身的转义。
“`python
匹配换行符
pattern_newline_normal = “\n” # 在普通字符串中需要双反斜杠
pattern_newline_raw = r”\n” # 在原始字符串中只需要单反斜杠
print(re.search(pattern_newline_normal, “hello\nworld”))
print(re.search(pattern_newline_raw, “hello\nworld”))
匹配反斜杠本身
pattern_backslash_normal = “\\” # 需要四个反斜杠才能在普通字符串中表示两个,一个用于Python转义,一个用于正则转义
pattern_backslash_raw = r”\” # 在原始字符串中只需要两个反斜杠来匹配一个反斜杠
print(re.search(pattern_backslash_normal, “path\to\file”))
print(re.search(pattern_backslash_raw, “path\to\file”))
“`
从现在开始,我们将默认使用原始字符串来书写正则表达式。
3.3 字符集 []
方括号 []
定义一个字符集,匹配其中任意一个字符。
[abc]
匹配 ‘a’, ‘b’, 或 ‘c’。[0-9]
匹配任何数字。[a-z]
匹配任何小写字母。[A-Z]
匹配任何大写字母。[a-zA-Z]
匹配任何字母。[a-zA-Z0-9]
匹配任何字母或数字。[+-*/]
匹配加号、减号、乘号或斜杠。
在字符集内部,大多数元字符(如 .
*
+
?
|
()
)会失去其特殊含义,被视为普通字符。但有几个例外:
* ^
在字符集开头表示否定:[^0-9]
匹配任何非数字字符。
* -
表示范围:[a-z]
。要匹配字面意义的 -
,可以放在开头或结尾,或者使用 \-
转义:[+-]
或 [-+]
或 [\-+.]
。
* ]
要匹配字面意义的 ]
,放在开头或使用 \]
转义:[]abc]
或 [abc\]]
.
* \
仍然用于转义:[\$]
匹配美元符号。
“`python
import re
text = “color: red, Color: Blue, cOLOR: green”
匹配 color,忽略大小写 (使用 flags=re.IGNORECASE 或 re.I)
pattern_color = r”[Cc][Oo][Ll][Oo][Rr]”
print(re.findall(pattern_color, text)) # [‘color’, ‘Color’, ‘cOLOR’]
匹配非数字字符
text_mixed = “Item #123 costing $45.67″
pattern_non_digit = r”[^0-9]”
print(re.findall(pattern_non_digit, text_mixed))
[‘I’, ‘t’, ‘e’, ‘m’, ‘ ‘, ‘#’, ‘ ‘, ‘ ‘, ‘c’, ‘o’, ‘s’, ‘t’, ‘i’, ‘n’, ‘g’, ‘ ‘, ‘$’, ‘.’, ”] – 注意空格和标点都匹配了
“`
3.4 预定义字符类 (Special Sequences)
re
模块提供了一些常用的预定义字符类,方便表示常见的字符集。它们都由反斜杠开头:
\d
: 匹配任何数字,相当于[0-9]
。\D
: 匹配任何非数字字符,相当于[^0-9]
。\w
: 匹配任何字母数字字符(包括下划线),相当于[a-zA-Z0-9_]
。\W
: 匹配任何非字母数字字符,相当于[^a-zA-Z0-9_]
。\s
: 匹配任何空白字符(空格、制表符、换行符等),相当于[ \t\n\r\f\v]
。\S
: 匹配任何非空白字符,相当于[^ \t\n\r\f\v]
。
“`python
import re
text = “The price is $15.99 for item_1.”
print(re.findall(r”\d”, text)) # [‘1’, ‘5’, ‘9’, ‘9’, ‘1’]
print(re.findall(r”\D”, text)) # [‘T’, ‘h’, ‘e’, ‘ ‘, ‘p’, ‘r’, ‘i’, ‘c’, ‘e’, ‘ ‘, ‘i’, ‘s’, ‘ ‘, ‘$’, ‘.’, ‘ ‘, ‘f’, ‘o’, ‘r’, ‘ ‘, ‘i’, ‘t’, ‘e’, ‘m’, ‘‘, ‘.’]
print(re.findall(r”\w”, text)) # [‘T’, ‘h’, ‘e’, ‘p’, ‘r’, ‘i’, ‘c’, ‘e’, ‘i’, ‘s’, ‘f’, ‘o’, ‘r’, ‘i’, ‘t’, ‘e’, ‘m’, ‘‘, ‘1’]
print(re.findall(r”\W”, text)) # [‘ ‘, ‘ ‘, ‘$’, ‘.’, ‘ ‘, ‘ ‘, ‘.’]
print(re.findall(r”\s”, text)) # [‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘]
print(re.findall(r”\S”, text)) # [‘T’, ‘h’, ‘e’, ‘p’, ‘r’, ‘i’, ‘c’, ‘e’, ‘i’, ‘s’, ‘$’, ‘1’, ‘5’, ‘.’, ‘9’, ‘9’, ‘f’, ‘o’, ‘r’, ‘i’, ‘t’, ‘e’, ‘m’, ‘_’, ‘1’, ‘.’]
“`
3.5 量词 (Quantifiers)
量词用于指定匹配前一个元素(字符、字符集或分组)出现的次数。
*
: 匹配前一个元素 零次或多次。相当于{0,}
。+
: 匹配前一个元素 一次或多次。相当于{1,}
。?
: 匹配前一个元素 零次或一次。相当于{0,1}
。{n}
: 匹配前一个元素 恰好 n 次。{n,}
: 匹配前一个元素 至少 n 次。{n,m}
: 匹配前一个元素 至少 n 次,但不超过 m 次。
“`python
import re
text = “color co col colou colour”
print(re.findall(r”col”, text)) # [‘col’, ‘col’, ‘col’, ‘col’, ‘col’]
print(re.findall(r”colo*”, text)) # [‘colo’, ‘colo’, ‘colo’, ‘colou’, ‘colour’] – * 匹配 o 零次或多次
print(re.findall(r”colo+”, text)) # [‘colo’, ‘colo’, ‘colou’, ‘colour’] – + 匹配 o 一次或多次
print(re.findall(r”colo?”, text)) # [‘colo’, ‘colo’, ‘colo’, ‘colo’, ‘colo’] – ? 匹配 o 零次或一次
print(re.findall(r”colo{2}”, text)) # [] – o 恰好出现 2 次,没有匹配
print(re.findall(r”colo{1,2}”, text))# [‘colo’, ‘colo’, ‘colo’, ‘colou’] – o 出现 1 或 2 次
text_nums = “1 12 123 1234 12345″
print(re.findall(r”\d{3}”, text_nums)) # [‘123’, ‘234’] – 恰好 3 个数字
print(re.findall(r”\d{2,4}”, text_nums)) # [’12’, ‘123’, ‘1234’] – 2到4个数字
print(re.findall(r”\d{3,}”, text_nums)) # [‘123’, ‘1234’, ‘12345’] – 至少3个数字
“`
默认情况下,量词是贪婪的 (Greedy),会尽可能多地匹配字符。例如,.*
会匹配到行尾。
可以通过在量词后加上 ?
使其变为非贪婪的 (Non-Greedy) 或惰性的,尽可能少地匹配字符。
“`python
import re
text = “Hello World“
贪婪匹配,从第一个到最后一个
print(re.search(r”<.*>”, text).group()) # Hello World
非贪婪匹配,只匹配最近的一对<>
print(re.search(r”<.*?>”, text).group()) #
text_greedy = “abcde”
print(re.search(r”a.c”, text_greedy).group()) # abc
print(re.search(r”a.?c”, text_greedy).group()) # abc (这里结果一样,因为贪婪也是到c就停了)
text_more = “a b c a d c”
print(re.search(r”a.c”, text_more).group()) # a b c a d c (贪婪匹配到最后一个c)
print(re.search(r”a.?c”, text_more).group()) # a b c (非贪婪匹配到第一个c)
“`
3.6 定位符 (Anchors)
定位符不匹配任何字符,而是匹配位置。
^
: 匹配字符串的开头。在re.MULTILINE
标志下,也匹配每一行的开头。$
: 匹配字符串的结尾。在re.MULTILINE
标志下,也匹配每一行的结尾。\b
: 匹配单词边界。单词边界指一个\w
字符与一个\W
字符之间的位置,或\w
字符与字符串开头/结尾之间的位置。\B
: 匹配非单词边界。
“`python
import re
text = “hello world\nworld hello”
^ 匹配开头
print(re.findall(r”^world”, text)) # [] – world 不在开头
print(re.findall(r”^hello”, text)) # [‘hello’]
$ 匹配结尾
print(re.findall(r”world$”, text)) # [] – world 不在结尾
print(re.findall(r”hello$”, text)) # [‘hello’]
使用 re.MULTILINE (re.M) 标志
print(re.findall(r”^world”, text, re.M)) # [‘world’] – 第二行的开头
print(re.findall(r”hello$”, text, re.M)) # [‘hello’] – 第一行的结尾
text_words = “cat caterpillar category”
\b 匹配单词边界
print(re.findall(r”\bcat\b”, text_words)) # [‘cat’] – 只匹配独立的单词 cat
print(re.findall(r”cat\B”, text_words)) # [‘cat’, ‘cat’] – cat 后面不是单词边界 (后面跟着字母)
print(re.findall(r”\Bcat”, text_words)) # [‘cat’] – cat 前面不是单词边界 (前面跟着字母 ‘er’ in caterpillar)
“`
3.7 点号 .
点号 .
匹配除换行符 \n
外的任何单个字符。
如果想让 .
也匹配换行符,可以使用 re.DOTALL
(或 re.S
) 标志。
“`python
import re
text = “a.b\nc”
print(re.findall(r”a.b”, text)) # [‘a.b’]
print(re.findall(r”a.c”, text)) # [] – . 不匹配 \n
print(re.findall(r”a.c”, text, re.S)) # [‘a\nc’] – 在 DOTALL 模式下,. 匹配 \n
“`
3.8 分组与捕获 ()
圆括号 ()
用于将一个或多个字符组合成一个整体,视为一个分组。分组有以下用途:
- 应用量词:
(abc)+
匹配一个或多个连续的 “abc”。 - 限定范围:
(a|b)c
匹配 “ac” 或 “bc”。 - 捕获匹配的子字符串: 这是分组最常用的功能。每个分组都会捕获其匹配的文本,并可以在匹配对象中通过索引或名称访问。
“`python
import re
text = “Phone: 123-456-7890”
匹配数字-数字-数字 的模式,并捕获每一部分
pattern_phone = r”(\d{3})-(\d{3})-(\d{4})”
match = re.search(pattern_phone, text)
if match:
print(f”完整匹配: {match.group(0)}”) # 完整匹配: 123-456-7890 (group(0) 或 group() 总是指整个匹配)
print(f”区号: {match.group(1)}”) # 区号: 123 (第一个分组)
print(f”前三位: {match.group(2)}”) # 前三位: 456 (第二个分组)
print(f”后四位: {match.group(3)}”) # 后四位: 7890 (第三个分组)
print(f”所有分组: {match.groups()}”) # 所有分组: (‘123’, ‘456’, ‘7890’)
在 findall() 中使用分组,如果模式中包含分组,findall() 返回的是分组内容的元组列表
text_emails = “[email protected], [email protected]”
pattern_emails = r”(\w+)@([\w.]+)”
print(re.findall(pattern_emails, text_emails)) # [(‘user1’, ‘example.com’), (‘user2’, ‘mail.org’)]
如果模式没有分组,findall() 返回的是整个匹配项的列表
pattern_no_group = r”\w+@[\w.]+”
print(re.findall(pattern_no_group, text_emails)) # [‘[email protected]’, ‘[email protected]’]
“`
3.9 非捕获分组 (?:...)
有时你只需要将多个项组合起来应用量词或使用 |
进行选择,但又不关心捕获这个分组的内容。这时可以使用非捕获分组 (?:...)
。它可以提高一点性能,并且不会影响分组的索引。
“`python
import re
text = “apple, banana, orange”
匹配 apple 或 banana 或 orange
pattern_fruits = r”(?:apple|banana|orange)”
print(re.findall(pattern_fruits, text)) # [‘apple’, ‘banana’, ‘orange’]
比较捕获分组和非捕获分组在 findall 中的返回
text_compare = “itemA:10, itemB:20″
pattern_capture = r”(item\w+):(\d+)”
pattern_non_capture = r”(?:item\w+):(\d+)” # 外层是非捕获,内层捕获数字
print(re.findall(pattern_capture, text_compare)) # [(‘itemA’, ’10’), (‘itemB’, ’20’)] – 捕获两个分组
print(re.findall(pattern_non_capture, text_compare)) # [’10’, ’20’] – 只捕获了数字分组
“`
3.10 命名分组 (?P<name>...)
为了提高代码的可读性,可以使用命名分组。通过 (?P<name>...)
语法,可以为分组指定一个名称。在匹配对象中,可以通过名称或索引来访问捕获的内容。
“`python
import re
text = “Born on 1990-05-20”
pattern_date = r”(?P
match = re.search(pattern_date, text)
if match:
print(f”年份 (按名称): {match.group(‘year’)}”) # 年份 (按名称): 1990
print(f”月份 (按名称): {match.group(‘month’)}”) # 月份 (按名称): 05
print(f”日期 (按名称): {match.group(‘day’)}”) # 日期 (按名称): 20
print(f”年份 (按索引): {match.group(1)}”) # 年份 (按索引): 1990
print(f”所有命名分组: {match.groupdict()}”) # 所有命名分组: {‘year’: ‘1990’, ‘month’: ’05’, ‘day’: ’20’}
在 re.sub() 中使用命名分组进行替换
text_swap = “Swap year and month: 1990-05-20”
pattern_swap = r”(?P
在替换字符串中,可以使用 \g 或 \g 引用捕获的分组
replacement_swap = r”\g
new_text_swapped = re.sub(pattern_swap, replacement_swap, text_swap)
print(f”日期格式转换后: {new_text_swapped}”) # 日期格式转换后: Swap year and month: 05/20/1990
“`
3.11 逻辑或 |
竖线 |
表示逻辑 OR。它用于在两个或多个模式之间进行选择。
“`python
import re
text = “apple banana orange grape”
匹配 apple 或 orange
pattern_either = r”apple|orange”
print(re.findall(pattern_either, text)) # [‘apple’, ‘orange’]
结合分组使用
text_colors = “reddish bluely greenish”
匹配 red 或 blue 或 green 后面跟着 ish
pattern_suffixes = r”(red|blue|green)ish”
print(re.findall(pattern_suffixes, text_colors)) # [‘red’, ‘blue’, ‘green’] – findall() 在有分组时只返回分组内容
如果想返回整个匹配,使用非捕获分组
pattern_suffixes_full = r”(?:red|blue|green)ish”
print(re.findall(pattern_suffixes_full, text_colors)) # [‘reddish’, ‘bluely’, ‘greenish’]
“`
3.12 前向断言与后向断言 (Lookaheads & Lookbehinds)
断言是一种特殊的模式,它们测试某个位置是否满足某个条件,但不消耗字符(即不包含在最终匹配结果中)。这就像一个零宽度匹配。
- 前向肯定断言:
(?=...)
要求...
必须出现在当前位置之后,但不匹配...
本身。 - 前向否定断言:
(?!...)
要求...
不能出现在当前位置之后。 - 后向肯定断言:
(?<=...)
要求...
必须出现在当前位置之前,但不匹配...
本身。 - 后向否定断言:
(?<!...)
要求...
不能出现在当前位置之前。
注意: Python 的后向断言中的模式 ...
必须是固定宽度的,这意味着它必须匹配固定数量的字符(不能包含 *
, +
, ?
, {m,n}
这种可变数量的量词,除非在较新的 Python 版本中支持了某些有限的可变宽度断言)。前向断言没有这个限制。
“`python
import re
text = “apple $10, banana $20, orange 30”
匹配后面跟着 $ 的数字 (只匹配数字本身)
pattern_price = r”\d+(?= \$)” # 匹配一个或多个数字,但要求后面跟着一个空格和一个$
print(re.findall(pattern_price, text)) # [’10’, ’20’]
匹配后面跟着字母的数字 (只匹配数字本身)
pattern_digit_before_letter = r”\d(?=\w)”
print(re.findall(pattern_digit_before_letter, “1a 2b 3 “)) # [‘1’, ‘2’]
匹配前面是 $ 的数字 (只匹配数字本身)
pattern_price_behind = r”(?<=\$)\d+” # 要求前面是一个$,然后匹配一个或多个数字
print(re.findall(pattern_price_behind, text)) # [’10’, ’20’]
匹配前面不是 $ 的数字 (只匹配数字本身)
pattern_not_price = r”(?<!\$)\b\d+\b” # 要求前面不是$,匹配独立的数字
print(re.findall(pattern_not_price, text)) # [’30’]
匹配不含数字的单词
text_words = “word1 word2 nonumbers third4″
pattern_no_digits_word = r”\b(?!\d+\b)\w+\b” # 匹配单词边界,要求后面不是 数字+单词边界,然后匹配单词
print(re.findall(pattern_no_digits_word, text_words)) # [‘nonumbers’]
“`
断言是正则表达式中比较高级和灵活的特性,它们允许在不消耗字符的情况下施加匹配条件。
4. 匹配对象 (Match Object)
当 re.match()
或 re.search()
成功匹配时,它们返回一个匹配对象。这个对象包含了匹配的详细信息,提供了一些有用的方法:
match.group(index=0)
: 返回指定分组捕获的字符串。index=0
(或不带参数)返回整个匹配的子字符串。索引从 1 开始对应捕获分组()
。match.groups()
: 返回一个元组,包含所有捕获分组(索引从 1 开始)的内容。如果没有分组,返回空元组。match.groupdict()
: 返回一个字典,包含所有命名分组的内容,键为分组名,值为捕获的字符串。match.start(group=0)
: 返回指定分组匹配的子字符串在原字符串中的起始索引。match.end(group=0)
: 返回指定分组匹配的子字符串在原字符串中的结束索引(不包含该位置)。match.span(group=0)
: 返回一个元组(start, end)
,表示指定分组匹配的子字符串在原字符串中的起始和结束索引。match.re
: 返回匹配对象使用的正则表达式对象。match.string
: 返回进行匹配的原始字符串。
“`python
import re
text = “Date: 2023-10-27, Time: 14:30″
pattern = r”(\d{4})-(\d{2})-(\d{2}).*?(\d{2}):(\d{2})” # 捕获日期和时间部分
match = re.search(pattern, text)
if match:
print(f”匹配对象: {match}”)
print(f”完整匹配: {match.group()}”)
print(f”年份: {match.group(1)}”)
print(f”月份: {match.group(2)}”)
print(f”日期: {match.group(3)}”)
print(f”小时: {match.group(4)}”)
print(f”分钟: {match.group(5)}”)
print(f"所有分组: {match.groups()}")
print(f"年份起始位置: {match.start(1)}")
print(f"完整匹配结束位置: {match.end(0)}")
print(f"分钟位置区间: {match.span(5)}")
# 使用命名分组的例子 (同上节类似)
pattern_named = r"(?P<date>\d{4}-\d{2}-\d{2}).*?(?P<time>\d{2}:\d{2})"
match_named = re.search(pattern_named, text)
if match_named:
print(f"命名分组: {match_named.groupdict()}")
“`
5. 常用的正则表达式标志 (Flags)
re
模块提供了一些标志来修改匹配的行为。这些标志可以作为函数的最后一个参数传递,或者在编译时传递给 re.compile()
。
re.IGNORECASE
或re.I
: 忽略大小写进行匹配。re.MULTILINE
或re.M
: 使^
匹配字符串开头以及每一行的开头,使$
匹配字符串结尾以及每一行的结尾。re.DOTALL
或re.S
: 使.
匹配包括换行符\n
在内的任何字符。re.VERBOSE
或re.X
: 忽略模式字符串中的空白符(空格、制表符、换行符),并允许在模式中添加注释。这有助于提高复杂正则表达式的可读性。
“`python
import re
text = “Hello World\nhello Python”
re.I (忽略大小写)
print(re.findall(r”hello”, text, re.I)) # [‘Hello’, ‘hello’]
re.M (多行模式)
print(re.findall(r”^hello”, text)) # [] – 只有字符串开头是 Hello
print(re.findall(r”^hello”, text, re.M)) # [‘hello’] – 匹配第二行开头
re.S (DOTALL 模式)
text_multiline = “Line 1\nLine 2″
print(re.search(r”Line.Line”, text_multiline)) # None – . 不匹配 \n
print(re.search(r”Line.Line”, text_multiline, re.S)) #
re.X (详细模式) – 用于写可读性强的复杂模式
pattern_verbose = re.compile(r”””
^ # 匹配字符串开头
(?P
\s+ # 匹配一个或多个空白字符
(?P
$ # 匹配字符串结尾
“””, re.X) # 注意 re.X 标志
match_verbose = pattern_verbose.search(“Alice 30″)
if match_verbose:
print(f”详细模式匹配: {match_verbose.groupdict()}”) # 详细模式匹配: {‘name’: ‘Alice’, ‘age’: ’30’}
``
re.X
使用标志时,模式中的
#到行尾的内容会被视为注释而被忽略(除非
#在字符集
[]` 内或被转义)。这使得你可以像写普通代码一样为正则表达式添加解释。
6. 实际应用示例
结合前面学习的概念,我们可以解决一些实际问题。
6.1 验证邮箱格式 (简化版)
一个简单的邮箱格式通常是 “用户名@域名.后缀”。
“`python
import re
def is_valid_email(email):
# 一个简化的邮箱模式:以字母数字或.%+-开头,后跟@,再跟字母数字或.-,最后是点和至少两个字母
# 这个模式很简化,不能覆盖所有有效邮箱,但用于演示足够
pattern = r”^[a-zA-Z0-9.%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$”
if re.match(pattern, email):
return True
else:
return False
print(f”[email protected] 是邮箱? {is_valid_email(‘[email protected]’)}”) # True
print(f”[email protected] 是邮箱? {is_valid_email(‘[email protected]’)}”) # True
print(f”[email protected] 是邮箱? {is_valid_email(‘[email protected]’)}”) # False (域名不能以点开头)
print(f”@example.com 是邮箱? {is_valid_email(‘@example.com’)}”) # False (用户名不能为空)
print(f”test@example 是邮箱? {is_valid_email(‘test@example’)}”) # False (缺少后缀)
“`
6.2 提取网页中的链接 (简化版)
从 HTML 文本中提取 <a>
标签的 href
属性值。
“`python
import re
html_text = “””
Visit Example.
More info at About Us or Another Page.
FTP Link
“””
匹配 <a 后跟任意字符,然后是 href=” 或 href=’,捕获引号内的内容,再跟任意字符和
使用非贪婪匹配 .*? 防止匹配过长
pattern_href = r'<a.?href=“\’[“\’].?<\/a>’
links = re.findall(pattern_href, html_text)
print(f”提取的链接: {links}”)
提取的链接: [‘http://example.com’, ‘/about’, ‘https://another.com/page’, ‘ftp://resource.com’]
“`
注意: 使用正则表达式解析 HTML 是一种简陋且容易出错的方法,对于复杂的 HTML 结构,强烈推荐使用专门的 HTML 解析库,如 Beautiful Soup 或 lxml。
6.3 从日志文件中提取特定信息
假设日志文件中有这样的条目: [2023-10-27 14:30:01] INFO: User 'Alice' logged in from 192.168.1.100
“`python
import re
log_entry = “[2023-10-27 14:30:01] INFO: User ‘Alice’ logged in from 192.168.1.100”
捕获日期、时间、级别、用户名和IP地址
pattern_log = r”^[(?P
match = re.search(pattern_log, log_entry)
if match:
print(“日志信息提取:”)
print(f” 日期: {match.group(‘date’)}”)
print(f” 时间: {match.group(‘time’)}”)
print(f” 级别: {match.group(‘level’)}”)
print(f” 用户: {match.group(‘user’)}”)
print(f” IP地址: {match.group(‘ip’)}”)
else:
print(“日志格式不匹配”)
“`
7. 正则表达式的局限性与注意事项
尽管正则表达式非常强大,但它并非万能。
- 复杂模式难以理解和维护: 过于复杂的正则表达式很快会变得难以阅读、理解和调试。有时,使用更简单的字符串方法或结合其他工具会更合适。
- 不适合解析嵌套结构: 正则表达式难以正确解析具有任意深度嵌套的结构,例如 HTML、XML 或编程语言代码。对于这类任务,应该使用专门的解析器。
- 性能问题: 虽然
re.compile()
可以提高性能,但写得不好的正则表达式(如过多的回溯)可能导致性能急剧下降(称为 ReDoS – Regular Expression Denial of Service)。 - 避免过度使用: 不要仅仅为了使用正则表达式而使用它。如果简单的字符串方法(如
startswith()
,endswith()
,split()
,replace()
)足够解决问题,优先使用它们,因为它们通常更清晰且性能更高。
8. 调试与测试正则表达式
编写和调试正则表达式通常需要反复尝试。以下是一些有用的方法:
- 使用在线正则表达式测试工具: 许多网站(如 regex101.com, regexper.com, regexr.com)提供了在线测试环境,可以实时看到你的模式匹配了哪些部分,并提供详细解释。
- 从简单开始,逐步构建: 先写一个能匹配核心部分的简单模式,然后逐步添加更多的限制和细节(如量词、边界、断言等)。
- 使用
re.findall()
或re.finditer()
查看所有匹配: 这有助于理解模式在整个字符串中的行为。 - 使用命名分组: 命名分组可以帮助你理解捕获的部分代表什么,提高模式的可读性。
- 使用
re.VERBOSE
标志: 将复杂模式分解并添加注释。
9. 总结
Python 的 re
模块提供了全面而强大的正则表达式支持。通过本文的学习,你应该已经掌握了:
re
模块的核心函数:match()
,search()
,findall()
,finditer()
,sub()
,split()
,compile()
。- 正则表达式的基本语法元素:普通字符、元字符、字符集
[]
、预定义字符类\d
\w
\s
、量词*
+
?
{}
、定位符^
$
\b
\B
、点号.
、分组()
、非捕获分组(?:)
、命名分组(?P<name>)
、逻辑或|
、断言(?=)
(?!)
(?<=)
(?<!)
。 - 原始字符串
r"..."
的重要性。 - 如何使用匹配对象提取匹配信息。
- 常用的正则表达式标志。
- 一些实际的应用示例。
- 正则表达式的局限性以及调试技巧。
正则表达式的学习需要时间和实践。最好的方式是多动手,尝试用正则表达式解决你遇到的各种字符串处理问题。随着经验的积累,你会越来越熟练地运用这个强大的工具。
希望这篇详细的指南能帮助你更好地理解和使用 Python 正则表达式!开始你的正则表达式之旅吧!