part10-3 Python常见模块(正则表达式)

 

六、 Python 正则表达式

正则表达式(Regular Expression)用于描述一种字符串匹配的模式(Pattern),即可用于检查一个字符串是否含有某个子串,也可用于从字符串中提取匹配到的子串,或者对字符串中匹配到的子串执行替换操作。

正则表达式是一个非常实用的工具,它包含的知识点较多,它的模式匹配能力也非常强,学习需要由浅入深的学习。

熟练使用正则表达式是一个很重要的技能。可用正则表达式来开发数据抓取、网络爬虫等程序。在 Python 中的正则表达式就是几个常用函数,难点是正则表达式字符串的开发。

1、 Python 的正则表达式支持

导入 re 模块后,可使用 re.__all__ 命令查看该模块所包含的全部属性和函数。示例如下:
>>> import re
>>> re.__all__
['match', 'fullmatch', 'search', 'sub', 'subn', 'split', 'findall', 'finditer', 'compile', 'purge', 'template', 'escape', 'error', 'A', 'I', 'L', 'M', 'S', 'X', 'U', 'ASCII', 'IGNORECASE', 'LOCALE', 'MULTILINE', 'DOTALL', 'VERBOSE', 'UNICODE']

re 模块中的常用函数介绍:
(1)、re.compile(pattern,flags=0):该函数用于将 pattern 代表的正则表达式字符串编译成 _sre.SRE_Pattern 对象,该对象是正则表达式编译之后在内存中的对象,编译后的对象可以缓存并复用正则表达式字符串。在代码中如果需要多次使用同一个正则表达式字符串,则可以先编译后再使用,这样可提高运行效率。

参数 flags 表示正则表达式的旗标。经过编译后的 _sre.SRE_Pattern 对象可以调用 re 模块中大部分函数。例如下面代码所示,将编译后的 _sre.SRE_Pattern 对象调用 re 模块的 search() 方法进行匹配。

import re
# 先编译正则表达式
p = re.compile('abc')
# 调用 _sre.SRE_Pattern 对象的 search() 方法 进行匹配
print(p.search('www.abc.com'))

# 下面代码直接使用 re 模块的 search() 函数匹配目标字符串
print(re.search('abc', 'www.abc.com'))

运行结果如下所示:
<_sre.SRE_Match object; span=(4, 7), match='abc'>
<_sre.SRE_Match object; span=(4, 7), match='abc'>

 

从输出可以看到,两次调用 search() 函数匹配到的结果是一样的,但是第一种方式预编译了正则表达式,可以复用 p 对象(该对象缓存了正则表达式字符串),有更好的性能。

(2)、re.match(pattern,string,flags=0):根据 pattern 的正则模式从 string 字符串的开始位置进行匹配。如果从开始位置匹配不成功,match() 函数就返回 None。flags 参数代表正则表达式的匹配旗标。该函数返回 _sre.SRE_Match 对象,该对象包含的 span(n) 方法用于获取第 n+1 个组的匹配位置,group(n) 方法用于获取第 n+1 个组所匹配的子串。

(3)、re.search(pattern, string, flags=0):根据 pattern 的正则模式在 string 代表的字符串中进行扫描,并返回字符串中第一处匹配 pattern 的匹配对象。flags 参数同样代表正则表达式的匹配旗标。该函数也返回 _sre.SRE_Match 对象。

match() 与 search() 的区别在于:match() 必须从字符串开始处就匹配,但 search() 可以搜索整个字符串。示例如下:

import re
m1 = re.match('www', 'www.michael.com')     # 从字符串开始处匹配
print(m1.span())        # span 返回匹配的位置,(0, 3)
print(m1.group())       # group 返回匹配的组,www
print(re.match('mich', 'www.mich.com'))     # 如果从开始位置匹配不到,返回 None
m2 = re.search('www', 'www.michael.com')    # 从开始位置匹配
print(m2.span())        # (0, 3)
print(m2.group())       # www
m3 = re.search('mich', 'www.michael.com')   # 从中间位置开始匹配,返回 Match 对象
print(m3.span())        # (4, 8)
print(m3.group())       # mich

运行代码,输出结果如下:
(0, 3)
www
None
(0, 3)
www
(4, 8)
mich

从上面代码的输出可知,match() 函数要求必须从字符串开始处匹配,而 search() 函数是扫描整个字符串,可以从字符串的任意位置开始匹配。

(4)、re.findall(pattern, string, flags=0):根据 pattern 的匹配模式对 string 字符串整个扫描,并返回字符串中所有与pattern 模式匹配的子串组成的列表。flags 参数同样是正则表达式的匹配旗标。

(5)、re.finditer(pattern, string, flags=0):根据 pattern 的匹配模式对 string 字符串整个扫描,并返回字符串中所有与 pattern 模式匹配的子串组成的迭代器,迭代器的元素是 _sre.SRE_Match 对象。flags 参数同样是正则表达式的匹配旗标。

findall() 和 finditer() 函数的功能基本相同,不同的是在于它们的返回值,findall() 返回所有匹配 pattern 的子串组成的
列表;而 finditer() 返回所有匹配 pattern 的子串组成的迭代器。

另外,search() 与 findall()、finditer() 的区别是,search() 只返回字符串中第一个匹配 pattern 的子串;而 findall() 和 finditer() 则返回字符串所有匹配 pattern 的子串。

findall() 和 finditer() 的使用示例如下:

import re
# 返回所有匹配 pattern 的子串组成的列表,re.I 旗标表示忽略大小写
print(re.findall('py', 'Py is very good, Py.org is official website', re.I))
# 返回所有匹配 pattern 的子串组成的迭代器,忽略大小写
it = re.finditer('py', 'Py is very good, Py.org is official website', re.I)
for i in it:
    print(str(i.span()) + "-->" + i.group())

运行代码,输出结果如下:
['Py', 'Py']
(0, 2)-->Py
(17, 19)-->Py

(6)、 re.fullmatch(pattern, string, flags=0):该函数要求整个string字符串能匹配 pattern,如果匹配则返回包含匹配信息的 _sre.SRE_Match 对象;否则返回 None。

(7)、re.sub(pattern, repl, string, count=0, flags=0):该函数用于将 string 字符串所有匹配 pattern 的内容替换成 repl;repl 可以是字符串,也可以是一个函数。count 参数控制最多替换多少次,如果指定 count为0,则表示全部替换。

sub() 函数的用法示例如下:

import re
my_date = '2019-11-11'
# 将 my_date 字符串中的 - 替换为 /
print(re.sub(r'-', '/', my_date))
# 只做一次替换
print(re.sub(r'-', '/', my_date, 1))

输出如下所示:
2019/11/11
2019/11-11

上面代码中的 r'-' 是原始字符串,r 代表原始字符串,可以避免对字符串中的特殊字符进行转义。sub() 在执行替换时可以基于被替换的内容进行改变。例如下面代码将字符串中的每个英文单词都变成一本图书的名字。示例如下:

import re
# 在匹配的字符串前后添加内容
def func(matched):
    # matched 就是匹配对象,通过该对象的 group() 方法可以获取被匹配的字符串
    result = '《' + matched.group('lang') + "入门到高级》"
    return result
s = 'Python 很好, Linux 也很好'
# 对 s 里面的英文单词进行替换,用 re.A 旗标控制单词
print(re.sub(r'(?P<lang>\w+)', func, s, flags=re.A))

运行结果如下所示:
《Python入门到高级》 很好, 《Linux入门到高级》 也很好

从输出结果可以看出,使用 sub() 函数执行替换时,指定使用 func() 函数作为替换内容,而 func() 函数负责在 pattern 匹配的字符串之前添加 “《”,在 pattern 匹配的字符串之后添加 “入门到高级》”。所以会看到上面的输出结果。

r'(?P<lang>\w+)' 正则表达式用圆括号表达式创建了一个组,并使用 “?P” 选项为该组起名为 lang,所起的组名要放在尖括号内。剩下的 “\w+” 才是正则表达式的内容,其中 “\w” 代表任意字符;“+” 限定前面的 “\w” 可出现一次到多次,因此,“\w+” 代表一个或多个任意字符。由于在后面指定的 re.A 选项,这样 “\w” 就只能匹配 ASCII 字符,不能匹配汉字。

在使用 sub() 函数执行替换时正则表达式 “\w+” 所匹配的内容可以通过组名 “lang” 来获取,这样 func() 函数就调用了 matched.group('lang') 来获取 “\w+” 所匹配的内容。

(8)、re.split(pattern, string, maxsplit=0, flags=0):使用 pattern 对 string 进行分割,该函数返回分割得到的多个子串组成的列表。maxsplit 参数控制最多分割多少次,默认全部分割。

split() 函数用法示例如下:

import re
# 使用逗号对字符串进行分割
print(re.split(', ', 'python, linux, java'))
# 输出:['python', 'linux', 'java']
# 指定只分割一次,被分割成两个子串
print(re.split(', ', 'python, linux, java', 1))
# 输出:['python', 'linux, java']
# 使用 n 进行分割,未匹配成功时就不分割,比如使用 b 字符进行分割
print(re.split('n', 'python, linux, java'))
# 输出:['pytho', ', li', 'ux, java']

 

(9)、re.purge():清除正则表达式缓存。

(10)、re.excape(pattern):对模式中除 ASCII 字符、数值、下划线(_)之外的其他字符进行转义。也就是对模式中的特殊字符进行转义。

escape() 函数的用法示例如下:

import re
# 对模式中的特殊字符进行转义
print(re.escape(r'www.michael.com is very strong, I like it! 666'))
# 输出:www\.michael\.com\ is\ very\ strong\,\ I\ like\ it\!\ 666
print(re.escape(r'A-Zand0-9?'))
# 输出:A\-Zand0\-9\?

从上面代码的输出结果可知,escape() 函数对非 ASCII 字符、数值、下划线(_)之外的其他字符都进行了转义。

在 re 模块中还包含有两个类,分别是正则表达式对象(类型是 _sre.SRE_Pattern)和匹配(Match)对象。其中正则表达式对象是调用 re.compile() 函数的返回值,该对象的方法与 re 模块中的函数大致对应。但是正则表达对象的 search()、match()、fullmatch()、findall()、finditer()方法的功能要强大一些,因为这些方法可以额外指定 pos 和 endpos 两个参数,用于指定只处理目标字符串从 pos 开始到 endpos 结束之间的子串。下面代码示例用正则表达式对象的方法来执行匹配。代码如下:

import re
# 首先编译得到正则表达式对象,从类的角度来看就是在创建对象(或实例)pa
pa = re.compile('mich')
# 调用 pa 对象的 match 方法,该方法原本是从开始位置匹配的,
# 但是在这里可以指定开始匹配的位置,指定从索引 4 开始匹配
print(pa.match('www.michael.com', 4).span())    # (4, 8)
# 下面指定索引起始和结束位置,指定索引4 到 6 之间执行匹配,匹配失败
print(pa.match('www.michael.com', 4, 15))    # None
# 下面指定从索引 4 到索引 8 之间执行完全匹配,匹配成功
print(pa.fullmatch('www.michael.com', 4, 8).span())     # (4, 8)

上面代码中使用正则表达式对象来调用 match()、fullmatch()方法时指定了 pos 和 endpos 参数,这样可以只处理目标字符串的中间一段。在使用 compile() 函数编译正则表达式后,该函数所返回的对象就会缓存该正则表达式,从而可以多次调用该正则表达式执行匹配。例如上面代码多次使用了 pa 对象来执行匹配。

re 模块中的 Match 对象(类型是 _sre.SRE_Match)是 match()、search() 方法的返回值,该对象中包含了详细的正则表达式匹配信息,包括正则表达匹配的位置、正则表达式匹配的子串。

_sre.SRE_Match 对象(匹配对象)包含的方法和属性有下面这些:

(1)、match.group([group1, ...]):获取该匹配对象中指定组所匹配的字符串。

(2)、match.__getitem__(g):这是 match.group(g) 的简化写法。由于 match 对象提供了 __getitem__() 方法,因此程序可使用 match[g] 来代替 match.group(g)。

(3)、match.groups(default=None):返回 match 对象中所有组所匹配的字符串组成的元组。

(4)、match.groupdict(default=None):返回 match 对象中所有组所匹配的字符串组成的字典。

(5)、match.start([group]):获取该匹配对象中指定组所匹配的字符串的开始位置。

(6)、match.end([group]):获取该匹配对象中指定组所匹配的字符串的结束位置。

(7)、match.span([group]):获取该匹配对象中指定组所匹配的字符串的开始位置和结束位置。该方法相当于同时返回 start() 和 end() 方法的返回值。

上面7个方法都涉及到组,在正则表达式中,用圆括号将多个表达式括起来就形成组。如果正则表达式中没有圆括号,则整个表达式就属于一个默认组。下面代码示例了使用组的情形:

import re
# 在正则表达式中使用组
m = re.search(r'(?P<py>python).(?P<o>org)', r'www.python.org is official website')
print(m.group(0))       # python.org
# 调用简化的写法,底层是调用 m.__getitem__(0)
print(m[0])             # python.org
print(m.span(0))        # (4, 14)
print(m.group(1))       # python
# 调用简化的写法,底层是调用 m.__getitem__(1)
print(m[1])             # python
print(m.span(1))        # (4, 10)
print(m.group(2))       # org
# 调用简化的写法,底层是调用 m.__getitem__(2)
print(m[2])             # org
print(m.span(2))        # (11, 14)
# 返回所有组所匹配的字符串组成的元组
print(m.groups())       # ('python', 'org')
# 要返回字典的情况时,必须要为组起一个名字,名字就是字典的键
print(m.groupdict())    # {'py': 'python', 'o': 'org'}

运行代码,输出结果如下:
python.org
python.org
(4, 14)
python
python
(4, 10)
org
org
(11, 14)
('python', 'org')
{'py': 'python', 'o': 'org'}

上面代码中 search() 函数的正则表达式是 r'(?P<py>python).(?P<o>org)',这个正则表达式中有两个组,并且每个组都起了一个名字。接下来的代码中可以依次获取 group(0)、group(1)、group(2)的值,依次获取的是整个正则表达式所匹配的子串、第一个组匹配的子串、第二个组匹配的子串;也可以依次获取 span(0)、span(1)、span(2)的值,依次获取整个正则表达式所匹配子串的开始和结束位置、第一个组匹配子串的开始和结束位置、第二个组匹配子串的开始和结束位置。

只要正则表达式能匹配到结果,则不管正则表达是否包含组,group(0)、span(0)都能获取到内容。其中 group(0) 是获取整个匹配到的整个子串,span(0) 获取到的是整个子串的开始和结束位置。

在正则表达式中,使用 ?P<name> 方式为组指定名字后,就可以调用 _sre.SRE_Match 对象(匹配对象)的 groupdict() 方法获得字典,字典的 key 是为组指定的名字,value 是匹配到的单组结果。

下面继续说下 _sre.SRE_Match 对象(匹配对象)的方法:

 

(8)、match.pos:该属性返回传给正则表达式对象的 search()、match() 等方法的 pos 参数。

(9)、match.endpos:是传给正则表达式对象的 search()、match()等方法的 endpos 参数值。

(10)、match.lastindex:最后一个正则匹配的捕获组的整数索引。如果没有组匹配,该属性值是 None。例如用 (a)b、((a)(b))或 ((ab)) 对字符串 'ab' 执行匹配,该属性都会返回 1;但如果使用 (a)(b) 正则表达式对 'ab' 执行匹配,则 lastindex 等于 2。

(11)、match.lastgroup:返回最一个匹配的捕获组的名字;如果该组没有名字或根本没有组匹配,该属性返回 None。

(12)、match.re:返回执行正则表达式匹配时所用的正则表达式。

(13)、match.string:返回执行正则表达式匹配时所用的字符串。被匹配的字符串string。

 

2、 正则表达式旗标

Python 的正则表达式旗标都使用 re 模块中的属性来代表,旗标有下面这些:

(1)、re.A 或 re.ASCII:控制\w,\W,\b,\B,\d,\D,\s,\S 这些特殊符号只匹配 ASCII 字符,而不是匹配所有的 Unicode 字符。也可在正则表达式中使用 (?a) 行为旗标来代表。

(2)、re.DEBUG:显示编译正则表达式的 Debug 信息,没有行为旗标。

(3)、re.I 或 re.IGNORECASE:在匹配时不区分大小写。对应的行为旗标是 (?i)。

re.I 旗标示例如下:

# 默认要区分大小写,所以无匹配结果
>>> import re
>>> re.findall(r'pyth', 'Python is good, PYTHON is good')
[]
# 使用 re.I 旗标后,限定不区分大小写,现在有匹配结果
>>> re.findall(r'pyth', 'Python is good, PYTHON is good', re.I)
['Pyth', 'PYTH']
# 使用行为旗标 (?i) 限定不区分大小写,同样能匹配到结果
>>> re.findall(r'(?i)pyth', 'Python is good, PYTHON is good')
['Pyth', 'PYTH']
(4)、re.L 或 re.LOCALE:根据当前区域设置使用正则表达式匹配时不区分大小写。该旗标只对 bytes 模式起作用,对应的行内旗标是 (?L)。

(5)、re.M 或 re.MULTILINE:匹配多行模式的旗标。使用该旗标后,可以匹配每一行的开头,以及 “^” 符号能匹配字符串的开头;同时 “$” 符号能匹配字符串的末尾和每一行的末尾。没有该旗标的正则表达式,“^” 符号只匹配字符串的开头,“$” 符号只匹配字符串的结尾,或者匹配到字符串默认的换行符(如果有)之前。对应的行为旗标是 (?m)。

(6)、re.S 或 re.DOTALL:指定该旗标后,点(.)符号可以匹配换行符,默认不匹配换行符。行内旗标是 (?s)。

(7)、re.U 或 re.Unicode:控制\w,\W,\b,\B,\d,\D,\s,\S 这些特殊符号能匹配所有的 Unicode 字符。在 Python 3.x 版本中,默认匹配的就是所有 Unicode 字符,所以该旗标基本上没什么用。

(8)、re.X 或 re.VERBOSE:使用该旗标后,正则表达式可以换行,还可以为正则表达式添加注释,提高正则表达式可读性。对应的行内旗标是 (?x)。

re.X 旗标的用法示例如下:

import re
a = re.compile(r"""\d{3}  # 地方区号
                    \-  # 号码分界线
                    \d{8}   # 匹配8个数字""", re.X)
b = re.compile(r'\d{3}\-\d{8}')

print(a.search('你好,我成都的电话号码是:028-12345678').group())
print(b.search('你好,我首都的电话号码是:010-87654321').group())

运行代码,输出结果如下所示:
028-12345678
010-87654321

上面代码中在编译第一个正则表达式时使用 re.X 旗标,因此在表达式中可以换行,还可添加注释。将编译后的对象赋值给 a 变量,通过后面的调用语句的输出结果可以看到,该正则表达式完全没有问题。

3、 创建正则表达式

正则表达式用于匹配一批字符串,正则表达式同时也是一个特殊的字符串。在正则表达中有它可以支持的合法字符、特殊字符、预定义字符、方括号表达式、边界匹配符。这些都是有特殊用途的。

(1)、正则表达式所支持的合法字符如下表所示

字符释义
x x表示任意合法的字符
\uhhhh 十六进制值0xhhhh所表示的Unicode字符
\t 制表符('\u0009')
\n 新行(换行)符('\u000A')
\r 回车符('\u000D')
\f 换页符('\u000C')
\a 报警(bell)符('\u0007')
\e Escape符('\u001B')
\cx x对应控制符,例如,\cM匹配Ctrl+M。x值必须是A~Z或a~z之一

 

(2)、正则表达式中的特殊字符
特殊字符有特殊用途,比如反斜线(\)是转义符。要匹配这些特殊字符,需要先将这些特殊字符转义,也就是在这些特殊字符前面添加一个反斜线(\)。正则表达式中的特殊字符如下所示:

特殊字符说明
$ 匹配一行的结尾。要匹配$字符本身,就使用$(下同)
^ 匹配一行的开头
() 标记子表达式(也就是组)的开始位置和结束位置
[] 用于确定中括号表达式的开始位置和结束位置
{} 用于标记前面子表达式的出现次数,如{m,n}匹配前一个字符m到n次
* 指定前面的子表达式可以出现0次或多次
+ 指定前面的子表达至少要出现1次,也可以出现多次
? 指定前面的子表达式可以出现0次或1次
. 匹配除换行符之外的任意单个字符
\ 用于转义特殊字符,或者用于指定八进制、十六进制字符
| 指定在两项之间任选一项

 

在正则表达中,通常都需要将多个字符拼接起来。示例如下:

>>> print(re.fullmatch(r'\u0041\\', 'A\\'))     # 匹配到:A\
<_sre.SRE_Match object; span=(0, 2), match='A\\'>
>>> print(re.fullmatch(r'\u0061\t', 'a\t'))     # 匹配到:a<制表符>
<_sre.SRE_Match object; span=(0, 2), match='a\t'>
>>> print(re.search(r'\?\[', 'python?[isgood')) # 匹配到:?[
<_sre.SRE_Match object; span=(6, 8), match='?['>

有了上面的这些特殊字符,在正则表达式中还需要使用通配符,通配符是可以匹配多个字符的特殊字符。正则表达式中的通配符被称为“预定义字符”。

(3)、 正则表达式所支持的预定义字符
预定义字符有下面7个:

预定义字符说明
. 默认可以匹配除换行符之外的任意字符。可使用re.S旗标来增加匹配换行符
\d 匹配0~9的所有数字
\D 匹配非数字
\s 匹配所有的空白字符,包括空格、制表符、回车符、换页符、换行符等
\S 匹配所有的非空白字符
\w 匹配所有的单词字符,包括0~9的所有数字、26个英文字母和下划线(_)、汉字
\W 匹配所有非单词字符

 

>>> re.findall(r'c\wt', 'cat, cbt, c8t, c_t')       # 匹配结果如下所示
['cat', 'cbt', 'c8t', 'c_t']
>>> re.findall(r'\d\d\d-\d\d\d-\d\d\d\d', '400-800-1234, 8001-5553-6666')   # 匹配结果如下所示
['400-800-1234']

有了上面这些预定义字符,还需要进一步使用方括号表达式来丰富正则表达式。

(4)、方括号表达式

方括号表达式说明
表示枚举 例如[abc],表示a、b、c其中任意一个字符
表示范围 例如[a-f],表示 a~f 范围内的任意字符;[\u0041-\u0056]表示十六进制字符\u0041到\u0056范围的字符。范围可以和枚举结合使用,如[a-cx-z]表示a~c、x~z范围内的任意字符
表示求否:^ 例如abc表示非a、b、c的任意字符;a-f表示不是a~f范围的任意字符

 

方括号表达式使用灵活,几乎可以匹配任意字符。例如要匹配所有的中文字符,可以利用 [\\u0041-\\u0056] 的形式,因为所有的中文字符的 Unicode 值是连续的,只要找出所有中文字符中最小、最大的 Unicode 值,就可以利用这种形式来匹配出所有中文字符。

(5)、边界匹配符
Python 的正则表达式还有边界匹配符,可用于匹配行的开头、结尾,单词的边界等。边界匹配符如下所示:

边界匹配符说明
^ 行的开头
$ 行的结尾
\b 单词的边界,即只能匹配单词前后的空白
\B 非单词的边界,即只能匹配不在单词前后的空白
\A 只匹配字符串的开头
\Z 只匹配字符串的结尾,仅用于最后的结束符

 

4、 子表达式

正则表达式还可以使用圆括号表达式,可将多表达式组成一个子表达式,在圆括号中可以使用“或”运算符(|)。圆括号表达式也是功能丰富的用法之一。圆括号表达式,也叫子表达式(组)支持的用法如下:
(1)、(exp):匹配 exp 表达式并捕获成一个自动命名的组,后面可通过 “\1” 引用第一个捕获组所匹配的子串,通过 “\2” 引用第二个捕获组所匹配的子串,......,以此类推。示例如下:

>>> re.search(r'Windows (95|98|NT|2000)[\w ]+\1', 'Windows 98 publised in 98')
<_sre.SRE_Match object; span=(0, 25), match='Windows 98 publised in 98'>

上面代码用到的正则表达式是 r'Windows (95|98|NT|2000)[\w ]+\1';紧接着的 [\w ]+ 表达式可匹配任意单词字符和空格,方括号后面的 “+” 表示方括号可出现1次或多次;最后是 “\1” 表示引用第一个组所匹配到的子串,假如第一个匹配到的是98,则 “\1”也必须是98,因此该正则表达式可匹配 “Windows 98 publised in 98”。如果将上面代码改为如下形式就不能匹配:

>>> print(re.search(r'Windows (95|98|NT|2000)[\w ]+\1', 'Windows 98 publised in 95'))
None

从输出可看出,未能成功匹配到结果,是因为第一个组匹配到的子串是98,因此 “\1” 也必须引用子串98,所以该正则表达式不能匹配 “Windows 98 publised in 95”

(2)、(?P<name>exp):匹配 exp 表达式并捕获成命名组,该组的名字为 name。后面可通过 (?P=name) 来引用前面捕获的组。通过此处介绍可以看出,(exp) 和 (?P<name>exp) 的功能大致相同,只是 (exp) 捕获的组没有显式指定组名,因此后面只能使用\1、\2等方式来引用这种组所匹配的子串;而 (?P<name>exp) 捕获的组有名称,因此后面可通过 (?P=name) 的方式来引用命名组所匹配的子串。

(3)、(?P=name):引用 name 命名组所匹配的子串。
示例代码如下所示:

>>> re.search(r'<(?P<tag>\w+)>\w+</(?P=tag)>', '<div>hello</div>')
<_sre.SRE_Match object; span=(0, 16), match='<div>hello</div>'>

上面代码中的正则表达式是 r'<(?P<tag>\w+)>\w+</(?P=tag)>',表达式开始是 “<” 符号,表示直接匹配该符号;接下来是一个命名组:(?P<tag>\w+),组名是 tag,该组可以匹配1个或多个任意字符;紧接着后面是 “>” 符号,这部分用于匹配一个 HTML 或 XML 标签。尖括号外面的 “\w+” 用于匹配标签中的内容;后面定义的 “</” 直接匹配这两个字符;之后的 (?P=tag) 就用于引用前面的 tag 组所匹配的子串,也就是说,该正则表达式要求内容必须在合理关闭的 HTML 或 XML 标签内。例如下面代码就不能匹配:

>>> print(re.search(r'<(?P<tag>\w+)>\w+</(?P=tag)>', '<div>hello</p>'))
None

这是由于前后两个标签不相同,因此不能匹配。

(4)、(?:exp):匹配 exp 表达式并且不捕获。这种组与 (exp) 的区别就在于它是不捕获的,因此不能通过 \1、\2等方式来引用。例如下面这行代码运行时会报错:

re.search(r'Windows (?:95|98|NT|2000)[\w ]+\1', 'Windows 98 publised in 98')

将上面代码中的 \1 去掉后,表示不捕获前面匹配的组,就可以正常匹配:

>>> re.search(r'Windows (?:95|98|NT|2000)[\w ]+', 'Windows 98 publised in 98')
<_sre.SRE_Match object; span=(0, 25), match='Windows 98 publised in 98'>
# 如果不想匹配后面的数字,可将 [\w ]+ 部分修改为 [a-z ]+
>>> re.search(r'Windows (?:95|98|NT|2000)[a-z ]+', 'Windows 98 publised in 98')
<_sre.SRE_Match object; span=(0, 23), match='Windows 98 publised in '>

(5)、(?<=exp):括号中的子模式必须出现在匹配内容的左侧,但 exp 不作为匹配结果的一部分。

(6)、(?=exp):括号中的子模式必须出现在匹配内容的右侧,但 exp 不作为匹配结果的一部分。

上面两个子表达式主要用于对匹配内容进行限定,括号中的子模式本身不作为匹配的一部分。例如要获取 HMTL 代码中的<a>标签的内容。

>>> re.search(r'(?<=<a>).+?(?=</a>)', 'hello! <a>michael.com</a>! technology')
<_sre.SRE_Match object; span=(10, 21), match='michael.com'>

在上面的正则表达式中 (?<=<a>) 是一个限定组,该组的内容是 <a>,由于该组用了 (?<=exp) 声明,因此在被匹配内容的左侧必须有 <a>;后一个限定组是 (?=</a>),该组的内容是 </a>,该组用了 (?=exp) 声明,因此要求在被匹配内容的右侧必须出现 </a>。所以上面的正则表达式会将 <a> 和 </a> 之间的内容匹配出来。再例如:

>>> re.search(r'(?<=<a>).+?(?=</a>)', 'hello! <a><div>michael</div></a>! technology')
<_sre.SRE_Match object; span=(10, 28), match='<div>michael</div>'>

(7)、 (?<!exp):括号中的子模式必须不出现在匹配内容的左侧,且 exp 不作为匹配的一部分。这个是 (?<=exp) 的逆向表达。

(8)、(?!exp):括号的子模式必须不出现在匹配内容的右侧,且 exp 不作为匹配的一部分。这个是 (?=exp) 的逆向表达。

(9)、(?#comment):注释组。“?#” 后的内容是注释,不影响正则表达式本身。示例如下:

>>> re.search(r'[a-zA-Z0-9_]{3,}(?#username)@michael\.com', 'hello stark@michael.com')
<_sre.SRE_Match object; span=(6, 23), match='stark@michael.com'>

上面代码中的 (?#username) 就是注释,对正则表达式不会有什么影响,只用于对部分内容进行说明。

(10)、(?aiLmsux):旗标组,用于为整个正则表达式添加行内旗标,可同时指定一个或多个旗标。示例如下:

>>> re.findall(r'(?im)[a-z0-9]{3,}@michael\.com', 'Stark@MICHAEL.COM,\
... Tom@michael.COM')
['Stark@MICHAEL.COM', 'Tom@michael.COM']

上面代码中的 (?im) 组表示该正则表达式在匹配时不区分大小写,并且可以匹配多行。所以匹配到多个结果,并以列表形式返回结果。如果将该旗标组去掉,就不能匹配到结果。

(11)、(?imsx-imsx:exp):只对当前组起作用的旗标。该组旗标与 (?aiLmsux) 组旗标的区别是,(?aiLmsux) 组旗标作用于整个正则表达式,而这组旗标只影响组内的子表达式。示例如下:

>>> re.search(r'(?i:[a-z0-9]){3,}@michael\.com', 'Stark@michael.com')
<_sre.SRE_Match object; span=(0, 17), match='Stark@michael.com'>

上面这个表达中的 (?i:[a-z0-9]) 组表示子表达式不区分大小写,但整个表达式依然区分大小写。因此这个表达式可以匹配 Stark@michael.com,但不能匹配 Stark@Michael.com,因为后面部分依然要区分大小写。

如果在旗标前使用减号 “-”,则表明去掉该旗标。比如在执行 search() 方法时传入了 re.I 参数,这表示整个正则表达式不区分大小写;如果希望某个组内的表达式依然区分大小,则可使用 (-i:exp) 来表示。例如:

>>> re.search(r'(?-i:[a-z0-9]){3,}@michael\.com', 'stark@Michael.Com', re.I)
<_sre.SRE_Match object; span=(0, 17), match='stark@Michael.Com'>

在 search() 方法中指定了 re.I 选项,表示整个正则表达式在匹配时不区分大小写;但是又要求用户名必须区分大小写,于是就把用户名部分放在用组定义成的子表达中,并为该子表达式指定 “?-i:” 选项(表明去除 re.I 选项),这样在组内的子表达式就会区分大小写。所在上面这个表达式可以匹配 stark@Michael.com、stark@michael.com,但不能匹配 Stark@Michael.com,因为用户名是区分大小写的。

5、贪婪模式与勉强模式

正则表达可以限定频度,用于限定前面的模式可以出现的次数。Python 正则表达式支持的频度限定有下面几种:

(1)、*:限定前面的子表达可以出现 0~N 次。例如 r'zo*' 能匹配 'z',也能匹配 'zoo'、'zooo'等。这里 * 等价于 {0,}。

(2)、+:限定前面的子表达可以出现 1~N 次。例如 r'zo+' 不能匹配 'z',可匹配 'zo'、'zoo'、'zooo'等。+ 等价于 {1,}。

(3)、?:限定前面的子表达出现 0~1 次。例如 r'zo?' 能匹配 'z' 和 'zo' 两个字符串。? 等价于 {0,1}。

(4)、{m,n}:m 和 n 均为大于 0 的整数,其中 m<=n,限定前面的子表达出现 m~n 次。例如 r'fo{1,3}d' 可匹配 'fod'、'food'、'foood'这三个字符串。

(5)、{n,}:n 是一个大于0的整数,限定前面的子表达式至少出现 n 次。例如 r'fo{2,}d' 可匹配 'food'、'foood'、'fooood'等字符串。

(6)、{,m}:m 是一个大于0的整数,限定前面的子表达最多出现 m 次。例如 r'fo{,3}d' 可匹配 'fd'、'fod'、'food'、'foood' 这四个字符串。

(7)、{n}:n 是一个大于0的整数,限定前面的子表达必须出现 n 次。例如 r'fo{2}d' 只能匹配 'food' 字符串。

在默认情况下,正则表达式的频度限定是贪婪模式的。贪婪模式指的是表达式中的模式会尽可能多的匹配字符。示例如下:

>>> re.search(r'@.+\.', 'stark@michael.com.cn')
<_sre.SRE_Match object; span=(5, 18), match='@michael.com.'>

上面的正则表达式 r'@.+\.' 是匹配 @ 符号和点(.)号之间的全部内容,希望匹配的结果是 “@michael.”。但是由于在@和点号之间用的是 “.+” 表示匹配任意字符,而且此时是贪婪模式,因此 “.+” 会尽可多的进行匹配,只要最后有一个 “.” 结尾即可,所以匹配到的结果是 “@michael.com.”

只要在频度限定之后添加一个英文问号贪婪模式就变成了勉强模式,勉强模式指的是表达式中的模式会尽可能少的匹配字符。示例如下:

>>> re.search(r'@.+?\.', 'stark@michael.com.cn.')
<_sre.SRE_Match object; span=(5, 14), match='@michael.'>

这次将正则表达式中间部分由 “.+” 改为 “.+?” 就成了勉强模式。该模式会尽可能少的匹配字符,只要它最后有一个“.”结尾即可,因此匹配结果是 '@michael.'。

posted @ 2020-01-07 16:09  远方那一抹云  阅读(495)  评论(0编辑  收藏  举报