深入理解Python中的正则表达式
2022-04-122.1k 阅读
正则表达式基础
什么是正则表达式
正则表达式(Regular Expression),又称规则表达式,是一种文本模式,用于描述字符串的匹配规则。它可以用于查找、替换、验证文本等操作。正则表达式在各种编程语言中都有广泛应用,Python 也不例外。在 Python 中,通过 re
模块来支持正则表达式操作。
正则表达式的基本语法
- 字符匹配
- 普通字符:大多数普通字符,如字母、数字、标点符号等,在正则表达式中就表示它们自身。例如,正则表达式
abc
会匹配字符串中连续出现的abc
。 - 元字符:一些具有特殊含义的字符,如
.
、^
、$
、*
、+
、?
、{}
、[]
、()
、|
等。.
:匹配除换行符\n
之外的任意单个字符。例如,正则表达式a.c
可以匹配abc
、a c
、a#c
等。^
:匹配字符串的开始位置。例如,正则表达式^abc
只会匹配以abc
开头的字符串,像abcdef
可以匹配,而defabc
则不能匹配。$
:匹配字符串的结束位置。例如,正则表达式abc$
只会匹配以abc
结尾的字符串,如defabc
可以匹配,abcdef
则不能匹配。
- 普通字符:大多数普通字符,如字母、数字、标点符号等,在正则表达式中就表示它们自身。例如,正则表达式
- 字符类
[]
:定义一个字符类,匹配方括号内的任意一个字符。例如,[abc]
可以匹配a
、b
或c
中的任意一个字符。- 范围表示:在字符类中,可以使用
-
表示字符范围。例如,[a - z]
匹配任意小写字母,[0 - 9]
匹配任意数字。 - 否定字符类:
[^ ]
表示匹配不在方括号内的任意字符。例如,[^a - z]
匹配任意非小写字母的字符。
- 量词
*
:匹配前面的字符零次或多次。例如,a*
可以匹配空字符串,也可以匹配一个或多个a
,如a
、aa
、aaa
等。+
:匹配前面的字符一次或多次。例如,a+
至少匹配一个a
,可以是a
、aa
、aaa
等,但不能匹配空字符串。?
:匹配前面的字符零次或一次。例如,a?
可以匹配空字符串或一个a
。{n}
:匹配前面的字符恰好n
次。例如,a{3}
只匹配连续出现三次的a
,即aaa
。{n,}
:匹配前面的字符至少n
次。例如,a{3,}
匹配连续出现三次或更多次的a
,如aaa
、aaaa
等。{n,m}
:匹配前面的字符至少n
次,最多m
次。例如,a{3,5}
匹配连续出现三次到五次的a
,如aaa
、aaaa
、aaaaa
。
- 分组
()
:用于分组,可以将多个字符作为一个整体进行操作。例如,(ab)+
会把ab
作为一个整体,匹配一个或多个ab
,如ab
、abab
、ababab
等。- 捕获组:分组默认是捕获组,在匹配成功后,可以通过索引获取捕获组内匹配的内容。例如,
(a(b)c)
有两个捕获组,第一个捕获组是abc
,第二个捕获组是b
。
- 分支
|
:表示分支,匹配|
两边的任意一个表达式。例如,a|b
可以匹配a
或b
。
Python 中的 re
模块
常用函数
re.search()
- 功能:在字符串中搜索正则表达式的第一个匹配项。
- 语法:
re.search(pattern, string, flags = 0)
,其中pattern
是正则表达式,string
是要搜索的字符串,flags
是可选参数,用于控制匹配行为。 - 示例:
import re
text = "python is a great language, python is fun"
match = re.search('python', text)
if match:
print('Match found:', match.group())
else:
print('Match not found')
- 解释:上述代码使用
re.search
在text
字符串中搜索python
,如果找到匹配项,就打印出匹配的内容。match.group()
方法用于获取匹配的字符串。
re.match()
- 功能:从字符串的开头开始匹配正则表达式。
- 语法:
re.match(pattern, string, flags = 0)
。 - 示例:
import re
text = "python is a great language"
match = re.match('python', text)
if match:
print('Match found:', match.group())
else:
print('Match not found')
- 解释:这里使用
re.match
从text
字符串开头匹配python
,由于text
开头是python
,所以能匹配成功并打印出匹配内容。如果字符串不是以python
开头,则匹配失败。
re.findall()
- 功能:在字符串中找到所有匹配正则表达式的子串,并以列表形式返回。
- 语法:
re.findall(pattern, string, flags = 0)
。 - 示例:
import re
text = "python is a great language, python is fun"
matches = re.findall('python', text)
print('Matches found:', matches)
- 解释:
re.findall
在text
字符串中找到所有的python
子串,并以列表形式返回,所以会打印出['python', 'python']
。
re.finditer()
- 功能:在字符串中找到所有匹配正则表达式的子串,并返回一个迭代器,迭代器的每个元素是一个
Match
对象。 - 语法:
re.finditer(pattern, string, flags = 0)
。 - 示例:
- 功能:在字符串中找到所有匹配正则表达式的子串,并返回一个迭代器,迭代器的每个元素是一个
import re
text = "python is a great language, python is fun"
for match in re.finditer('python', text):
print('Match found at position:', match.start())
- 解释:通过
re.finditer
遍历text
字符串中所有匹配python
的子串,match.start()
方法用于获取匹配子串在原字符串中的起始位置。
re.sub()
- 功能:用指定的字符串替换字符串中所有匹配正则表达式的子串。
- 语法:
re.sub(pattern, repl, string, count = 0, flags = 0)
,其中repl
是替换的字符串,count
是替换的最大次数,默认为 0 表示替换所有匹配项。 - 示例:
import re
text = "python is a great language, python is fun"
new_text = re.sub('python', 'Java', text)
print('New text:', new_text)
- 解释:上述代码将
text
字符串中所有的python
替换为Java
,并打印出替换后的新字符串。
Match
对象
当使用 re.search()
、re.match()
或 re.finditer()
找到匹配项时,会返回一个 Match
对象。Match
对象有以下常用方法和属性:
group([group1, ...])
- 功能:返回整个匹配的子串或指定捕获组的子串。如果没有指定参数,返回整个匹配的子串;如果指定了一个或多个捕获组的索引,则返回相应捕获组的子串。
- 示例:
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
print('Whole match:', match.group())
print('Group 1:', match.group(1))
- 解释:在这个例子中,
(\w+)
是一个捕获组。match.group()
返回整个匹配的子串I love python
,match.group(1)
返回第一个捕获组匹配的子串python
。
start([group])
- 功能:返回整个匹配子串或指定捕获组子串在原字符串中的起始位置。如果没有指定参数,返回整个匹配子串的起始位置;如果指定了捕获组索引,则返回相应捕获组子串的起始位置。
- 示例:
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
print('Whole match start position:', match.start())
print('Group 1 start position:', match.start(1))
- 解释:
match.start()
返回整个匹配子串I love python
在text
字符串中的起始位置 0,match.start(1)
返回第一个捕获组匹配子串python
的起始位置 7。
end([group])
- 功能:返回整个匹配子串或指定捕获组子串在原字符串中的结束位置(结束位置不包含在匹配子串内)。参数用法与
start([group])
类似。 - 示例:
- 功能:返回整个匹配子串或指定捕获组子串在原字符串中的结束位置(结束位置不包含在匹配子串内)。参数用法与
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
print('Whole match end position:', match.end())
print('Group 1 end position:', match.end(1))
- 解释:
match.end()
返回整个匹配子串I love python
的结束位置 13,match.end(1)
返回第一个捕获组匹配子串python
的结束位置 13。
span([group])
- 功能:返回一个元组,包含整个匹配子串或指定捕获组子串在原字符串中的起始位置和结束位置,等价于
(start(group), end(group))
。 - 示例:
- 功能:返回一个元组,包含整个匹配子串或指定捕获组子串在原字符串中的起始位置和结束位置,等价于
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
print('Whole match span:', match.span())
print('Group 1 span:', match.span(1))
- 解释:
match.span()
返回(0, 13)
,表示整个匹配子串的起始和结束位置;match.span(1)
返回(7, 13)
,表示第一个捕获组匹配子串的起始和结束位置。
高级正则表达式特性
非贪婪匹配
- 贪婪匹配:在正则表达式中,默认情况下量词是贪婪的,即尽可能多地匹配字符。例如,正则表达式
a.*b
在字符串aabb
中,会匹配整个aabb
,因为.*
会贪婪地匹配尽可能多的字符直到遇到最后一个b
。 - 非贪婪匹配:通过在量词后面加上
?
可以实现非贪婪匹配。例如,a.*?b
在字符串aabb
中,只会匹配aab
,因为.*?
会尽可能少地匹配字符,一旦遇到b
就停止匹配。- 示例:
import re
text = "aabb"
greedy_match = re.search('a.*b', text)
non_greedy_match = re.search('a.*?b', text)
if greedy_match:
print('Greedy match:', greedy_match.group())
if non_greedy_match:
print('Non - greedy match:', non_greedy_match.group())
- 解释:上述代码分别演示了贪婪匹配和非贪婪匹配在同一字符串上的效果。贪婪匹配结果为
aabb
,非贪婪匹配结果为aab
。
零宽断言
- 正向肯定断言:
(?=pattern)
,断言在当前位置之后能匹配pattern
,但不消耗字符。例如,正则表达式\w+(?=ing)
会匹配以ing
结尾的单词,但不包含ing
部分。- 示例:
import re
text = "running jumping"
matches = re.findall('\w+(?=ing)', text)
print('Matches:', matches)
- 解释:
re.findall
使用正向肯定断言,在text
字符串中找到以ing
结尾的单词部分,所以会打印出['run', 'jump']
。
- 正向否定断言:
(?!pattern)
,断言在当前位置之后不能匹配pattern
。例如,\w+(?!ing)
会匹配不以ing
结尾的单词。- 示例:
import re
text = "running jumping play"
matches = re.findall('\w+(?!ing)', text)
print('Matches:', matches)
- 解释:此代码使用正向否定断言,在
text
字符串中找到不以ing
结尾的单词,所以会打印出['play']
。
- 反向肯定断言:
(?<=pattern)
,断言在当前位置之前能匹配pattern
,但不消耗字符。例如,(?<=abc)\d+
会匹配在abc
之后的数字。- 示例:
import re
text = "abc123 def456"
matches = re.findall('(?<=abc)\d+', text)
print('Matches:', matches)
- 解释:这里使用反向肯定断言,在
text
字符串中找到在abc
之后的数字部分,所以会打印出['123']
。
- 反向否定断言:
(?<!pattern)
,断言在当前位置之前不能匹配pattern
。例如,(?<!abc)\d+
会匹配不在abc
之前的数字。- 示例:
import re
text = "abc123 def456"
matches = re.findall('(?<!abc)\d+', text)
print('Matches:', matches)
- 解释:此代码使用反向否定断言,在
text
字符串中找到不在abc
之前的数字部分,所以会打印出['456']
。
命名捕获组
- 普通捕获组:在前面的例子中,我们使用
()
定义捕获组,并通过索引获取捕获组的内容。例如,(a(b)c)
有两个捕获组,索引分别为 1 和 2。 - 命名捕获组:可以给捕获组命名,通过
(?P<name>pattern)
的形式定义,其中name
是捕获组的名称,pattern
是捕获组的匹配模式。- 示例:
import re
text = "I love python"
match = re.search('I love (?P<language>\w+)', text)
if match:
print('Language:', match.group('language'))
- 解释:这里定义了一个名为
language
的命名捕获组,通过match.group('language')
可以获取该捕获组匹配的内容,即python
。命名捕获组使得代码更易读,尤其是在有多个捕获组的复杂正则表达式中。
标志位(Flags)
re.I
(IGNORECASE):使匹配对大小写不敏感。例如,re.search('python', 'Python is great', re.I)
能匹配成功,因为re.I
忽略了大小写。re.M
(MULTILINE):在多行模式下,^
和$
不仅匹配字符串的开头和结尾,还匹配每一行的开头和结尾。例如,对于字符串line1\nline2
,使用re.findall('^line', string, re.M)
会找到两个匹配项,分别是line1
和line2
开头的line
。re.S
(DOTALL):使.
可以匹配包括换行符\n
在内的任意字符。默认情况下,.
不匹配换行符。例如,re.search('a.*b', 'a\nb', re.S)
能匹配成功,因为re.S
让.
可以匹配换行符。re.X
(VERBOSE):可以在正则表达式中添加注释,使正则表达式更易读。例如:
import re
pattern = re.compile(r"""
\d+ # 匹配一个或多个数字
\. # 匹配点号
\d+ # 匹配一个或多个数字
""", re.X)
text = "1.2"
match = pattern.search(text)
if match:
print('Match found:', match.group())
- 解释:在
re.compile
中使用re.X
标志位,使得可以在正则表达式中添加注释,增强了代码的可读性。
实际应用场景
数据验证
- 验证邮箱地址:邮箱地址的格式通常为
用户名@域名
,可以使用正则表达式进行验证。- 示例:
import re
def validate_email(email):
pattern = r'^[a-zA - Z0 - 9_.+-]+@[a-zA - Z0 - 9 -]+\.[a-zA - Z0 - 9-.]+$'
if re.match(pattern, email):
return True
return False
email1 = "user@example.com"
email2 = "user.example.com"
print(validate_email(email1))
print(validate_email(email2))
- 解释:上述代码定义了一个函数
validate_email
,使用正则表达式验证邮箱地址的格式。^
和$
确保整个字符串都匹配指定模式,[a-zA - Z0 - 9_.+-]+
匹配用户名部分,@
匹配@
符号,[a-zA - Z0 - 9 -]+\.[a-zA - Z0 - 9-.]+
匹配域名部分。
- 验证手机号码:不同国家的手机号码格式不同,以中国手机号码为例,通常为 11 位数字,且以 1 开头。
- 示例:
import re
def validate_phone(phone):
pattern = r'^1\d{10}$'
if re.match(pattern, phone):
return True
return False
phone1 = "13800138000"
phone2 = "12345678901"
print(validate_phone(phone1))
print(validate_phone(phone2))
- 解释:此函数
validate_phone
使用正则表达式验证手机号码,^1
表示以 1 开头,\d{10}
表示后面跟着 10 位数字。
文本提取
- 从 HTML 中提取链接:在处理网页数据时,经常需要从 HTML 代码中提取链接。
- 示例:
import re
html = '<a href="https://example.com">Example</a>'
links = re.findall(r'href="([^"]+)"', html)
print('Links:', links)
- 解释:这里使用
re.findall
和正则表达式href="([^"]+)"
从 HTML 代码中提取链接。([^"]+)
是一个捕获组,用于捕获href
属性值中的链接部分。
- 从日志文件中提取特定信息:假设日志文件中有如下格式的记录:
[2023 - 01 - 01 12:00:00] INFO: Message
,可以提取日期、时间、日志级别和消息。- 示例:
import re
log = '[2023 - 01 - 01 12:00:00] INFO: Message'
pattern = r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.*)'
match = re.search(pattern, log)
if match:
print('Date and time:', match.group(1))
print('Log level:', match.group(2))
print('Message:', match.group(3))
- 解释:通过定义合适的正则表达式,使用捕获组分别提取日志中的日期时间、日志级别和消息内容。
文本替换
- 替换敏感词:在处理文本时,可能需要替换一些敏感词汇。
- 示例:
import re
text = "This is a bad word"
new_text = re.sub(r'bad', 'good', text)
print('New text:', new_text)
- 解释:使用
re.sub
将文本中的bad
替换为good
。
- 格式化文本:例如,将日期格式从
YYYY - MM - DD
转换为MM/DD/YYYY
。- 示例:
import re
date = "2023 - 01 - 01"
new_date = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', date)
print('New date:', new_date)
- 解释:通过捕获组分别捕获年、月、日,然后在替换字符串中按照新的格式重新组合。
编写高效的正则表达式
避免不必要的捕获组
- 捕获组的开销:每个捕获组都会增加正则表达式引擎的处理开销,因为引擎需要记录捕获组的匹配内容。例如,在
(a|b)
这样的简单分支中,如果不需要获取捕获组的内容,就可以使用非捕获组(?:a|b)
。非捕获组不会记录匹配内容,从而提高匹配效率。 - 示例对比:
import re
import timeit
# 使用捕获组
pattern1 = re.compile('(a|b)')
text = 'ab'
def test1():
pattern1.findall(text)
# 使用非捕获组
pattern2 = re.compile('(?:a|b)')
def test2():
pattern2.findall(text)
print('Time with capturing group:', timeit.timeit(test1, number = 10000))
print('Time with non - capturing group:', timeit.timeit(test2, number = 10000))
- 解释:上述代码通过
timeit
模块对比了使用捕获组和非捕获组的执行时间。可以看到,在不需要获取捕获组内容时,使用非捕获组的效率更高。
优先使用字符类
- 字符类的效率:在匹配字符集合时,使用字符类比多个分支效率更高。例如,要匹配
a
、b
或c
,使用[abc]
比a|b|c
效率更高。因为字符类是一种更紧凑的表示方式,正则表达式引擎可以更快地处理。 - 示例对比:
import re
import timeit
# 使用分支
pattern1 = re.compile('a|b|c')
text = 'abc'
def test1():
pattern1.findall(text)
# 使用字符类
pattern2 = re.compile('[abc]')
def test2():
pattern2.findall(text)
print('Time with alternation:', timeit.timeit(test1, number = 10000))
print('Time with character class:', timeit.timeit(test2, number = 10000))
- 解释:通过
timeit
模块对比可以发现,使用字符类的正则表达式在匹配字符集合时效率更高。
简化复杂的正则表达式
- 逐步构建和测试:对于复杂的正则表达式,不要试图一次性写出完整的表达式。可以先从简单的部分开始,逐步构建并测试。例如,要匹配一个复杂的文件路径格式,可以先分别测试匹配盘符、路径分隔符、文件名等部分的正则表达式,然后再组合起来。
- 使用注释和命名捕获组:如前面提到的,使用注释(结合
re.X
标志位)和命名捕获组可以使复杂的正则表达式更易读和维护。例如,在匹配复杂的网络地址时,使用命名捕获组分别表示不同部分,代码会更清晰。 - 避免过度复杂:有时候,过于复杂的正则表达式可能会导致可读性和效率都降低。如果可以通过其他方式(如结合字符串操作函数)来简化处理,应该优先考虑。例如,在处理一些简单的文本替换任务时,可能使用字符串的
replace
方法比复杂的正则表达式更合适。
通过深入理解正则表达式的基本语法、Python 的 re
模块以及高级特性,并在实际应用中注意编写高效的正则表达式,开发者可以在文本处理任务中更加得心应手,提高编程效率和代码质量。无论是数据验证、文本提取还是文本替换等任务,正则表达式都是 Python 开发者工具箱中的强大工具。