python进阶(20) 正则表达式的超详细使用
正则表达式
正则表达式(Regular Expression
,在代码中常简写为regex
、 regexp
、RE
或re
)是预先定义好的一个“规则字符率”,通过这个“规则字符串”可以匹配、查找和替换那些符合“规则”的文本。
虽然文本的查找和替換功能可通过字符串提供的方法实现,但是实现起来极为困难,而且运算效率也很低。而使用正则表达式实现这些功能会比较简单,而且效率很高,唯一的困难之处在于编写合适的正则表达式。
Python 中正则表达式应用非常广泛,如数据挖掘、数据分析、网络爬虫、输入有效性验证等,Python 也提供了利用正则表达式实现文本的匹配、查找和替换等操作的 re
模块。
1.1 正则表达式字符串
正则表达式是一种字符串,正则表达式字符串是由普通字符和元字符组成的。
1)普通字符
普通字符是按照字符字面意义表示的字符。
2)元字符
元字符是预先定义好的一些特定字符,比如\w
、\.
都属于元字符。
1.1.1 元字符
元字符(Metacharacters)是用来描述其他字符的特殊字符,它由基本元字符和普通字符构成。基本元字符是构成元字符的组成要素。基本元字符主要有14个,具体如下图所示。
字符 | 说明 |
---|---|
\ | 转义符,表示转义 |
. | 表示任意一个字符 |
+ | 表示重复1次或多次 |
* | 表示重复0次或多次 |
? | 表示重复0次或1次 |
` | ` |
{} | 定义量词 |
[] | 定义字符类 |
() | 定义分组 |
^ | 可以表示取反,或匹配一行的开始 |
$ | 匹配一行的结束 |
上面表格中\w+
是元字符,它由两个基本元字符(\
和+
)和一个普通字符 w
构成。另外,还有.
元字符,它由两个基本元字符\
和,
构成。
学习正则表达式某种意义上讲就是在学习元字符的使用
,元字符是正则表达式的重点也是难点。下面会分门别类地介绍元字符的具体使用。
1.1.2 字符转义
在正则表达式中有时也需要字符转义,比如 w字符
不表示英文字母 w,而是表示任何语言的单词字符(如英文字母、亚洲文字等)、数字和下画线等内容时,需要在w
字母前加上反斜杠\
。反斜杠\
也是基本元字符,与 Python
语言中的字符转义是类似的。不仅可以对普通字符进行转义,还可以对基本元字符进行转义。如上面的表格,其中点.
字符是希望按照点.
的字面意义使用,作为.com
域名的一部分,而不是作为.
基本元字符使用,所以需要加反斜杠\
进行转义,即\.
才是表示点.
的字面意义。
1.1.3 开始与结束字符
本节通过一个示例介绍在 Python中如何使用正则表达式。
在1.1.1 节介绍基本元字符时介绍了^
和$
,它们可以用于匹配一行字符串的开始和结束。当以^
开始时,要求一行字符串的开始位置匹配:当以$
结束时,要求一行字符串的结位置匹配。所以正则表达式\w+@jiakecong.com
和^w+@jiakecong.com$
是不同的。
示例代码如下:
import re
p1 = r'\w+@jiakecong\.com'
p2 = r'^\w+@jiakecong\.com$'
text = "Tony 's email is tony_guan111@jiakecong.com"
m = re.search(p1, text)
print(m)
m = re.search(p2, text)
print(m)
email = "tony_guan111@jiakecong.com"
m = re.search(p2, email)
print(m)
输出结果如下:
<re.Match object; span=(17, 43), match='tony_guan111@jiakecong.com'>
None
<re.Match object; span=(0, 26), match='tony_guan111@jiakecong.com'>
1..2 字符类
在正则表达式中可以使用字符类,一个字符类定义一组字符,其中的任一一个字符出现在输入字符串中即匹配成功。注意每次匹配只能匹配字符类中的一个字符。
1.2.1 定义字符类
定义一个普通的字符类需要使用[
和]
元字符类。例如想在输入字符串中匹配Java
或java
,可以使用正则表达式[Jj]ava
,示例代码如下:
p = r'[Jj]ava'
m = re.search(p, 'I like Java and Python')
print(m)
m = re.search(p, 'I like JAVA and Python')
print(m)
m = re.search(p, 'I like java and python')
print(m)
输出结果如下:
<re.Match object; span=(7, 11), match='Java'>
None
<re.Match object; span=(7, 11), match='java'>
上述代码中除了JAVA
不匹配正则表达式[Jj]ava
,其他都匹配
1.2.2 字符串取反
在正则表达式中指定不想出现的字符,可以在字符类前加^
符号。示例代码如下:
import re
p = r'[^0123456789]'
m = re.search(p, '1000')
print(m)
m = re.search(p, 'python')
print(m)
上述代码定义的正则表达式[^0123456789]
,它表示输入字符串中出现非0-9数字即匹配,即出现在[0123456789]
以外的任意一字符即匹配
1.2.3 区间
上面示例中的[^0123456789]
正则表达式,事实上有些麻烦,这种连续的数字可以使用区间表示。区间是用连字符-
表示的,例如[0123456789]
采用区间表示为[0-9]
,[^0123456789]
采用区间表示为[^0-9]
。区间还可以表示连续的英文字母字符类,例如[a-z]
表示所有小写字母字符类,[A-Z]
表示所有大写字母字符类。
另外,也可以表示多个不同区间,[A-Za-z0-9]
表示所有字母和数字字符类,[0-25-7]
表示0、1、2、5、6、7几个字符组成的字符类。
示例代码如下:
import re
m = re.search('[A-Za-z0-9]', 'A10.3')
print(m)
m = re.search(r'[0-25-7]', 'A3489C')
print(m)
输出结果如下:
<re.Match object; span=(0, 1), match='A'>
None
1.2.4 预定义字符类
有些字符类很常用,例如[0-9]
等。为了书写方便,正则表达式提供了预定义的字符类,例如预定义字符类\d
等价于[0-9]
字符类。预定义字符类如下图所示
字符 | 说明 |
---|---|
. | 匹配任意一个字符 |
\ | 匹配反斜杠\字符 |
\n | 匹配换行 |
\r | 匹配回车 |
\f | 匹配一个换页符 |
\t | 匹配一个水平制表符 |
\v | 匹配一个垂直制表符 |
\s | 匹配一个空格符,等价于[\t\n\r\f\v] |
\S | 匹配一个非空格符,等价于[^\s] |
\d | 匹配一个数字字符,等价于[0-9] |
\D | 匹配一个非数字字符,等价[^0-9] |
\w | 匹配任意语言的单词字符、数字和下划线'_'等字符,如果正则表达式标志设置为ASCII,则只匹配[a-zA-Z0-9] |
\W | 等价于[^\w] |
示例代码如下:
import re
p = r'\D'
m = re.search(p, 'assss')
print(m)
m = re.search(p, '1000')
print(m)
text = '你们好hello'
m = re.search(r'\w', text)
print(m)
输出结果如下:
<re.Match object; span=(0, 1), match='a'>
None
<re.Match object; span=(0, 1), match='你'>
上述代码正则表达式\D
就等于[^0123456789]
。另一个正则表达式\w
表示任意字符,会在text
字符串中查找匹配字符,找到的结果是你
字符。
1.3 量词
之前学习的正则表达式元字符只能匹配显示一次字符或字符串,如果想匹配显示多次字符或字符串可以使用量词
1.3.1 量词的使用
量词表示字符或字符串重复的次数,正则表达式中的量词如下表:
字符 | 说明 |
---|---|
? | 出现0或1次 |
* | 出现0或多次 |
+ | 出现1或多次 |
出现n次 | |
至少出现n次,但不超过m次 | |
至少出现n次 |
量词的使用示例代码如下:
import re
m = re.search(r'\d?', '87654321')
print(m)
m = re.search(r'\d?', 'ABC')
print(m)
m = re.search(r'\d*', '87654321')
print(m)
m = re.search(r'\d*', 'ABC')
print(m)
m = re.search(r'\d+', '87654321')
print(m)
m = re.search(r'\d+', 'ABC')
print(m)
m = re.search(r'\d{8}', '87654321')
print(m)
m = re.search(r'\d{8}', 'ABC')
print(m)
m = re.search(r'\d{7,8}', '87654321')
print(m)
m = re.search(r'\d{9, }', '87654321')
print(m)
输出结果如下:
<re.Match object; span=(0, 1), match='8'>
<re.Match object; span=(0, 0), match=''>
<re.Match object; span=(0, 8), match='87654321'>
<re.Match object; span=(0, 0), match=''>
<re.Match object; span=(0, 8), match='87654321'>
None
<re.Match object; span=(0, 8), match='87654321'>
None
<re.Match object; span=(0, 8), match='87654321'>
None
1.3.2 贪婪量词和懒惰量词
量词还可以细分为贪婪量词和懒惰量词,贪婪量词会尽可能多地匹配字符,懒惰量词会尽可能少地匹配字符。大多数计算机语言的正则表达式量词默认是贪婪的,要想使用懒惰量词在量词后面加?
即可
示例代码如下:
import re
m = re.search(r'\d{5,8}', '87654321')
print(m)
m = re.search(r'\d{5,8}?', '87654321')
print(m)
输出结果如下:
<re.Match object; span=(0, 8), match='87654321'>
<re.Match object; span=(0, 5), match='87654'>
上述代码使用了贪婪量词{5,8}
,输入字符串87654321
是长度8位的数字字符串,尽可能多地匹配字符结果是87654321
。代码使用懒惰量词{5,8}?
,输入字符串87654321
是长度8位的数字字符串,尽可能少的匹配字符结果是87654
。
1.4 分组
在此之前学习的量词只能重复显示一个字符,如果想让一个字符串作为整体使用量词,可将整个字符串放到一对小括号中,这就是分组(也称子表达式)
1.4.1 分组的使用
对正则表达式进行分组不经可以对一个字符串整体使用量词,还可以在正则表达式中引用已经存在的分组。示例代码如下:
import re
p = r'(121){2}'
m = re.search(p, '121121abcabc')
print(m)
print(m.group()) # 返回匹配的字符串
print(m.group(1)) # 返回第一组内容
p = r'(\d{3,4})-(\d{7,8})'
m = re.search(p, '010-87654321')
print(m)
print(m.group()) # 返回匹配字符串
print(m.groups()) # 获得所有组内容
输出结果如下:
<re.Match object; span=(0, 6), match='121121'>
121121
121
<re.Match object; span=(0, 12), match='010-87654321'>
010-87654321
('010', '87654321')
上述代码定义的正则表达式(121)是将121字符串分为一组,(121){2}表示对121重复两次,即121121。代码调用match
对象的group()
方法返回匹配的字符串,group()
方法语法如下:
match.group([group1, ...])
其中参数group1
是组编号,在正则表达式中组编号是从1开始的,所以代码正则表达式m.group(1)
表示返回第一组内容
代码 r'(\d{3,4})-(\d{7,8})'
正则表达式可以用来验证固定电话号码,在-
之前是3-4位的区号,-
之后是7-8位的电话号码。在该正则表达式中有两个分组。代码m.groups()
方法是返回所有分组,返回值是一个元组
1.4.2 分组命名
在Python程序中访问分组时,除了可以通过组编号进行访问,还可以通过组名进行访问,前提是要在正则表达式中为组命名。组命名通过在组开头添加?P<分组名>
实现。
示例代码如下:
import re
p = r'(?P<area_code>\d{3,4})-(?P<phone_code>\d{7,8})'
m = re.search(p, '010-87654321')
print(m)
print(m.group()) # 返回匹配字符串
print(m.groups()) # 获得所有组内容
# 通过组编号返回组内容
print(m.group(1))
print(m.group(2))
# 通过组名返回组内容
print(m.group('area_code'))
print(m.group('phone_code'))
输出结果如下:
<re.Match object; span=(0, 12), match='010-87654321'>
010-87654321
('010', '87654321')
010
87654321
010
87654321
上述代码其实和1.4.1的代码是一样的,只是给正则表达式命名了,以后就可以通过组编号或组名字来访问
1.4.3 反向引用分组
除了可以在程序diamante中访问正则表达式匹配之后的分组内容,还可以再正则表达式内部引用之前的分组。
下面通过示例熟悉以下反向引用分组。假设由于工作需要想解析一段XML代码,需要找到某一个开始标签和结束标签,示例代码如下:
import re
p = r'<([\w]+)>.*</([\w]+)>'
m = re.search(p, '<a>abc</a>')
print(m)
p = r'<([\w]+)>.*</([\w]+)>'
m = re.search(p, '<a>abc</b>')
print(m)
输出结果如下:
<re.Match object; span=(0, 10), match='<a>abc</a>'>
<re.Match object; span=(0, 10), match='<a>abc</b>'>
上述代码的正则表达式分成了两组,两组内容完全一样。但是测试结果发现他们都是匹配的,但是<a>abc</b>
明显不是有效的XML代码,因为开始标签和结束标签应该是一致的。可见代码r'<([\w]+)>.*</([\w]+)>'
并不能保证开始标签和结束标签是一致的。为了解决此问题,可以引用反向引用,即让第二组反向引用第一组。在正则表达式中反向引用语法是\组编号
,组编号是从1开始的。示例代码如下:
import re
p = r'<([\w]+)>.*</\1>' # 使用了反向引用 ①
m = re.search(p, '<a>abc</a>')
print(m) # 匹配
m = re.search(p, '<a>abc</b>')
print(m) # 不匹配
输出结果如下:
<re.Match object; span=(0, 10), match='<a>abc</a>'>
None
上述代码第①行时定义正则表达式,其中\1
是反向引用第一个组,从运行结果可见字符串<a>abc</a>
是匹配的,而<a>abc</b>
字符串不匹配
1.4.4 非捕获分组
前面介绍的分组称为捕获分组。捕获分组的匹配子表达式结果被暂时保存到内存中,以备表达式或其他程序引用,这个过程称为"捕获",捕获结果可以通过组编号或组名进行引用。但是有时并不想引用子表达式的匹配结果,不想捕获匹配结果,只是将小括号作为一个整体进行匹配,此时可以使用非捕获分组,在组开头使用?
,可以实现非捕获分组
示例代码如下:
import re
s = 'img1.jpg,img2.jpg,img3.bmp'
# 捕获分组
p = r'\w+(\.jpg)'
mlist = re.findall(p, s) ①
print(mlist)
# 非捕获分组
p = r'\w+(?:\.jpg)'
mlist = re.findall(p, s) ②
print(mlist)
输出结果如下:
['.jpg', '.jpg']
['img1.jpg', 'img2.jpg']
上述代码实现了从字符串中查找.jpg
结尾的文本,其中代码第①行和第②行的正则表达式区别在于前者是捕获分组,后者是非捕获分组。捕获分组将括号中的内容作为子表达式进行捕获匹配,将匹配的子表达式(即组的内容)返回,结果是['.jpg','.jpg']
。而非捕获分组将括号中的内容作为普通的正则表达式字符串进行整体匹配,即找到.jpg
结尾的文本,所以最后结果是['img1.jpg', 'img2.jpg']
。
1.5 re模块
re
是Python
内置的正则表达式模块,前面虽然使用过re
模块一些函数,但还有很多重要函数没有详细介绍,这一节将详细介绍这些函数
1.5.1 search()和match()函数
search()
和match()
函数非常相似,它们的区别如下所示
search()
:在输入字符串中查找,返回第一个匹配内容,如果找到一个则match
对象,如果没有找到返回None
match()
:在输入字符串开始处查找匹配内容,如果找到一个则match
对象,如果没有找到返回None
示例代码如下:
import re
p = r'\w+@jiakecong\.com'
text = "Tony 's email is tony_guan111@jiakecong.com" ①
m = re.search(p, text)
print(m)
m = re.match(p, text)
print(m)
email = 'tony_guan111@jiakecong.com' ②
m = re.search(p, email)
print(m)
m = re.match(p, email)
print(m)
# match对象几个方法
print('match对象几个方法:') ③
print(m.group())
print(m.start())
print(m.end())
print(m.span())
输出结果如下:
<re.Match object; span=(17, 43), match='tony_guan111@jiakecong.com'>
None
<re.Match object; span=(0, 26), match='tony_guan111@jiakecong.com'>
<re.Match object; span=(0, 26), match='tony_guan111@jiakecong.com'>
match对象几个方法:
tony_guan111@jiakecong.com
0
26
(0, 26)
上述代码第①行输入字符串开头不是email
,search()
函数可以匹配成功,而match()
函数却匹配失败。代码第②行输入字符串开头就是email格式的邮箱,所以search()
和match()
函数都可以匹配成功
search
和match()
函数如果匹配成功都返回match
对象。match
对象有一些常用方法,见代码第③行。其中group()
方法返回匹配的子字符串;start()
方法返回子字符串的开始索引;end()
方法返回子字符串的结束索引;span
方法返回子字符串的跨度,它是一个二元素的元组。
1.5.2 findall()和finditer()函数
findall()
和finditer()
函数非常相似,它们的区别如下所示
- findall():在输入字符串中查找所有匹配内容,如果匹配成功,则返回
match
列表对象,如果匹配失败则返回None
- finditer():在输入字符串中查找所有匹配内容,如果匹配成功,则返回容纳
match
的可迭代对象,通过迭代对象每次可以返回一个match
对象,如果匹配失败则返回None
示例代码如下:
import re
p = r'[Jj]ava'
text = 'I like Java and java'
match_list = re.findall(p, text) ①
print(match_list)
match_iter = re.finditer(p, text) ②
for m in match_iter: ③
print(m.group())
输出结果如下:
['Java', 'java']
Java
java
上述代码第①行的findall()
函数返回match
列表对象。代码第②行的finditer()
函数返回可迭代对象。代码第③行通过for
循环遍历可迭代对象
1.5.3 字符串分割
字符串分割使用split
函数,该函数按照匹配的子字符串进行字符串分割,返回字符串列表对象
re.split(pattern, string, maxsplit=0, flags=0)
其中参数pattern
是正则表达式;参数string
是要分割的字符串;参数maxsplit
是最大分割次数,maxsplit
默认值为零,表示分割次数没有限制;参数flags
是编译标志
示例代码如下:
import re
p = r'\d+'
text = 'AB12CD34EF'
clist = re.split(p, text) ①
print(clist)
clist = re.split(p, text, maxsplit=1) ②
print(clist)
clist = re.split(p, text, maxsplit=2) ③
print(clist)
输出结果如下:
['AB', 'CD', 'EF']
['AB', 'CD34EF']
['AB', 'CD', 'EF']
上述代码调用split()
函数通过数字对AB12CD34EF
字符串进行分割,\d+
正则表达式匹配一到多个数字。代码第①行split()
函数中参数maxsplit
和flags
是默认的,分割的次数没有限制,分割结果是['AB', 'CD', 'EF']
列表
代码第②行split()
函数指定maxsplit
为1,分割结果是['AB', 'CD34EF']
列表,列表元素的个数是maxsplit+1
。
代码第③行split()
函数指定maxsplit
为2,2是最大可能的分割次数,因此maxsplit≥2
与maxsplit=0
是一样的。
1.5.4 字符串替换
字符串替换使用sub()
函数,该函数用于替换匹配的子字符串,返回值是替换之后的字符串。
re.sub(pattern, rep1, string, count=0, flags=0)
其中参数pattern
是正则表达式;参数rep1
是替换字符串;参数string
是要提供的字符串;参数count
是要替换的最大数量,默认值为零,表示替换数量没有限制;参数flags
是编译标志
示例代码如下:
import re
p = r'\d+'
text = 'AB12CD34EF'
replace_text = re.sub(p, ' ', text) ①
print(replace_text)
replace_text = re.sub(p, ' ', text, count=1) ②
print(replace_text)
replace_text = re.sub(p, ' ', text, count=2) ③
print(replace_text)
输出结果如下:
AB CD EF
AB CD34EF
AB CD EF
上述代码调用sub()
函数替换AB12CD34EF
字符串中的数字。代码第①行sub()
函数中参数count
和flags
都是默认的,替换的最大数量没有限制,替换结果是AB CD EF
代码第②行sub()
函数指定count
为1,替换结果是AB CD34EF
代码第③行sub()
函数指定count
为2,2是最大可能的替换次数,因此count≥2
与count=0
是一样的。
1.6 编译正则表达式
上面所有介绍的正则表达式内容足以开发实际项目了,但是为了提高效率,还可以对Python正则表达式进行编译。编译的正则表达式可以重复使用,这样能减少正则表达式的解析和验证,提高效率
在re
模块中的compile()
函数可以编译正则表达式,compile()
函数语法如下:
re.compile(pattern[, flags=0])
其中参数pattern
是正则表达式,参数flags
是编译标志。compile()
函数返回一个编译的正则表达式对象regex
1.6.1 已编译正则表达式对象
正则表达式方法需要一个已编译的正则表达式对象才能调用,这些方法与re模块函数功能类似。
实际例子如下:
import re
p = r'\w+@jkc\.com'
regex = re.compile(p)
text = "Tony's email is tony_g588@jkc.com"
m = regex.search(text)
print(m) # <re.Match object; span=(16, 33), match='tony_g588@jkc.com'>
m = regex.match(text)
print(m) # None
p = r'[Jj]ava'
regex = re.compile(p)
text = "I like Java and java"
match_list = regex.findall(text)
print(match_list) # ['Java', 'java']
match_iter = regex.finditer(text)
for m in match_iter:
print(m.group()) # Java java
p = r'\d+'
regex = re.compile(p)
text = "AB12CD34EF"
clist = regex.split(text)
print(clist) # ['AB', 'CD', 'EF']
repace_text = regex.sub(' ', text)
print(repace_text) # AB CD EF
1.6.2 编译标志
compile()函数编译正则表达式对象时,还可以设置编译标志。编译标志可以改变正则表达式引擎行为
ASCII和Unicode
之前介绍过预定义字符类\w
和\W
,其中\w
匹配单词字符,在Python2
中是ASCII编码,在Python3
中则是Unicode
编码,所以包含任何语言的单词字符。可以通过编译标志re.ASCII(或re.A)
设置采用ASCII
编码,通过编译标志re.UNICODE(re.U)
设置采用Unicode
编码
示例如下:
text = '你好hello'
p = r'\w+'
regex = re.compile(p, re.U)
m = regex.search(text)
print(m) # <re.Match object; span=(0, 7), match='你好hello'>
regex = re.compile(p, re.A)
m = regex.search(text)
print(m) # <re.Match object; span=(2, 7), match='hello'>
re.U
可以匹配中文字符,而re.A
只能匹配ASCII编码
忽略大小写
默认情况下正则表达式引擎对大小写是敏感的,但有时在匹配过程中需要忽略大小写,可以通过编译标志re.IGNORECASE(或re.I)
实现
p = r'(java).*(python)'
regex = re.compile(p, re.I)
m = regex.search('I like Java and Python')
print(m)
m = regex.search('I like JAVA and PYTHON')
print(m)
m = regex.search('I like java and python')
print(m)