轻松掌握 Python 正则表达式使用方法
正则表达式(Regular Expression,简称 Regex 或 RE)是一种强大的文本处理工具,它使用一套特殊的字符组合(模式)来匹配、查找、替换或验证文本字符串。想象一下,如果你需要在海量文本中找出所有符合特定格式的电话号码、邮箱地址,或者批量替换掉文档中的特定词汇,手动操作无疑效率低下且容易出错。这时,正则表达式就成了你的得力助手。
Python通过其内置的 re
模块提供了完整的正则表达式支持。虽然初学时正则表达式的语法可能看起来有些神秘甚至令人生畏,但一旦掌握了其基本原理和常用规则,你会发现它能极大地提升你的文本处理能力。
本文将带你循序渐进地学习如何在 Python 中使用正则表达式,从最基础的概念到常用的函数和技巧,让你轻松掌握这项强大的技能。
第一部分:理解正则表达式的基础概念
在深入 Python 的 re
模块之前,我们首先需要理解正则表达式本身的“语言”。正则表达式由普通字符和元字符组成。普通字符(如字母、数字)只匹配它们自身,而元字符则具有特殊的含义。
1. 普通字符 (Literal Characters)
这是最简单的部分。大多数字符,如字母(a-z, A-Z)、数字(0-9)以及没有特殊含义的符号,都只匹配它们自己。
cat
:只会精确匹配字符串中的 “cat”。123
:只会精确匹配字符串中的 “123”。hello world
:只会精确匹配字符串中的 “hello world”。
2. 元字符 (Metacharacters)
元字符是正则表达式的魔法所在,它们不匹配自身,而是表示某种模式或位置。以下是一些最常用的元字符:
.
(点号): 匹配除换行符之外的任意单个字符。a.b
可以匹配 “aab”, “acb”, “a0b”, “a#b” 等等,但不匹配 “ab” 或 “accdb”。
^
(脱字符): 匹配字符串的开头。^hello
只匹配以 “hello” 开头的字符串,如 “hello world”,但不匹配 “say hello”。
$
(美元符号): 匹配字符串的结尾。world$
只匹配以 “world” 结尾的字符串,如 “hello world”,但不匹配 “world cup”。
*
(星号): 匹配前一个字符出现零次或多次。a*
可以匹配 “”, “a”, “aa”, “aaa” 等等。ab*c
可以匹配 “ac”, “abc”, “abbc”, “abbbc” 等等。
+
(加号): 匹配前一个字符出现一次或多次。a+
可以匹配 “a”, “aa”, “aaa” 等等,但不能匹配 “”。ab+c
可以匹配 “abc”, “abbc”, “abbbc” 等等,但不能匹配 “ac”。
?
(问号): 匹配前一个字符出现零次或一次。a?
可以匹配 “” 或 “a”。ab?c
可以匹配 “ac” 或 “abc”。
{m,n}
(花括号): 匹配前一个字符出现至少 m 次,至多 n 次。a{3}
:精确匹配 “aaa”。a{2,4}
:匹配 “aa”, “aaa”, “aaaa”。a{2,}
:匹配 “aa”, “aaa”, “aaaa”, … (至少两次)。a{,5}
:匹配 “”, “a”, “aa”, “aaa”, “aaaa”, “aaaaa” (至多五次)。
|
(竖线): 匹配竖线左边或右边的表达式 (逻辑 OR)。cat|dog
可以匹配 “cat” 或 “dog”。
()
(圆括号): 分组。可以将多个字符视为一个整体进行操作,也用于捕获匹配的文本。(ab)+
可以匹配 “ab”, “abab”, “ababab” 等等。(cat|dog)s
可以匹配 “cats” 或 “dogs”。
[]
(方括号): 字符集。匹配方括号中列出的任意一个字符。[abc]
:匹配 “a”, “b”, 或 “c”。[0-9]
:匹配任意一个数字 (等同于\d
)。[a-z]
:匹配任意一个小写字母。[A-Z]
:匹配任意一个大写字母。[a-zA-Z0-9]
:匹配任意一个字母或数字。[^abc]
:在方括号内,^
表示非,匹配除了 “a”, “b”, “c” 之外的任意一个字符。
\
(反斜杠): 转义字符。用于取消元字符的特殊含义,使其匹配自身。.
匹配任意字符,但\.
只匹配字面量点号.
。*
是量词,但\*
只匹配字面量星号*
。\\
匹配字面量反斜杠\
。
3. 常用的特殊序列 (Special Sequences)
为了方便,正则表达式提供了一些反斜杠加字符的组合,用于表示常见的字符集或位置。
\d
: 匹配任意一个数字 (等同于[0-9]
)。\D
: 匹配任意一个非数字字符 (等同于[^0-9]
)。\s
: 匹配任意一个空白字符 (包括空格、制表符\t
、换行符\n
、回车符\r
等)。\S
: 匹配任意一个非空白字符 (等同于[^\s]
)。\w
: 匹配任意一个词字符 (包括字母、数字和下划线_
,等同于[a-zA-Z0-9_]
)。\W
: 匹配任意一个非词字符 (等同于[^\w]
)。\b
: 匹配词边界。词边界是指一个词字符和非词字符之间的位置,或者字符串的开头/结尾与词字符之间的位置。例如,\bword\b
可以匹配 “word” 在 “hello word!” 或 “the word is” 中的出现,但不匹配 “wordy” 或 “inward” 中的 “word”。\B
: 匹配非词边界。
4. 贪婪与非贪婪匹配 (Greedy vs. Non-Greedy)
默认情况下,量词 (*
, +
, ?
, {m,n}
) 是贪婪的,它们会尽可能多地匹配字符。
- 例如,对字符串
"<a>b<c>"
使用模式<.*>
会匹配整个"<a>b<c>"
,因为.*
会一直匹配到最后一个>
。
在量词后面加上一个 ?
可以使其变为非贪婪的(或称为懒惰的),它会尽可能少地匹配字符。
- 例如,对字符串
"<a>b<c>"
使用模式<.*?>
会分别匹配"<a>"
和"ệc>"
。*?
匹配零次或多次,但尽可能少。
非贪婪量词包括:*?
, +?
, ??
, {m,n}?
。
第二部分:Python 的 re
模块
Python 通过 re
模块提供了与正则表达式交互的函数。使用这个模块通常包括导入它,然后调用其函数,传入正则表达式模式和要搜索的字符串。
python
import re
1. 生字符串 (Raw Strings): r”…”
在 Python 中处理正则表达式时,强烈建议使用生字符串(Raw Strings)。在普通字符串中,反斜杠 \
是转义字符,比如 \n
表示换行。但在正则表达式中,反斜杠 \
也有特殊含义(如 \d
),如果你想匹配一个字面量反斜杠,你需要写 \\
。这会导致反斜杠过多,使模式难以阅读。
生字符串以 r
开头(如 r"pattern"
),它会忽略字符串中反斜杠的特殊含义,将反斜杠视为普通字符(除非它后面跟着引号本身)。
- 普通字符串:要匹配
\d
这个序列,需要写"\\d"
。 - 生字符串:要匹配
\d
这个序列,只需要写r"\d"
。
使用生字符串可以避免混淆,让你的正则表达式模式更清晰。
2. re
模块的主要函数
-
re.search(pattern, string, flags=0)
:- 在
string
中搜索与pattern
匹配的第一个位置。 - 如果找到匹配,返回一个
Match
对象;否则,返回None
。 - 这是最常用的查找函数,因为它会在整个字符串中查找匹配。
“`python
import retext = “hello world, python regex is powerful!”
pattern = r”python”match = re.search(pattern, text)
if match:
print(“找到匹配项:”, match.group()) # 输出找到的匹配字符串
print(“匹配起始位置:”, match.start())
print(“匹配结束位置:”, match.end())
print(“匹配范围:”, match.span())
else:
print(“未找到匹配项”)查找数字
text2 = “There are 123 apples and 45 oranges.”
pattern2 = r”\d+” # 匹配一个或多个数字
match2 = re.search(pattern2, text2)
if match2:
print(“找到第一个数字:”, match2.group()) # 输出 “123”
“` - 在
-
re.match(pattern, string, flags=0)
:- 尝试从
string
的开头匹配pattern
。 - 如果字符串的开头与模式匹配,返回一个
Match
对象;否则,返回None
。 - 注意:与
search
的区别在于match
只检查字符串的起始位置。
“`python
import retext = “hello world”
pattern1 = r”hello”
pattern2 = r”world”match1 = re.match(pattern1, text)
match2 = re.match(pattern2, text)if match1:
print(“match1 匹配成功:”, match1.group()) # 输出 “match1 匹配成功: hello”
else:
print(“match1 匹配失败”)if match2:
print(“match2 匹配成功:”, match2.group())
else:
print(“match2 匹配失败”) # 输出 “match2 匹配失败” (因为 world 不在开头)对比 search
search2 = re.search(pattern2, text)
if search2:
print(“search2 匹配成功:”, search2.group()) # 输出 “search2 匹配成功: world”
“` - 尝试从
-
re.findall(pattern, string, flags=0)
:- 在
string
中找到与pattern
匹配的所有非重叠项。 - 返回一个列表。
- 如果模式中包含捕获组
()
,则返回一个包含所有捕获组内容的列表(如果只有一个组,是字符串列表;如果有多个组,是元组列表)。 - 如果模式中不包含捕获组,则返回一个包含所有完整匹配项的字符串列表。
“`python
import retext = “Emails: [email protected], [email protected], [email protected]”
匹配简单的邮箱格式
pattern = r”\w+@\w+.\w+”
emails = re.findall(pattern, text)
print(“找到的邮箱:”, emails) # 输出 [‘[email protected]’, ‘[email protected]’, ‘[email protected]’]模式包含捕获组
pattern_groups = r”(\w+)@(\w+).(\w+)”
email_parts = re.findall(pattern_groups, text)
print(“找到的邮箱 parts:”, email_parts)输出 [(‘test1’, ‘example’, ‘com’), (‘user’, ‘mail’, ‘org’), (‘info’, ‘website’, ‘net’)]
text2 = “Numbers: 10, 25, -5, 99″
pattern2 = r”\d+” # 匹配一个或多个数字 (无分组)
numbers = re.findall(pattern2, text2)
print(“找到的数字串:”, numbers) # 输出 [’10’, ’25’, ‘5’, ’99’] – 注意这里把 -5 分成了 ‘5’如果要匹配带符号的数字,模式需要调整,例如 r”-?\d+”
pattern2_signed = r”-?\d+”
numbers_signed = re.findall(pattern2_signed, text2)
print(“找到的带符号数字串:”, numbers_signed) # 输出 [’10’, ’25’, ‘-5′, ’99’]“`
- 在
-
re.finditer(pattern, string, flags=0)
:- 在
string
中找到与pattern
匹配的所有非重叠项。 - 返回一个迭代器,迭代器中的每个元素都是一个
Match
对象。 - 当处理大量匹配项,或者需要访问每个匹配项的详细信息(如位置或分组)时,
finditer
比findall
更高效,因为它不会一次性将所有结果加载到内存中。
“`python
import retext = “Emails: [email protected], [email protected], [email protected]”
pattern = r”(\w+)@(\w+).(\w+)”for match in re.finditer(pattern, text):
print(“找到一个邮箱:”)
print(” 完整匹配:”, match.group())
print(” 用户名:”, match.group(1)) # 第一个捕获组
print(” 域名:”, match.group(2)) # 第二个捕获组
print(” 后缀:”, match.group(3)) # 第三个捕获组
print(” 位置:”, match.span())
“` - 在
-
re.sub(pattern, repl, string, count=0, flags=0)
:- 在
string
中查找所有与pattern
匹配的子串,并将它们替换为repl
。 repl
可以是一个字符串,也可以是一个函数。count
指定最多替换多少次,0
表示替换所有匹配项(默认)。- 返回替换后的字符串。
- 在
repl
字符串中,\g<组号>
或\g<组名>
可以引用捕获组的内容(如\g<1>
引用第一个捕获组)。
“`python
import retext = “The quick brown fox jumps over the lazy fox.”
pattern = r”fox”
replacement = “dog”new_text = re.sub(pattern, replacement, text)
print(“替换后的文本:”, new_text) # 输出 “The quick brown dog jumps over the lazy dog.”只替换第一个
new_text_one = re.sub(pattern, replacement, text, count=1)
print(“替换第一个:”, new_text_one) # 输出 “The quick brown dog jumps over the lazy fox.”使用捕获组进行替换 (例如,交换姓和名)
name = “Doe, John”
pattern_name = r”(\w+), (\w+)” # 捕获姓和名
replacement_name = r”\g<2> \g<1>” # 使用 \g<组号> 引用捕获组,第二个组(名)在前,第一个组(姓)在后formatted_name = re.sub(pattern_name, replacement_name, name)
print(“格式化姓名:”, formatted_name) # 输出 “John Doe”repl 可以是一个函数
def double_number(match):
number = int(match.group(0)) # 获取匹配到的数字字符串并转为整数
return str(number * 2) # 返回翻倍后的字符串text_numbers = “Numbers: 10 and 25.”
pattern_numbers = r”\d+”doubled_text = re.sub(pattern_numbers, double_number, text_numbers)
print(“数字翻倍后的文本:”, doubled_text) # 输出 “Numbers: 20 and 50.”
“` - 在
-
re.split(pattern, string, maxsplit=0, flags=0)
:- 根据与
pattern
匹配的子串来分割string
。 - 返回一个字符串列表。
maxsplit
指定最多进行多少次分割,0
表示不限制。- 注意: 如果模式包含捕获组
()
,则捕获组匹配的内容也会包含在结果列表中。
“`python
import retext = “apple,banana;orange grape”
使用逗号、分号或空格作为分隔符
pattern = r”[,; ]”
fruits = re.split(pattern, text)
print(“分割后的水果列表:”, fruits) # 输出 [‘apple’, ‘banana’, ‘orange’, ‘grape’]maxsplit 限制分割次数
text2 = “a:b:c:d”
pattern2 = r”:”
parts = re.split(pattern2, text2, maxsplit=2)
print(“限制分割次数:”, parts) # 输出 [‘a’, ‘b’, ‘c:d’]模式包含捕获组
text3 = “hello(world)python”
pattern3 = r”([()])” # 匹配并捕获括号
parts_with_groups = re.split(pattern3, text3)
print(“包含捕获组的分割:”, parts_with_groups) # 输出 [‘hello’, ‘(‘, ‘world’, ‘)’, ‘python’]
“` - 根据与
-
re.compile(pattern, flags=0)
:- 将正则表达式模式编译成一个正则表达式对象。
- 编译后的对象具有与
re
模块函数同名的方法(如search()
,findall()
,sub()
等)。 - 优点:
- 效率更高: 如果在同一个程序中多次使用同一个正则表达式模式,编译可以避免重复解析模式,提高效率。
- 代码更清晰: 可以将模式存储在一个变量中,使代码更易读。
“`python
import re编译模式
email_pattern_compiled = re.compile(r”(\w+)@(\w+).(\w+)”)
text1 = “User email is [email protected]”
text2 = “Admin email is [email protected]”使用编译后的对象的方法
match1 = email_pattern_compiled.search(text1)
if match1:
print(“From text1:”, match1.group())match2 = email_pattern_compiled.search(text2)
if match2:
print(“From text2:”, match2.group())对比 findall 方法
text_all = “Emails: [email protected], [email protected], [email protected]”
all_emails = email_pattern_compiled.findall(text_all)
print(“所有邮箱:”, all_emails)
“`
3. Match 对象的方法
当 re.search()
或 re.match()
找到匹配时,它们返回一个 Match
对象。这个对象包含了匹配的详细信息。
match.group(0)
或match.group()
: 返回整个匹配到的字符串。match.group(n)
: 返回第 n 个捕获组(由圆括号定义的组)匹配到的字符串。组号从 1 开始计数。match.groups()
: 返回一个元组,包含所有捕获组匹配到的字符串。match.start(n=0)
: 返回第 n 个组匹配到的子串在原字符串中的起始索引。n=0
表示整个匹配项。match.end(n=0)
: 返回第 n 个组匹配到的子串在原字符串中的结束索引(不包含)。n=0
表示整个匹配项。match.span(n=0)
: 返回一个元组(start, end)
,表示第 n 个组匹配到的子串在原字符串中的起始和结束索引。n=0
表示整个匹配项。
“`python
import re
text = “My number is 123-456-7890.”
模式捕获区号、前缀和行号
pattern = r”(\d{3})-(\d{3})-(\d{4})”
match = re.search(pattern, text)
if match:
print(“完整匹配:”, match.group(0)) # 或 match.group() -> 123-456-7890
print(“区号:”, match.group(1)) # -> 123
print(“前缀:”, match.group(2)) # -> 456
print(“行号:”, match.group(3)) # -> 7890
print(“所有分组:”, match.groups()) # -> (‘123’, ‘456’, ‘7890’)
print("完整匹配位置:", match.span(0)) # 或 match.span() -> (13, 25)
print("区号位置:", match.span(1)) # -> (13, 16)
print("完整匹配起始索引:", match.start(0)) # 或 match.start() -> 13
print("完整匹配结束索引:", match.end(0)) # 或 match.end() -> 25
“`
4. 正则表达式标志 (Flags)
re
模块的函数和编译方法都接受一个可选的 flags
参数,用于修改匹配的行为。常用的标志包括:
re.IGNORECASE
或re.I
: 进行忽略大小写的匹配。re.search(r"python", "Python", re.I)
会匹配成功。
re.MULTILINE
或re.M
: 使^
和$
匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。在多行文本中查找模式时非常有用。- 没有
re.M
时,^abc$
只匹配 “abc” (整个字符串只有abc)。 - 使用
re.M
时,对于字符串"abc\ndef\nghi"
,^def$
会匹配成功。
- 没有
re.DOTALL
或re.S
: 使点号.
匹配包括换行符\n
在内的所有字符。默认情况下,.
不匹配换行符。re.search(r"a.b", "a\nb")
不会匹配成功(默认)。re.search(r"a.b", "a\nb", re.S)
会匹配成功。
-
re.VERBOSE
或re.X
: 允许你在正则表达式中添加空格和注释,提高模式的可读性。在写复杂模式时非常有用。“`python
import re不使用 VERBOSE
pattern1 = r”^(\d{3})-(\d{3})-(\d{4})$”
使用 VERBOSE,添加空格和注释
pattern2 = r”””
^ # 匹配字符串开头
(\d{3}) # 捕获区号 (三个数字)
– # 匹配连字符
(\d{3}) # 捕获前缀 (三个数字)
– # 匹配连字符
(\d{4}) # 捕获行号 (四个数字)
$ # 匹配字符串结尾
“””phone_number = “123-456-7890”
match1 = re.match(pattern1, phone_number)
match2 = re.match(pattern2, phone_number, re.VERBOSE)print(“使用普通模式:”, match1.group() if match1 else “不匹配”)
print(“使用 VERBOSE 模式:”, match2.group() if match2 else “不匹配”)忽略大小写
match_case = re.search(r”apple”, “Apple”, re.IGNORECASE)
print(“忽略大小写匹配:”, match_case.group() if match_case else “不匹配”)多行匹配
text_multiline = “Line 1\nLine 2\nLine 3”
匹配以 Line 2 结尾的行
match_multiline = re.search(r”Line 2$”, text_multiline, re.MULTILINE)
print(“多行匹配:”, match_multiline.group() if match_multiline else “不匹配”)点号匹配换行符
text_dotall = “a\nb”
match_dotall_no_s = re.search(r”a.b”, text_dotall)
match_dotall_s = re.search(r”a.b”, text_dotall, re.DOTALL)
print(“点号不匹配换行:”, match_dotall_no_s) # None
print(“点号匹配换行:”, match_dotall_s.group() if match_dotall_s else “不匹配”) # a\nb
“`
可以将多个标志用按位 OR |
组合起来使用,例如 re.I | re.M
。
第三部分:进阶技巧与最佳实践
掌握了基础概念和函数后,一些进阶技巧和最佳实践能让你更高效地使用正则表达式。
1. 非捕获组 (Non-capturing Groups): (?:...)
圆括号 ()
默认既用于分组,也用于捕获。如果你只需要分组而不需要捕获(即不想在 match.groups()
或 re.findall()
的结果中包含这部分匹配),可以使用非捕获组 (?:...)
。这有时能稍微提高性能,并且在使用 findall
时可以改变返回列表的结构。
“`python
import re
text = “cat dog mouse”
捕获组 (cat|dog) 会被 findall 捕获
pattern1 = r”(cat|dog)\s+\w+”
result1 = re.findall(pattern1, text)
print(“捕获组 findall:”, result1) # 输出 [‘cat’, ‘dog’] – 只返回括号内的内容
非捕获组 (?:cat|dog) 只用于分组,不捕获
pattern2 = r”(?:cat|dog)\s+\w+”
result2 = re.findall(pattern2, text)
print(“非捕获组 findall:”, result2) # 输出 [‘cat dog’, ‘dog mouse’] – 返回整个匹配项
“`
2. 命名捕获组 (Named Capture Groups): (?P<name>...)
使用数字索引获取捕获组内容(如 match.group(1)
) 在模式复杂时容易出错。命名捕获组允许你为捕获组指定一个名字,然后通过名字来获取内容,这大大提高了可读性。
“`python
import re
text = “Date: 2023-10-26”
命名捕获年、月、日
pattern = r”(?P
match = re.search(pattern, text)
if match:
print(“年:”, match.group(“year”)) # 通过名字访问
print(“月:”, match.group(“month”)) # 通过名字访问
print(“日:”, match.group(“day”)) # 通过名字访问
print(“所有命名分组:”, match.groupdict()) # 返回一个字典 {name: value}
“`
在 re.sub()
的替换字符串中,可以使用 \g<name>
来引用命名捕获组。
“`python
import re
name = “Doe, John”
pattern_name = r”(?P
replacement_name = r”\g
formatted_name = re.sub(pattern_name, replacement_name, name)
print(“使用命名分组替换:”, formatted_name) # 输出 “John Doe”
“`
3. 测试你的正则表达式
写复杂的正则表达式时,很容易出错。使用在线的正则表达式测试工具(如 regex101.com, regexr.com)可以帮助你实时构建和测试模式,查看每个部分的匹配效果,以及解释模式的含义。这比在 Python 中反复运行代码效率高得多。
4. 逐步构建复杂模式
不要试图一次性写出一个很长的复杂模式。从简单的部分开始,逐步添加元字符、量词和分组,并在测试工具或代码中验证每一步的效果。
5. 性能考虑
- 如前所述,对于需要多次使用的模式,使用
re.compile()
可以提高效率。 - 避免过度使用复杂的先行断言/后行断言(lookarounds)如果可以用更简单的方式实现。
- 小心使用
.*
或.+
这样的贪婪模式,它们可能导致“回溯失控”(catastrophic backtracking),使得匹配过程变得非常慢,尤其是在处理长字符串时。在可能的情况下,使用非贪婪模式.*?
或.+?
,或者更精确地指定允许匹配的字符集(如[^>]*
匹配除了>
之外的任意字符零次或多次)。
6. 清晰度和可维护性
- 为你的模式变量选择有意义的名字。
- 对于特别复杂的模式,考虑使用
re.VERBOSE
标志来添加注释和格式。 - 在代码中添加注释,解释模式的用途和关键部分。
第四部分:实践案例(综合应用)
让我们通过一个稍微复杂一些的例子来综合应用学到的知识。假设我们要从一段文本中提取所有符合 YYYY-MM-DD
或 MM/DD/YYYY
格式的日期。
“`python
import re
text = “””
Meeting scheduled for 2023-10-26.
The project started on 04/15/2022.
Deadline is 2024-01-31.
Another date: 12/01/2023.
Invalid format: 2023/10/26 or 10-26-2023.
“””
构建模式:
格式1: YYYY-MM-DD
\d{4} 匹配年份 (4个数字)
– 匹配连字符
\d{2} 匹配月份 (2个数字)
– 匹配连字符
\d{2} 匹配日期 (2个数字)
格式2: MM/DD/YYYY
\d{2} 匹配月份 (2个数字)
/ 匹配斜杠
\d{2} 匹配日期 (2个数字)
/ 匹配斜杠
\d{4} 匹配年份 (4个数字)
使用 | 连接两种格式,使用非捕获组 (?:…) 将它们组合
date_pattern = re.compile(r”(?:\d{4}-\d{2}-\d{2})|(?:\d{2}/\d{2}/\d{4})”)
查找所有匹配项
found_dates = date_pattern.findall(text)
print(“找到的日期:”, found_dates)
输出: 找到的日期: [‘2023-10-26′, ’04/15/2022’, ‘2024-01-31′, ’12/01/2023’]
如果我们需要提取年、月、日,可以使用捕获组并使用 finditer 或 search
date_pattern_groups = re.compile(r”(\d{4})-(\d{2})-(\d{2})|(\d{2})/(\d{2})/(\d{4})”)
print(“\n按部分提取日期:”)
for match in date_pattern_groups.finditer(text):
# match.groups() 会返回一个包含所有捕获组的元组,对于这个模式,
# 如果匹配的是 YYYY-MM-DD,则前三个组有值,后三个组为 None
# 如果匹配的是 MM/DD/YYYY,则前三个组为 None,后三个组有值
year, month, day, month2, day2, year2 = match.groups()
if year: # 匹配 YYYY-MM-DD 格式
print(f" 格式 YYYY-MM-DD: 年={year}, 月={month}, 日={day}")
elif year2: # 匹配 MM/DD/YYYY 格式
print(f" 格式 MM/DD/YYYY: 年={year2}, 月={month2}, 日={day2}")
使用 sub 将日期格式统一替换为 YYYY/MM/DD
我们需要处理两种格式的匹配
def format_date(match):
year1, month1, day1, month2, day2, year2 = match.groups()
if year1: # 匹配 YYYY-MM-DD
return f”{year1}/{month1}/{day1}”
elif year2: # 匹配 MM/DD/YYYY
return f”{year2}/{month2}/{day2}”
return match.group(0) # 如果出于某种原因没有匹配到组 (尽管在这里应该不会发生)
注意这里 sub 的 pattern 和 finditer 使用的 pattern 是一样的 (有捕获组)
formatted_text = re.sub(date_pattern_groups, format_date, text)
print(“\n格式化后的文本:”)
print(formatted_text)
输出:
Meeting scheduled for 2023/10/26.
The project started on 2022/04/15.
Deadline is 2024/01/31.
Another date: 2023/12/01.
Invalid format: 2023/10/26 or 10-26-2023.
注意 invalid format 的第一个日期被替换了,因为它匹配了 YYYY-MM-DD 的一部分 \d{4}/\d{2}/\d{2} -> 2023/10/26
为了更严格的匹配,需要在模式中添加词边界 \b
date_pattern_strict = re.compile(r”\b(?P
def format_date_strict(match):
if match.group(‘y1’): # 匹配 YYYY-MM-DD
return f”{match.group(‘y1’)}/{match.group(‘m1’)}/{match.group(‘d1’)}”
elif match.group(‘y2’): # 匹配 MM/DD/YYYY
return f”{match.group(‘y2’)}/{match.group(‘m2’)}/{match.group(‘d2’)}”
return match.group(0)
formatted_text_strict = re.sub(date_pattern_strict, format_date_strict, text)
print(“\n使用严格模式和命名分组格式化后的文本:”)
print(formatted_text_strict)
输出: (这次 Invalid format 的日期不会被替换)
Meeting scheduled for 2023/10/26.
The project started on 2022/04/15.
Deadline is 2024/01/31.
Another date: 2023/12/01.
Invalid format: 2023/10/26 or 10-26-2023.
“`
这个例子展示了如何结合使用不同的元字符、量词、分组、re.compile
、re.finditer
、re.sub
以及命名捕获组来解决实际问题。通过逐步构建模式和利用 re
模块提供的强大功能,你可以高效地处理各种复杂的文本匹配和操作任务。
总结
正则表达式是文本处理的瑞士军刀,而 Python 的 re
模块提供了这把刀的强大接口。掌握正则表达式需要时间和练习,但投入的努力绝对物有所值。
本文从正则表达式的基础概念(普通字符、元字符、特殊序列)讲起,强调了生字符串的重要性,详细介绍了 Python re
模块中的核心函数(search
, match
, findall
, finditer
, sub
, split
, compile
),讲解了 Match
对象的使用,以及常用的标志和一些进阶技巧(非捕获组、命名组)和最佳实践。最后通过一个日期处理的综合案例展示了如何将这些知识融会贯通。
记住,学习正则表达式最好的方法就是动手实践。从简单的模式开始,不断尝试解决不同的文本处理问题,并利用在线工具辅助学习和调试。随着你对各种元字符和规则越来越熟悉,你会发现正则表达式不再神秘,而是成为了你处理文本数据的强大而灵活的工具。
希望本文能帮助你轻松踏入 Python 正则表达式的世界,并祝你在文本处理的道路上越走越顺!