Python 正则表达式提取指南:从入门到精通
正则表达式(Regular Expression,简称 Regex 或 RE)是一种强大而灵活的文本处理工具,它用简洁的字符序列描述了一种字符串匹配模式。在 Python 中,标准库 re
模块提供了对正则表达式的完整支持。虽然正则表达式可以用于匹配、查找、替换、分割等多种文本操作,但其在数据提取方面的能力尤为突出。
本文将深入探讨如何使用 Python 的 re
模块进行高效、精确的数据提取,从基础概念讲起,逐步深入到高级技巧和实战应用。无论你是初学者还是希望提升技能的开发者,本文都能为你提供一份详尽的指南。
第一章:初识正则表达式与 Python 的 re
模块
1.1 什么是正则表达式?
简单来说,正则表达式就是用来描述文本模式的“代码”。你可以用一个正则表达式来定义你想要查找、匹配或提取的字符串特征。例如:
\d+
:匹配一个或多个数字。[a-zA-Z]+
:匹配一个或多个英文字母。\w+@\w+\.\w+
:一个非常简化的电子邮件地址模式。
正则表达式的强大之处在于,它允许你组合这些基本模式,创建复杂的匹配规则,从而准确地定位和处理目标文本。
1.2 Python 的 re
模块
Python 通过内置的 re
模块提供了正则表达式功能。使用它之前,你需要先导入:
python
import re
re
模块提供了多个函数,用于执行不同的正则表达式操作。对于数据提取,最常用的函数包括:
re.search()
: 在整个字符串中查找第一个匹配的模式。re.match()
: 仅在字符串的开头查找匹配的模式。re.findall()
: 查找字符串中所有不重叠的匹配模式。re.finditer()
: 查找字符串中所有不重叠的匹配模式,并返回一个迭代器,生成匹配对象(Match object)。re.compile()
: 编译正则表达式模式,提高重复使用时的效率。
本文的重点是提取,而提取的前提是匹配。因此,我们将先从匹配函数开始,然后深入讲解如何从匹配结果中提取数据。
1.3 原始字符串(Raw Strings)的重要性
在 Python 中,反斜杠 \
是一个转义字符。正则表达式中也大量使用反斜杠(如 \d
, \w
, \\
表示字面上的反斜杠)。这会导致一个问题:正则表达式中的 \
可能会与 Python 字符串本身的转义规则冲突。
例如,正则表达式 \n
匹配一个换行符,但 Python 字符串 "\\n"
才能表示字面上的反斜杠和字母 n。如果写成 "\n"
,Python 会将其解释为一个换行符。
为了避免这种混淆,强烈建议在定义正则表达式模式时使用原始字符串(raw strings),前缀为 r
:
“`python
普通字符串,\n会被解释为换行符
pattern_normal = “\n” # 如果想匹配字面上的 \n,需要这样写
原始字符串,\n会被解释为字面上的反斜杠和n
pattern_raw = r”\n” # 直接写即可匹配字面上的 \n
示例:匹配一个反斜杠后面跟着一个数字
regex_pattern = r”\\d” # 在原始字符串中,\ 就是匹配字面上的 \
“`
使用原始字符串 r"..."
可以让你按照正则表达式的语法直接书写模式,而无需担心 Python 的转义规则,这极大地提高了正则表达式的可读性和编写效率,尤其是在处理大量反斜杠的模式时。
第二章:匹配模式 – 提取数据的基础
在提取数据之前,首先需要使用正则表达式找到目标文本。re.search()
和 re.match()
是用于查找单个匹配项的函数。
2.1 re.search(pattern, string, flags=0)
re.search()
函数扫描整个 string
,查找第一个匹配 pattern
的位置。如果找到,返回一个 Match
对象;如果没有找到,返回 None
。
“`python
text = “Today is 2023-10-27.”
pattern = r”\d{4}-\d{2}-\d{2}” # 匹配 YYYY-MM-DD 格式的日期
match = re.search(pattern, text)
if match:
print(“找到匹配项:”, match.group()) # 使用 match.group() 获取整个匹配的字符串
else:
print(“未找到匹配项”)
“`
输出:
找到匹配项: 2023-10-27
这里的 match.group()
提取了整个匹配到的字符串。这是最基本的提取方式。
2.2 re.match(pattern, string, flags=0)
re.match()
函数只尝试从 string
的开头匹配 pattern
。如果开头匹配成功,返回一个 Match
对象;否则,返回 None
。
“`python
text1 = “Hello World!”
text2 = “The time is 10:30.”
pattern = r”Hello”
match1 = re.match(pattern, text1)
match2 = re.match(pattern, text2)
if match1:
print(“text1 从开头匹配成功:”, match1.group())
else:
print(“text1 未从开头匹配成功”)
if match2:
print(“text2 从开头匹配成功:”, match2.group())
else:
print(“text2 未从开头匹配成功”)
“`
输出:
text1 从开头匹配成功: Hello
text2 未从开头匹配成功
由于 re.match()
只检查字符串开头,它在提取数据时的应用场景相对较少,除非你确定你要提取的内容总是在字符串的起始位置。在大多数需要扫描整个字符串并提取数据的场景中,re.search()
或后面的 re.findall()
/re.finditer()
更常用。
2.3 Match
对象
当 re.search()
或 re.match()
找到匹配项时,它们返回一个 Match
对象。这个对象包含了匹配的详细信息,是进行数据提取的关键。Match
对象的核心方法包括:
match.group(0)
或match.group()
: 返回整个匹配到的字符串。match.start()
: 返回匹配到的子串在原字符串中的起始索引。match.end()
: 返回匹配到的子串在原字符串中的结束索引(不包含该位置)。match.span()
: 返回一个元组(start, end)
,表示匹配到的子串的起始和结束索引。
“`python
text = “The date is 2023-10-27.”
pattern = r”\d{4}-\d{2}-\d{2}”
match = re.search(pattern, text)
if match:
print(“匹配的整个字符串:”, match.group())
print(“起始索引:”, match.start())
print(“结束索引:”, match.end())
print(“索引范围 (start, end):”, match.span())
print(“从原字符串中切片验证:”, text[match.start():match.end()])
“`
输出:
匹配的整个字符串: 2023-10-27
起始索引: 13
结束索引: 23
索引范围 (start, end): (13, 23)
从原字符串中切片验证: 2023-10-27
理解 Match
对象及其基本方法是进一步学习捕获组提取数据的基础。
第三章:捕获组 – 精准提取特定部分
仅仅匹配整个模式通常不够,我们往往需要从匹配的字符串中提取出特定的子部分。正则表达式中的捕获组(Capturing Groups)就是为此而生的。
3.1 使用括号 ()
创建捕获组
在正则表达式中,使用一对圆括号 ()
括起来的部分就构成了一个捕获组。每个捕获组会捕获它所匹配到的文本。
“`python
text = “My email is [email protected] and phone is 13812345678.”
尝试提取用户名和域名
pattern = r”(\w+)@(\w+.\w+)”
match = re.search(pattern, text)
if match:
# match.group(0) 或 match.group() 是整个匹配项
print(“整个匹配项:”, match.group(0))
# match.group(1) 是第一个捕获组的内容
print(“用户名:”, match.group(1))
# match.group(2) 是第二个捕获组的内容
print(“域名:”, match.group(2))
# match.groups() 返回一个包含所有捕获组内容的元组
print(“所有捕获组:”, match.groups())
else:
print(“未找到匹配项”)
“`
输出:
整个匹配项: [email protected]
用户名: test
域名: example.com
所有捕获组: ('test', 'example.com')
在这个例子中:
* (\w+)
是第一个捕获组,匹配并捕获 @
符号前的连续单词字符(用户名)。
* (\w+\.\w+)
是第二个捕获组,匹配并捕获 @
符号后的域名部分。
通过 match.group(index)
,我们可以按索引访问每个捕获组捕获的内容,索引从 1 开始。match.groups()
则方便地一次性获取所有捕获组的内容。
3.2 多个捕获组的索引
捕获组的索引是按照它们在正则表达式中左括号 (
出现的顺序从 1 开始编号的。
“`python
text = “Date: 2023-10-27 Time: 15:30:00”
提取年、月、日、小时、分钟、秒
pattern = r”Date: (\d{4})-(\d{2})-(\d{2}) Time: (\d{2}):(\d{2}):(\d{2})”
match = re.search(pattern, text)
if match:
print(“整个匹配:”, match.group(0))
print(“年:”, match.group(1))
print(“月:”, match.group(2))
print(“日:”, match.group(3))
print(“小时:”, match.group(4))
print(“分钟:”, match.group(5))
print(“秒:”, match.group(6))
print(“所有捕获组:”, match.groups())
“`
输出:
整个匹配: Date: 2023-10-27 Time: 15:30:00
年: 2023
月: 10
日: 27
小时: 15
分钟: 30
秒: 00
所有捕获组: ('2023', '10', '27', '15', '30', '00')
3.3 非捕获组 (?:...)
有时候我们需要使用括号来组织正则表达式的一部分(例如,使用 |
进行分组),但又不希望这个分组成为一个捕获组。这时可以使用非捕获组 (?:...)
。非捕获组不会为匹配结果创建额外的捕获项,也不会影响捕获组的索引编号。
“`python
text = “Color can be red or blue.”
匹配 red 或 blue,但不捕获整个分组
pattern_non_capturing = r”Color can be (?:red|blue)”
match_nc = re.search(pattern_non_capturing, text)
if match_nc:
print(“使用非捕获组:”, match_nc.group(0))
# print(match_nc.group(1)) # 尝试访问group(1)会报错,因为它不是捕获组
print(“捕获组数量 (使用groups()):”, len(match_nc.groups())) # 结果是0
else:
print(“未找到匹配”)
print(“-” * 20)
同样的模式使用捕获组
pattern_capturing = r”Color can be (red|blue)”
match_c = re.search(pattern_capturing, text)
if match_c:
print(“使用捕获组:”, match_c.group(0)) # 整个匹配
print(“第一个捕获组内容:”, match_c.group(1)) # 捕获了 red 或 blue
print(“捕获组数量 (使用groups()):”, len(match_c.groups())) # 结果是1
else:
print(“未找到匹配”)
“`
输出:
“`
使用非捕获组: Color can be red
捕获组数量 (使用groups()): 0
使用捕获组: Color can be red
第一个捕获组内容: red
捕获组数量 (使用groups()): 1
“`
非捕获组对于优化性能(虽然影响通常很小)和避免不必要的捕获项非常有用,尤其是在复杂的模式中。
第四章:核心提取函数:findall
与 finditer
re.search()
和 re.match()
只能找到并提取第一个匹配项。如果文本中存在多个需要提取的数据实例,我们就需要使用 re.findall()
或 re.finditer()
。
4.1 re.findall(pattern, string, flags=0)
re.findall()
查找 string
中所有与 pattern
匹配的非重叠子串,并将它们作为一个列表返回。它的返回值类型取决于 pattern
中是否包含捕获组:
- 模式中没有捕获组: 返回一个包含所有完整匹配字符串的列表。
- 模式中有一个捕获组: 返回一个包含该捕获组捕获内容的列表。
- 模式中有多个捕获组: 返回一个列表,列表的每个元素是一个元组,元组中包含了每个捕获组在一个匹配项中捕获的内容。
理解这个返回类型的变化对于正确使用 findall
进行多项提取至关重要。
“`python
text = “Emails: [email protected], [email protected], [email protected]”
pattern_no_groups = r”\w+@\w+.\w+” # 没有捕获组
pattern_one_group = r”(\w+@\w+.\w+)” # 一个捕获组 (整个邮箱)
pattern_two_groups = r”(\w+)@(\w+.\w+)” # 两个捕获组 (用户名和域名)
没有捕获组
matches_no_groups = re.findall(pattern_no_groups, text)
print(“没有捕获组:”, matches_no_groups)
一个捕获组
matches_one_group = re.findall(pattern_one_group, text)
print(“一个捕获组:”, matches_one_group) # 注意:返回的是捕获组的内容,而不是整个匹配项
两个捕获组
matches_two_groups = re.findall(pattern_two_groups, text)
print(“两个捕获组:”, matches_two_groups) # 返回元组列表
“`
输出:
没有捕获组: ['[email protected]', '[email protected]', '[email protected]']
一个捕获组: ['[email protected]', '[email protected]', '[email protected]']
两个捕获组: [('test1', 'example.com'), ('user2', 'sample.org'), ('admin', 'website.net')]
从上面的例子可以看出:
* 当模式没有捕获组时,findall
返回的是整个匹配到的字符串列表。
* 当模式有一个捕获组时,即使你可能想要整个匹配项,findall
也只返回那个捕获组的内容列表。如果你确实需要整个匹配项,可以考虑使用非捕获组 (?:...)
包围整个模式,或者使用 finditer
。
* 当模式有多个捕获组时,findall
返回一个元组列表,每个元组对应一个匹配项,元组内的元素是该匹配项中各个捕获组的内容。这是从多个匹配项中同时提取多个字段的标准方式。
4.2 re.finditer(pattern, string, flags=0)
re.finditer()
也查找字符串中所有非重叠的匹配项,但它返回的是一个迭代器,生成的是 Match
对象,而不是直接的字符串或元组列表。
“`python
text = “Emails: [email protected], [email protected], [email protected]”
pattern = r”(\w+)@(\w+.\w+)”
matches_iterator = re.finditer(pattern, text)
print(“使用 finditer 迭代提取:”)
for match in matches_iterator:
print(” 整个匹配:”, match.group(0))
print(” 用户名:”, match.group(1))
print(” 域名:”, match.group(2))
print(” 起始索引:”, match.start())
print(” 结束索引:”, match.end())
print(“-” * 10)
“`
输出:
“`
使用 finditer 迭代提取:
整个匹配: [email protected]
用户名: test1
域名: example.com
起始索引: 8
结束索引: 27
整个匹配: [email protected]
用户名: user2
域名: sample.org
起始索引: 29
结束索引: 48
整个匹配: [email protected]
用户名: admin
域名: website.net
起始索引: 50
结束索引: 69
“`
re.finditer()
的优势在于:
- 内存效率: 对于非常大的文本,
finditer
以迭代器的方式处理,比findall
一次性构建整个列表更节省内存。 - 获取更多信息: 每个迭代项是
Match
对象,你可以方便地获取匹配的起始/结束位置 (start()
,end()
,span()
) 以及使用各种group()
方法(包括后面要讲到的命名组)。 - 统一处理: 无论模式中是否有捕获组,你总是通过
match.group(0)
获取整个匹配,通过match.group(i)
获取捕获组内容。
因此,当你需要处理大量文本、或者需要获取匹配位置信息、或者喜欢处理 Match
对象时,finditer
是一个更好的选择。
第五章:命名捕获组 – 提升可读性
当正则表达式中有多个捕获组时,使用索引 match.group(1)
, match.group(2)
来访问捕获的内容会使得代码难以阅读和维护。命名捕获组解决了这个问题。
5.1 使用 (?P<name>...)
创建命名捕获组
命名捕获组的语法是 (?P<name>...)
,其中 <name>
是你为这个捕获组指定的名字。
“`python
text = “Date: 2023-10-27 Time: 15:30:00”
使用命名捕获组提取年、月、日、小时、分钟、秒
pattern_named = r”Date: (?P
match = re.search(pattern_named, text)
if match:
print(“整个匹配:”, match.group(0))
# 使用名字访问捕获组
print(“年:”, match.group(‘year’))
print(“月:”, match.group(‘month’))
print(“日:”, match.group(‘day’))
print(“小时:”, match.group(‘hour’))
print(“分钟:”, match.group(‘minute’))
print(“秒:”, match.group(‘second’))
# 使用 groupdict() 获取所有命名捕获组的字典
print("所有命名捕获组:", match.groupdict())
# 命名捕获组也可以通过索引访问,但通常不推荐混用
print("年 (通过索引):", match.group(1))
else:
print(“未找到匹配项”)
“`
输出:
整个匹配: Date: 2023-10-27 Time: 15:30:00
年: 2023
月: 10
日: 27
小时: 15
分钟: 30
秒: 00
所有命名捕获组: {'year': '2023', 'month': '10', 'day': '27', 'hour': '15', 'minute': '30', 'second': '00'}
年 (通过索引): 2023
5.2 match.groupdict()
match.groupdict()
方法返回一个字典,其中键是命名捕获组的名字,值是对应的捕获内容。这在需要以更结构化的方式访问捕获数据时非常方便。
5.3 findall
与命名捕获组
当模式中包含命名捕获组时,re.findall()
的行为与包含多个普通捕获组时相同:它返回一个元组列表。每个元组的元素顺序是捕获组在模式中出现的顺序(包括命名组和非命名组)。findall
不会返回字典。
“`python
text = “User [email protected], Admin [email protected]”
pattern_named = r”(\w+) user (?P
matches = re.findall(pattern_named, text)
print(matches)
“`
输出:
[('User', 'test1', 'example.com'), ('Admin', 'user2', 'sample.org')]
可以看到,返回的是一个元组列表,元组中包含的是普通捕获组 (\w+)
的内容、命名捕获组 <name>
的内容、命名捕获组 <domain>
的内容,顺序按照它们在模式中从左到右出现的顺序。
如果你需要结合 findall
的能力(找到所有匹配项)和命名捕获组的便利性(按名字访问数据),最好的方法是使用 re.finditer()
并遍历 Match
对象,然后对每个 Match
对象调用 groupdict()
。
“`python
text = “User [email protected], Admin [email protected]”
pattern_named = r”(\w+) user (?P
results = []
for match in re.finditer(pattern_named, text):
# 可以同时获取普通捕获组和命名捕获组
role = match.group(1)
data = match.groupdict()
data[‘role’] = role # 将普通捕获组内容也加入字典
results.append(data)
# 或者只获取命名捕获组
# results.append(match.groupdict())
print(results)
“`
输出(取决于如何构建字典,这里加入了role):
[{'name': 'test1', 'domain': 'example.com', 'role': 'User'}, {'name': 'user2', 'domain': 'sample.org', 'role': 'Admin'}]
这是一种更灵活且可读性更高的多项数据提取方式。
第六章:提高提取精度和灵活性的高级技巧
仅仅使用基本的字符匹配和捕获组有时不足以应对复杂的提取需求。理解一些高级的正则表达式特性可以帮助你编写更精确和健壮的提取模式。
6.1 量词的贪婪与非贪婪模式
量词(如 *
, +
, ?
, {m,n}
)默认是贪婪的(greedy),它们会尽可能多地匹配字符。这在提取数据时可能会导致问题,比如你想提取最短的匹配而不是最长的。
例如,从 <b>Bold text</b><i>Italic text</i>
中提取 <b>...</b>
部分。如果使用模式 <b>.*</b>
:
“`python
text = “Bold textItalic text”
pattern_greedy = r”.” # . 是贪婪的
match = re.search(pattern_greedy, text)
print(match.group(0))
“`
输出:
<b>Bold text</b><i>Italic text</i>
.*
匹配了从第一个 <b>
到最后一个 </b>
之间的所有字符,包括中间的 <i>...</i>
。
要使其变为非贪婪的(non-greedy)或惰性的(lazy),只需在量词后面加上一个 ?
:*?
, +?
, ??
, {m,n}?
。
“`python
text = “Bold textItalic text”
pattern_non_greedy = r”.?” # .? 是非贪婪的
match1 = re.search(pattern_non_greedy, text)
print(match1.group(0))
尝试提取所有匹配项
matches_all = re.findall(pattern_non_greedy, text)
print(matches_all)
“`
输出:
<b>Bold text</b>
['<b>Bold text</b>', '<i>Italic text</i>'] # 注意,.*? 配合 findall 可以用于提取多个非重叠的标签对
现在 .*?
只匹配了第一个 </b>
之前尽可能少的字符,成功提取了单个 <b>...</b>
标签对。非贪婪量词在提取由特定分隔符(如引号、标签)包围的内容时非常有用。
6.2 零宽断言(Lookarounds)
零宽断言是一种特殊的结构,它们匹配一个位置,而不是实际的字符。它们用于基于上下文来确定匹配位置,但这些上下文本身不包含在匹配结果中,因此它们是“零宽”的。这对于提取位于特定文本之前或之后的数据非常有用,而又不捕获这些前后的文本。
四种零宽断言:
- 肯定先行断言
(?=...)
:匹配后面跟着...
的位置。 - 否定先行断言
(?!...)
:匹配后面没有跟着...
的位置。 - 肯定后行断言
(?<=...)
:匹配前面是...
的位置。 - 否定后行断言
(?<!...)
:匹配前面不是...
的位置。
示例:提取金额数字,但不包含货币符号。
“`python
text = “Prices: $100, €50, ¥200, 75USD”
提取前面是 $ 或 € 的数字
pattern_lookbehind = r”(?<=[$€])\d+”
matches = re.findall(pattern_lookbehind, text)
print(“提取 $ 或 € 后面的数字:”, matches)
提取后面是 USD 的数字
pattern_lookahead = r”\d+(?=USD)”
matches = re.findall(pattern_lookahead, text)
print(“提取 USD 前面的数字:”, matches)
提取不是以 $ 或 € 开头的数字
pattern_negative_lookbehind = r”(?<![$€])\b\d+\b” # 使用 \b 确保匹配整个数字
matches = re.findall(pattern_negative_lookbehind, text)
print(“提取不是 $ 或 € 开头的数字:”, matches) # 会匹配 75
提取后面不是 USD 的数字
pattern_negative_lookahead = r”\b\d+\b(?!USD)”
matches = re.findall(pattern_negative_lookahead, text)
print(“提取后面不是 USD 的数字:”, matches) # 会匹配 100, 50, 200
“`
输出:
提取 $ 或 € 后面的数字: ['100', '50']
提取 USD 前面的数字: ['75']
提取不是 $ 或 € 开头的数字: ['200', '75']
提取后面不是 USD 的数字: ['100', '50', '200']
零宽断言的强大之处在于它们不消耗字符,只提供一个匹配位置的条件。这使得你可以精确地定义需要提取的数据的边界,而无需在捕获组中包含边界标记。
6.3 使用标志位(Flags)
re
模块的函数(search
, match
, findall
, finditer
, compile
等)都有一个可选的 flags
参数,用于修改匹配行为。常用的标志位:
re.IGNORECASE
或re.I
: 忽略大小写匹配。re.DOTALL
或re.S
: 使.
匹配包括换行符在内的所有字符。默认情况下.
不匹配换行符。re.MULTILINE
或re.M
: 使^
和$
匹配每行的开头和结尾,而不仅仅是整个字符串的开头和结尾。re.VERBOSE
或re.X
: 忽略模式字符串中的空白符和#
后面的注释,这有助于编写更易读的复杂模式。
示例:
“`python
text = “Line 1\nLine 2\nEND”
默认情况下 . 不匹配换行符
match_dot_default = re.search(r”Line.*END”, text)
print(“默认 . 匹配:”, match_dot_default.group() if match_dot_default else “未找到”)
使用 re.DOTALL 使 . 匹配换行符
match_dot_all = re.search(r”Line.*END”, text, re.DOTALL)
print(“DOTALL 匹配:”, match_dot_all.group() if match_dot_all else “未找到”)
print(“-” * 20)
text_multiline = “Start\nLine 1\nLine 2\nEnd”
默认 ^ 和 $ 匹配整个字符串开头/结尾
match_multiline_default = re.findall(r”^\w+”, text_multiline)
print(“默认 ^ 匹配:”, match_multiline_default)
使用 re.MULTILINE 使 ^ 和 $ 匹配每行开头/结尾
match_multiline_m = re.findall(r”^\w+”, text_multiline, re.MULTILINE)
print(“MULTILINE ^ 匹配:”, match_multiline_m)
print(“-” * 20)
使用 re.VERBOSE 编写可读性高的模式
pattern_verbose = re.compile(r”””
^(\d{4}) # 匹配年份
– # 匹配连字符
(\d{2}) # 匹配月份
– # 匹配连字符
(\d{2})$ # 匹配日期
“””, re.VERBOSE)
text_date = “2023-11-05”
match_verbose = pattern_verbose.match(text_date)
if match_verbose:
print(“VERBOSE 模式提取:”, match_verbose.groups())
else:
print(“VERBOSE 模式未匹配”)
“`
输出:
“`
默认 . 匹配: 未找到
DOTALL 匹配: Line 1
Line 2
END
默认 ^ 匹配: [‘Start’]
MULTILINE ^ 匹配: [‘Start’, ‘Line’, ‘Line’, ‘End’]
VERBOSE 模式提取: (‘2023′, ’11’, ’05’)
“`
re.VERBOSE
对于编写和调试复杂的提取模式尤其有用,因为它允许你在模式中加入注释和格式化,使其更易于理解。
第七章:实战示例:常见的提取任务
本章将通过几个实际场景,展示如何综合运用上述知识进行数据提取。
7.1 提取所有 URL
假设从一段 HTML 或文本中提取所有的 HTTP/HTTPS URL。
“`python
import re
text = “””
Visit our website at https://www.example.com/ or
our old site http://legacy.example.org/index.html.
You can also find us at http://sub.domain.co.uk/path?query=param.
Invalid URL: ftp://not-a-web-url, or just example.com.
“””
一个相对简单的 URL 模式,匹配 http 或 https 开头,后面跟着非空白字符
pattern_url = r”https?://[^\s]+”
urls = re.findall(pattern_url, text)
print(“提取到的 URL:”)
for url in urls:
print(url)
print(“-” * 20)
一个更详细的 URL 模式,尝试分解 URL 各部分 (协议, 域名, 路径+查询+片段)
注意:完整的 URL 正则表达式非常复杂,这里只是一个示例
pattern_url_parts = r”(?P
print(“提取 URL 并分解各部分:”)
for match in re.finditer(pattern_url_parts, text):
print(match.groupdict())
“`
输出:
“`
提取到的 URL:
https://www.example.com/
http://legacy.example.org/index.html
http://sub.domain.co.uk/path?query=param
提取 URL 并分解各部分:
{‘protocol’: ‘https://’, ‘domain’: ‘www.example.com’, ‘path’: ‘/’}
{‘protocol’: ‘http://’, ‘domain’: ‘legacy.example.org’, ‘path’: ‘/index.html’}
{‘protocol’: ‘http://’, ‘domain’: ‘sub.domain.co.uk’, ‘path’: ‘/path?query=param’}
“`
7.2 从配置文本中提取键值对
假设有如下格式的配置文本 key = value
,需要提取所有键和对应的值。
“`python
import re
config_text = “””
This is a comment
Database = mydb
User = admin
Password = secret
Server = 192.168.1.100
Port = 5432
“””
匹配以字母或数字开头的键,等于号,以及后面的值 (直到行尾)
使用 VERBOSE 模式增强可读性
pattern_config = re.compile(r”””
^ \s # 行开头,可能有前导空白
(?P
\s
(?P
\s* $ # 行尾,可能有后导空白
“””, re.MULTILINE | re.VERBOSE) # 使用 MULTILINE 使 ^ 和 $ 匹配每行
config_data = {}
for match in re.finditer(pattern_config, config_text):
key = match.group(‘key’)
value = match.group(‘value’).strip() # 移除值两端的空白
config_data[key] = value
print(“提取到的配置数据:”)
print(config_data)
“`
输出:
提取到的配置数据:
{'Database': 'mydb', 'User': 'admin', 'Password': 'secret', 'Server': '192.168.1.100', 'Port': '5432'}
这里使用了 re.MULTILINE
标志,使得 ^
和 $
可以在多行文本中正确地匹配每一行的开头和结尾。re.VERBOSE
使复杂的模式更易于理解。
7.3 从结构化日志中提取字段
假设有格式固定的日志行,如 [时间] [级别] 消息内容
。
“`python
import re
log_text = “””
[2023-10-27 10:00:01] INFO User logged in.
[2023-10-27 10:01:35] ERROR Database connection failed.
[2023-10-27 10:05:10] DEBUG Background process started.
“””
提取时间、级别和消息
pattern_log = r”[(?P
log_entries = []
for match in re.finditer(pattern_log, log_text):
log_entries.append(match.groupdict())
print(“提取到的日志条目:”)
import json
print(json.dumps(log_entries, indent=2)) # 使用 json pretty print 方便查看
“`
输出:
json
[
{
"timestamp": "2023-10-27 10:00:01",
"level": "INFO",
"message": "User logged in."
},
{
"timestamp": "2023-10-27 10:01:35",
"level": "ERROR",
"message": "Database connection failed."
},
{
"timestamp": "2023-10-27 10:05:10",
"level": "DEBUG",
"message": "Background process started."
}
]
第八章:最佳实践与注意事项
- 始于简单,逐步复杂: 从匹配最简单的部分开始,然后逐步添加更复杂的条件和捕获组。
- 使用原始字符串: 始终使用
r"..."
来定义你的正则表达式。 - 善用在线工具: 使用像 regex101.com, regexr.com 这样的在线工具来构建、测试和调试你的正则表达式。它们通常提供详细的解释,帮助你理解模式的匹配过程。
- 命名捕获组提高可读性: 对于包含多个捕获组的模式,使用命名捕获组让你的代码更易于理解和维护。
-
re.compile()
提升性能: 如果你需要在循环中多次使用同一个正则表达式,使用re.compile()
将其编译成一个 Regex 对象的效率更高。“`python
import re
import timetext = “abc ” * 10000 # 一个较长的字符串
pattern = r”abc”start_time = time.time()
for _ in range(1000):
re.search(pattern, text)
print(f”未使用 compile: {time.time() – start_time:.6f}秒”)compiled_pattern = re.compile(pattern)
start_time = time.time()
for _ in range(1000):
compiled_pattern.search(text)
print(f”使用 compile: {time.time() – start_time:.6f}秒”)
“`
(注意:对于非常简单的模式或较少的重复次数,性能差异可能不明显,但对于复杂模式和大量重复,差异会很显著) -
了解何时不使用 Regex: 正则表达式是强大的,但不是万能的。对于解析具有复杂、嵌套结构的文本(如 HTML/XML、JSON),使用专门的解析器(如 BeautifulSoup, lxml, json 模块)通常是更健壮、更高效、更不容易出错的方法。尝试用正则表达式解析任意 HTML 是一个常见的陷阱。
- 处理可能的匹配失败: 在代码中,始终检查
re.search()
或re.match()
的返回值是否为None
,或者在迭代re.finditer()
的结果时,确保循环能够正确处理空输入或无匹配的情况。使用try-except
或if match:
判断是良好的实践。 - 注意贪婪性: 当使用量词时,特别是
.*
,要警惕其贪婪性可能导致过度匹配,适时使用非贪婪量词.*?
。 - 谨慎使用
.
和.*
:.
默认不匹配换行符,如果需要,使用re.DOTALL
标志。.*
会匹配任意多字符,可能导致意外匹配,考虑使用更具体的字符集或模式。 - 测试边界情况: 除了典型的输入,还要测试空字符串、只有部分匹配、边界附近的文本等情况,确保模式的鲁棒性。
结论
正则表达式是 Python 中进行文本处理和数据提取的利器。通过本文的学习,你应该已经掌握了从基础的匹配(re.search
, re.match
)到核心的数据提取方法(捕获组、re.findall
, re.finditer
, 命名捕获组),以及提高精度和灵活性的高级技巧(非贪婪匹配、零宽断言、标志位)。
数据提取是许多编程任务中的常见需求,无论是从日志文件中解析信息、从网页抓取特定内容、还是从非结构化文本中提取关键数据,正则表达式都能发挥重要作用。
掌握正则表达式需要时间和实践。多写、多练、多调试,并结合在线工具的辅助,你将能够熟练运用这一强大工具,高效地完成各种文本数据提取任务。祝你在 Python 正则表达式的学习和应用之路上取得成功!