正则表达式:用于描述一种字符串匹配的模式。它可用于检查一个字符串是否含有某个子串,也可用于从字符串中提取匹配的子串,或者对字符串中匹配的子串执行替换操作。
一、Python正则表达式支持
在Python的交互解释器中先导入re模块,然后输入re.__all__命令,即可看到该模块所包含的全部属性和函数。
import re
print(re.__all__)
[‘match’, ‘fullmatch’, ‘search’, ‘sub’, ‘subn’, ‘split’, ‘findall’, ‘finditer’, ‘compile’, ‘purge’, ‘template’, ‘escape’, ‘error’, ‘Pattern’, ‘Match’, ‘A’, ‘I’, ‘L’, ‘M’, ‘S’, ‘X’, ‘U’, ‘ASCII’, ‘IGNORECASE’, ‘LOCALE’, ‘MULTILINE’, ‘DOTALL’, ‘VERBOSE’, ‘UNICODE’]
从上面输出结果可以看出,re模块包含了为数不多的几个函数和属性(用于控制正则表达式匹配的几个选项),下面先介绍这些函数的作用。
1、re.compile(pattern, flags=0): 该函数用于将正则表达式字符串编译成_sre.SRE_Pattern对象,该对象代表了正则表达式编译之后在内存中的对象,它可以缓存并复用正则表达式字符串。
如果程序需要多次使用同一个正则表达式字符串,可以考虑先编译它。该函数的pattern参数就是它所编译的正则表达式字符串,flags则代表了正则表达式的匹配旗标。
import re
# 先编译正则表达式
p = re.compile('abc')
# 调用_sre.SRE_Pattern对象的search()方法
p.search('www.abc.com')
# 上面两行代码和下面的代码效果基本相同
# 直接用正则表达式匹配目标字符串
re.search('abc', 'www.abc.com')
2、re.match(pattern, string, flags=0): 尝试从字符串的开始位置来匹配正则表达式,如果从开始位置匹配不成功,match()函数就返回None。其中pattern参数代表正则表达式;string代表被匹配的字符串;flags代表了正则表达式的匹配旗标。
该函数返回_sre.SRE_Pattern对象,该对象包含的span(n)方法用于获取第n+1个组的匹配位置,group(n)方法用于获取第n+1个组所匹配的子串。
3、re.search(pattern, string, flags=0): 扫描整个字符串,并返回字符串中第一处匹配pattern的匹配对象。其中pattern参数代表正则表达式;string代表被匹配的字符串;flags代表了正则表达式的匹配旗标。该函数返回_sre.SRE_Match对象。
match()与search()的区别在于:match()必须从字符串开始处就匹配,但search()则可以搜索整个字符串。
import re
m1 = re.match('www', 'www.fkit.org')
print(m1.span()) # (0, 3)
print(m1.group()) # www
print(re.match('fkit', 'www.fkit.com')) # None
m2 = re.search('www', 'www.fkit.org')
print(m2.span()) # (0, 3)
print(m2.group()) # www
m3 = re.search('fkit', 'www.fkit.com')
print(m3.span()) # (4, 8)
print(m3.group()) # fkit
4、re.findall(pattern, string, flags=0): 扫描整个字符串,并返回字符串中所有匹配pattern的子串组成的列表。
5、re.finditer(pattern, string, flags=0): 扫描整个字符串,并返回字符串中所有匹配pattern的子串组成的迭代器,迭代的元素是_sre.SRE_Match对象。
import re
# re.I:使用正则表达式匹配时,不区分大小写
print(re.findall('fkit', 'FkIt is very good, Fkit.org is my favorite', re.I)) # ['FkIt', 'Fkit']
it = re.finditer('fkit', 'FkIt is very good, Fkit.org is my favorite', re.I)
for e in it:
print(str(e.span()) + '-->' + e.group()) # (0, 4)-->FkIt (19, 23)-->Fkit
6、re.fullmatch(pattern, string, flags=0): 该函数要求整个字符串能匹配pattern,如果匹配则返回包含匹配信息的_sre.SRE_Match对象,否则返回None。
7、re.sub(pattern, repl, string, count=0, flags=0): 该函数用于将string字符串中所有匹配pattern的内容替换repl;repl既可是被替换的字符串,也可以是一个函数;count参数控制最多替换多少次,如果指定count为0,则表示全部替换。
import re
my_data = '2020-10-30'
# 将my_data字符串里的中画线替换成斜线
print(re.sub(r'-', '/', my_data)) # 2020/10/30
# 将my_data字符串里的中画线替换成斜线,只替换一次
print(re.sub(r'-', '/', my_data, 1)) # 2020/10-30
注:r’-'是原始字符串,其中r代表原始字符串,通过使用原始字符串,可以避免对字符串中的特殊字符进行转义。
在某些情况下,所执行的替换要基于被替换的内容进行改变。比如下面程序需要将字符串中的每个英文单词都变成一本图书的名字。
import re
# 在匹配的字符串前后添加内容
def fun(matched):
# matched就是匹配对象,通过该对象的group()方法可获取被匹配的字符串
value = '《疯狂' + (matched.group('lang')) + '讲义》'
return value
s = 'Python很好,Kotlin很好'
# 对s里面的英文单词(用re.A旗标控制)进行替换
# 使用fun函数指定替换的内容
print(re.sub(r'(?P<lang>\w+)', fun, s, flags=re.A)) # 《疯狂Python讲义》很好,《疯狂Kotlin讲义》很好
8、re.split(pattern, string, maxsplit=0, flags=0): 使用pattern对string进行分割,该函数返回分割得到的多个子串组成的列表。其中maxsplit参数控制最多分割几次。
import re
# 使用逗号对字符串进行分割
print(re.split(', ', 'fkit, fkjava, crazyit')) # ['fkit', 'fkjava', 'crazyit']
# 指定只分割一次,被切分成两个子串
print(re.split(', ', 'fkit, fkjava, crazyit', 1)) # ['fkit', 'fkjava, crazyit']
# 使用a进行分割
print(re.split('a', 'fkit, fkjava, crazyit')) # ['fkit, fkj', 'v', ', cr', 'zyit']
# 使用x进行分割,没有匹配内容,则不会执行分割
print(re.split('x', 'fkit, fkjava, crazyit')) # ['fkit, fkjava, crazyit']
9、re.purge(): 清除正则表达式缓存。
10、re.escape(pattern): 对模式中除ASCII字符、数值、下划线(_)之外的其他字符进行转义。
import re
# 对模式中的特殊字符进行转义
print(re.escape(r'www.crazyit.org is good, i love it')) # www\.crazyit\.org\ is\ good,\ i\ love\ it
print(re.escape(r'A-Zand0-9?')) # A\-Zand0\-9\?
示范使用正则表达式的方法来执行匹配:
import re
# 编译得到正则表达式对象
pa = re.compile('fkit')
# 调用match方法,原本应该从开始位置匹配
# 此处指定从索引4的地方开始匹配,可以匹配成功
print(pa.match('www.fkit.org', 4).span()) # (4, 8)
# 此处指定从索引4到索引6之间执行匹配,匹配失败
print(pa.match('www.fkit.org', 4, 6)) # None
# 此处指定从索引4到索引8之间执行全匹配,匹配成功
print(pa.fullmatch('www.fkit.org', 4, 8).span()) # (4, 8)
re模块中的Match对象(其具体类型为_sre.SRE_Match)是match()、search()方法的返回值,该对象包含了详细的正则表达式匹配信息,包括正则表达式的位置、正则表达式所匹配的子串。
_sre.SRE_Match对象包含了如下方法或属性:
- match.group([group1, …]): 获取该匹配对象中指定组所匹配的字符串。
- match.__ getitem__(g): 这是match.group(g)的简化写法。
- match.groups(default=None): 返回match对象中所有组所匹配的字符串组成的元组。
- match.groupdict(default=None): 返回match对象中所有组所匹配的字符串组成的字典。
- match.start([group]): 获取该匹配对象中指定组所匹配的字符串的开始位置。
- match.end([group]):获取该匹配对象中指定组所匹配的字符串的结束位置。
- match.span([group]): 获取该匹配对象中指定组所匹配的字符串的开始位置和结束位置。
# 在正则表达式中使用组
m = re.search(r'(fkit).(org)', r"www.fkit.org is a good domain")
# group(0):整个正则表达式所匹配的子串
print(m.group(0)) # fkit.org
# 调用的简化写法,底层是调用m.__getitem__(0)
print(m[0]) # fkit.org
print(m.span(0)) # (4, 12)
# group(1):第一个组所匹配的子串
print(m.group(1)) # fkit
# 调用的简化写法,底层是调用m.__getitem__(1)
print(m[1]) # fkit
print(m.span(1)) # (4, 8)
# # group(2):第二个组所匹配的子串
print(m.group(2)) # org
# 调用的简化写法,底层是调用m.__getitem__(2)
print(m[2]) # org
print(m.span(2)) # (9, 12)
# 返回所有组所匹配的字符串组成的元组
print(m.groups()) # ('fkit', 'org')
如果在正则表达式中为组指定了名字(用?P<名字>为正则表达式的组指定名字),
就可以调用groupdict()方法来获取所有组所匹配的字符串组成的字典 —— 其中组名作为字典的key。
# 为正则表达式定义了两个组,并为组指定了名字
m2 = re.search(r'(?P<prefix>fkit).(?P<suffix>org)', r"www.fkit.org is a good domain")
print(m2.groupdict()) # {'prefix': 'fkit', 'suffix': 'org'}
- match.pos: 该属性返回传给正则表达式对象的search()、match()等方法的pos参数。
- match.endpos: 该属性返回传给正则表达式对象的search()、match()等方法的endpos参数。
- match.lastindex: 该属性返回最后一个匹配的捕获组的整数索引。如果没有组匹配,该属性返回None。例如用(a)b、((a)(b))或((ab))对字符串’ab’执行匹配,该属性都会返回1;但如果使用(a)(b)正则表达式对’ab’执行匹配,则lastindex等于2。
- match.lastgroup: 该属性返回最后一个匹配的捕获组的名字。如果该组没有名字或根本没有组匹配,该属性返回None。
- match.re: 该属性返回执行正则表达式匹配时所用的正则表达式。
- match.string: 该属性返回执行正则表达式匹配时所用的字符串。
二、正则表达式旗标
Python支持的正则表达式旗标都使用该模块中的属性来代表,这些旗标如下所示。
- re.A或re.ASCII:该旗标控制\w、\W、\b、\B、\d、\D、\s和\S只匹配ASCII字符,而不匹配所有的Unicode字符。也可以在正则表达式中使用(?a)行内旗标来代表。
- re.DEBUG:显示编译正则表达式的Debug信息。没有行内旗标。
- re.I或re.IGNORECASE:使用正则表达式匹配时不区分大小写。对应于正则表达式中的(?i)行内旗标。
import re
# 默认区分大小写,所以无匹配
print(re.findall(r'fkit', 'Fkit is a good domain, FKIT is good')) # []
# 使用re.I不区分大小写
print(re.findall(r'fkit', 'Fkit is a good domain, FKIT is good', re.I)) # ['Fkit', 'FKIT']
- re.L或re.LOCALE:根据当前区域设置使用正则表达式匹配时不区分大小写。该旗标只能对bytes模式起作用,对应于正则表达式中的(?L)行内旗标。
- re.M或re.MULTILINE:多行模式的旗标。当指定该旗标后,“^”能匹配字符串的开头和每行的开头(紧跟在每一个换行符的后面); “$ ”能匹配字符串的末尾和每行的末尾(紧跟在每一个换行符的之前)。在默认情况下,“^”只匹配字符串的开头,“$”只匹配字符串的结尾,或者匹配到字符串默认的换行符(如果有)之前。对应于正则表达式中的(?m)行内旗标。
- re.S或re.DOTALL:让(.)能匹配包括换行符在内的所有字符,如果不指定该旗标,则点(.)能匹配不包含换行符的所有字符。对应于正则表达式中的(?s)行内旗标。
- re.U或re.Unicode:该旗标控制\w、\W、\b、\B、\d、\D、\s和\S能匹配所有的Unicode字符。这个旗标在Python 3.x 中完全是多余的,因为Python 3.x 默认的就是匹配所有的Unicode字符。
- re.X或re.VERBOSE:通过该旗标允许分行书写正则表达式,也允许为正则表达式添加注释,从而提高正则表达式的可读性。对应于正则表达式中的(?x)行内旗标。
例如,下面两个正则表达式都可匹配广州的座机号码,它们是完全一样的。
import re
# a使用了re.X旗标,意味着正则表达式可以换行,也可以添加注释
a = re.compile(r"""020 # 广州的区号
\- # 中间的短横线
\d{8} # 8个数值""", re.X)
b = re.compile(r'020\-\d{8}')
三、创建正则表达式
正则表达式就是一个用于匹配字符串的模板,它可以匹配一批字符串,所有创建正则表达式就是创建一个特殊的字符串。
正则表达式所支持的合法字符:
字符 | 解释 |
---|---|
x | 字符x(x可代表任意合法的字符) |
\uhhhh | 十六进制值0xhhhh所表示的Unicode字符 |
\t | 制表符(’\u0009’) |
\n | 新行(换行)符(’\000A’) |
\r | 回车符(’\000D’) |
\f | 换页符(’\000C’) |
\a | 报警(bell)符(’\0007’) |
\e | Escape符(’\001B’) |
\cx | x对应的控制符。例如\cM匹配Ctrl+M。x值必须为A-Z或a-z之一 |
正则表达式中的特殊字符:
特殊字符 | 说明 |
---|---|
$ | 匹配一行的结尾。要匹配$字符本身,请用 \ $ |
^ | 匹配一行的开头。要匹配^字符本身,请用 \ ^ |
() | 标记子表达式(也就是组)的开始位置和结束位置。要匹配这些字符,请使用 \ ( 和 \ ) |
[] | 用于确定中括号表达式的开始位置和结束位置。要匹配这些字符,请使用 \ [ 和 \ ] |
{} | 用于标记前面子表达式的出现频度。要匹配这些字符,请使用 \ { 和 \ } |
* | 指定前面子表达式可以出现零次或多次。要匹配*字符本身,请使用 \ * |
+ | 指定前面子表达式可以出现一次或多次。要匹配*字符本身,请使用 \ + |
? | 指定前面子表达式可以出现零次或多次。要匹配?字符本身,请使用 \ ? |
. | 匹配除换行符\ n 之外的任意单个字符。要匹配 . 字符本身,请使用\ . |
\ | 用于转义下一个字符,或指定八进制、十六进制字符。如果需要匹配 \ 字符,请使用 \ \ |
| | 指定在两项之间任选一项。如果要匹配 | 字符本身,请使用 \ | |
将上面多个字符拼起来,就可以创建一个正则表达式。例如:
import re
# 匹配A\
print(re.fullmatch(r'\u0041\\', 'A\\'))
# <re.Match object; span=(0, 2), match='A\\'>
# 匹配a<制表符>
print(re.fullmatch(r'\u0061\t', 'a\t'))
# <re.Match object; span=(0, 2), match='a\t'>
# // 匹配?[
print(re.fullmatch(r'\?\[', '?['))
# <re.Match object; span=(0, 2), match='?['>
通配符是可以匹配多个字符的特殊字符。正则表达式中的通配符的功能远远超出了普通通配符的功能,它被称为“预定义字符”。
正则表达式所支持的预定义字符:
预定义字符 | 说明 |
---|---|
. | 默认可匹配除换行符之外的任意字符,在使用re.S或s.DOTALL旗标之后,它还可匹配换行符 |
\d | 匹配0~9的所有数字 |
\D | 匹配非数字 |
\s | 匹配所有的空白字符,包括空格、制表符、回车符、换页符、换行符等 |
\S | 匹配所有的非空白字符 |
\w | 匹配所有的单词字符,包括0~9的所有数字、26个英文字母和下划线(_) |
\W | 匹配所有的非单词字符 |
import re
# c\wt可以匹配cat、cbt、cct、c0t、c9t等一批字符串
print(re.fullmatch(r'c\wt', 'cat'))
# <re.Match object; span=(0, 3), match='cat'>
# c\wt可以匹配cat、cbt、cct、c0t、c9t等一批字符串
print(re.fullmatch(r'c\wt', 'c9t'))
# <re.Match object; span=(0, 3), match='c9t'>
# 匹配如000-000-0000形式的电话号码
print(re.fullmatch(r'\d\d\d-\d\d\d-\d\d\d\d', '123-456-7890'))
# <re.Match object; span=(0, 12), match='123-456-7890'>
方括号表达式:
方括号表达式 | 说明 |
---|---|
表示枚举 | 例如[abc],表示a、b、c其中任意一个字符;[gz],表示g、z其中任意一个字符 |
表示范围 | 例如[a-f],表示a~f范围内的任意字符;[\\u0041-\\u0056],表示十六进制字符\u0041到\u0056范围的字符。范围可以和枚举结合使用,如[a-cx-z],表示a-c、x-z范围内的任意字符 |
表示求否:^ | 例如[^abc],表示非a、b、c的任意字符; [^a-f],表示不是a~f范围内的任意字符 |
边界匹配符:
边界匹配符 | 说明 |
---|---|
^ | 行的开头 |
$ | 行的结尾 |
\b | 单词的边界。即只能匹配单词前后的空白 |
\B | 非单词的边界。即只能匹配不在单词前后的空白 |
\A | 只匹配字符串的开头 |
\Z | 只匹配字符串的结尾,仅用于最后的结束符 |
四、子表达式
正则表达式还支持圆括号表达式,用于将多个表达式组成一个子表达式,在圆括号中可以使用或运算符(|)。
子表达式(组)支持如下用法:
- (exp):匹配exp表达式并捕获成一个自动命名的组,后面可通过“\1”引用第一个捕获组所匹配的子串,通过“\2”引用第二个捕获组所匹配的子串……依此类推。
import re
"""
正则表达式是r'Windows (95|98|NT|2000)[\w ]+\1',
其中(95|98|NT|2000)是一个组,该组可匹配95、98、NT、2000;
[\w ]+: 这个方括号表达式可匹配任意单词字符和空格,方括号后的"+"表示方括号表达式可出现1~N次
"\1": 引用第一个组所匹配的子串——假如第一个组匹配98,那么"\1"也必须是98
"""
print(re.search(r'Windows (95|98|NT|2000)[\w ]+\1', 'Windows 98 published in 98'))
# <re.Match object; span=(0, 26), match='Windows 98 published in 98'>
print(re.search(r'Windows (95|98|NT|2000)[\w ]+\1', 'Windows 98 published in 95'))
# None
- (?P< name>exp):匹配exp表达式并捕获成命名组,该组名字为name。
- (?P=name):引用name命名组所匹配的子串
"""
正则表达式是r'<(?P<tag>\w+)>\w+</(?P=tag)>',
表达式开始是"<"符号,它直接匹配该符号;
接下来定义了一个命名组:(?P<tag>\w+),该组的组名为tag,该组能匹配1~N个任意字符;
表达式又定义了一个">"符号,用于匹配一个HTML或XML标签。
接下来的"\w+"用于匹配标签中的内容;正则表达式又定义了"</",它直接匹配这两个字符;
之后的(?P=tag)就用于引用前面的tag组所匹配的子串。
"""
print(re.search(r'<(?P<tag>\w+)>\w+</(?P=tag)>', '<h3>xx</h3>'))
# <re.Match object; span=(0, 11), match='<h3>xx</h3>'>
print(re.search(r'<(?P<tag>\w+)>\w+</(?P=tag)>', '<h3>xx</h2>'))
# None
- (?:exp):匹配exp表达式并且不捕获。这种组与(exp)的区别就在于它是不捕获的,因此不能通过\1、\2等来引用。
import re
print(re.search(r'Windows (?:95|98|NT|2000)[a-z ]+', 'Windows 98 published in 98'))
# <re.Match object; span=(0, 24), match='Windows 98 published in '>
- (?<=exp):括号中的子模式必须出现在匹配内容的左侧,但exp不作为匹配的一部分。
- (?=exp):括号中的子模式必须出现在匹配内容的右侧,但exp不作为匹配的一部分。
import re
"""
在正则表达式r'(?<=<h1>).+?(?=</h1>)'中,
(?<=<h1>)是一个限定组,该组的内容就是<h1>,由于该组用了(?<=exp)声明,因此在被匹配内容的左侧必须有<h1>;
还有一个组是(?=</h1>),该组的内容是</h1>,该组用了(?=exp)声明,因此在被匹配内容的右侧必须有<h1>
"""
print(re.search(r'(?<=<h1>).+?(?=</h1>)', 'help! <h1>fkit.org</h1>! technology'))
# <re.Match object; span=(10, 18), match='fkit.org'>
print(re.search(r'(?<=<h1>).+?(?=</h1>)', 'help! <h1><div>fkit</div></h1>! technology'))
# <re.Match object; span=(10, 25), match='<div>fkit</div>'>
- (?<!exp):括号中的子模式必须不出现在匹配内容的左侧,但exp不作为匹配的一部分。其实它是(?<=exp)的逆向表达。
- (?!exp):括号中的子模式必须不出现在匹配内容的右侧,但exp不作为匹配的一部分。其实它是(?=exp)的逆向表达。
- (?#comment):注释组。“?#”后的内容是注释,不影响正则表达式本身。
import re
print(re.search(r'[a-zA-z0-9_]{3,}(?#username)@fkit\.org', 'sun@fkit.org'))
# <re.Match object; span=(0, 12), match='sun@fkit.org'>
- (?aiLmsux):旗标组,用于为整个正则表达式添加行内旗标,可同时指定一个或多个旗标。
import re
# 该正则表达式中指定了(?i)组,这意味着该正则表达式匹配时不区分大小写
print(re.search(r'(?i)[a-z0-9_]{3,}(?#username)@fkit\.org', 'Sun@FKIT.ORG'))
# <re.Match object; span=(0, 12), match='Sun@FKIT.ORG'>
- (?imsx-imsx:exp):只对当前组起作用的旗标。该组旗标与前一组旗标的区别是,前一组旗标作用于整个正则表达式,而这组旗标只影响组内的子表达式。
import re
print(re.search(r'(?i:[a-z0-9_]){3,}(?#username)@fkit\.org', 'Sun@fkit.org'))
# <re.Match object; span=(0, 12), match='Sun@fkit.org'>
如果在旗标前应用“-”,则表明去掉该旗标。比如在执行search()方法时传入了re.I参数,这意味着对整个正则表达式不区分大小写;如果希望某个组内的表达式依然区分大小写,则可使用(-i:exp)来表示。
import re
print(re.search(r'(?-i:[a-z0-9_]){3,}(?#username)@fkit\.org', 'sun@Fkit.org', re.I))
# <re.Match object; span=(0, 12), match='sun@Fkit.org'>
五、贪婪模式与勉强模式
正则表达式还提供了频度限定,用于限定前面的模式可出现的次数。Python正则表达式支持如下几种频度限定:
- *:限定前面的子表达式可出现0~N次。例如正则表达式 r’zo *’ 能匹配 ‘z’,也能匹配 ‘zoo’、‘zooo’ 等。 * 等价于{0, }。
- +:限定前面的子表达式可出现1~N次。例如正则表达式 r’zo+’ 不能匹配 ‘z’,可能匹配 ‘zoo’、‘zooo’ 等。 + 等价于{1, }。
- ?:前面的子表达式可出现0~1次。例如正则表达式 r’zo?’ 能匹配 ‘z’ 和 ‘zo’ 两个字符串。 ? 等价于{0, 1}。
- {n, m}:n和m均为非负整数,其中 n<=m,限定前面的子表达式出现 n~m 次。例如正则表达式 r’fo{1, 3}d’ 可匹配 ‘fod’、‘food’、‘foood’ 这三个字符串。
- {n, }:n是一个非负整数,限定前面的子表达式至少出现n次。例如正则表达式 r’fo{2, }d’ 可匹配‘food’、‘foood’、‘fooood’等字符串。
- {, m}:m是一个非负整数,限定前面的子表达式至多出现m次。例如正则表达式 r’fo{, 3}d’ 可匹配‘fd’、‘fod’、‘food’、‘foood’ 这四个字符串。
- {n}:n是一个非负整数,限定前面的子表达式必须出现n次。例如正则表达式 r’fo{2}d’ 只能匹配‘food’字符串。
在默认情况下,正则表达式的频度限定是贪婪模式的。所谓贪婪模式,指的是表达式中的模式会尽可能多地匹配字符。
import re
"""
正则表达式是r'@.+\.',该表达式就是匹配@符号和点号之间的全部内容。
但由于在@和点号之间用的是".+",其中"."可代表任意字符,而且此时是贪婪模式,
因此".+"会尽可能多地进行匹配,只要它最后有一个"."结尾即可,所以匹配结果是'@fkit.com.'
"""
print(re.search(r'@.+\.', 'sun@fkit.com.cn'))
# <re.Match object; span=(3, 13), match='@fkit.com.'>
只要在频度限定之后添加一个英文问号,贪婪模式就变成了勉强模式,所谓勉强模式,指的是表达式中的模式会尽可能少地匹配字符。
import re
print(re.search(r'@.+?\.', 'sun@fkit.com.cn'))
# <re.Match object; span=(3, 9), match='@fkit.'>