使用正则表达式可以完成比较复杂的文本查找和替换任务,因为内容相对较多,这里单独作为一节进行介绍。
在Python中使用正则表达式
在Python中,使用re模块提供的函数,可以直接用指定的正则表达式对给定文本进行字符串的搜索、替换和分割等,也可以先创建正则表达式对象,然后使用该对象的属性和方法来实现。搜索的结果以匹配对象的形式返回,可以利用该对象提供的属性和方法作进一步的显示和处理。
re模块
在Python中使用re模块实现正则表达式的应用。该模块提供了一系列函数,使用它们可以实现不同形式的文本查找、替换和分割等功能。
查找
re模块提供了4个函数来实现不同形式的查找功能,即match, search, findall和finditer函数,前两个函数返回一个满足要求的匹配对象,后两个返回所有满足要求的匹配对象。使用re模块,首先要导入该模块:
import re
- re.match函数
re.match函数从给定文本打头的位置开始匹配,如果匹配不成功,返回None。该函数的语法格式为:
re.match(pattern, string, flags=0)
其中各参数的含义如表3-1中所示。
表3-1 re.match函数的参数
| 参 数 | 说 明 |
|---|---|
| pattern | 进行匹配的正则表达式 |
| string | 给定的文本 |
| flags | 标记,指定正则表达式的匹配方式如是否区分大小写等 |
flags参数用来指定正则表达式的匹配方式,其取值如表3-2中所示。如果同时设置多个标记,标记之间可以用竖线连接,如re.M|re.I。
表3-2 标记设置
| 标记 | 完整写法 | 说 明 |
|---|---|---|
| re.I | re.IGNORECASE | 不区分大小写 |
| re.M | re.MULTILINE | 支持多行 |
| re.S | re.DOTALL | 使点做任意匹配,包括换行符在内的任意字符 |
| re.L | re.LOCALE | 进行本地化识别匹配 |
| re.U | re.UNICODE | 根据Unicode字符集解析字符 |
| re.X | re.VERBOSE | 支持更灵活更详细的模式,比如多行、忽略空白、加入注释等 |
如果匹配成功,re.match函数返回一个Match对象,否则返回None。
下面给定字符串和匹配规则,用re.match函数进行匹配。
图3-22所示工作表中,A2单元格中给定一个文本,现在要求用re.match函数,用子文本”abc”进行匹配。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.match('abc',a)
单击键盘上的Ctrl+Enter键,返回一个Match对象,查看它的字符串结果如图3-22工作表中单元格C2所示,找到了匹配的字符串”abc”,位置是第1个字符到第3个字符。
图3-22 使用re.match函数
如果给定的字符串和匹配字符串有大小写区分,例如A4单元格中的字符串英文字母有大小写区分,仍然用上面的代码进行匹配。
单元格B4中在Python模式下在公式栏输入下面代码:
import re
b=xl("A4")
re.match('abc',b)
单击键盘上的Ctrl+Enter键,B4单元格返回为空,表示匹配不成功。
如果不区分大小写,使用re.I标记。单元格B6中在Python模式下在公式栏输入下面代码:
import re
c=xl("A4")
re.match('abc',c,re.I)
单击键盘上的Ctrl+Enter键,现在匹配成功,返回匹配结果"Abc"。
- re.search函数
跟re.match函数不同,re.search函数在整个给定的字符串中进行查找,并返回第一个匹配成功的对象。该函数的语法格式为:
re.search(pattern, string, flags=0)
函数各参数的说明与re.match函数的相同,请参阅。
图3-23所示工作表中,A2单元格中给定一个字符串,要求在整个字符串中查找"def",不区分大小写,返回查找到的第1个结果。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.search('def',a,re.I)
单击键盘上的Ctrl+Enter键,匹配成功,返回包含匹配结果"dEf"的Match对象。
图3-23 使用re.search函数
- re.findall函数
re.findall函数在给定字符串中查找正则表达式所匹配的所有子字符串,并将结果以列表的形式返回,如果匹配不成功,返回空列表。
注意:re.match和re.search函数只匹配一次,re.findall函数则找出所有匹配结果。
re.findall函数的语法格式为:
findall(pattern, string,flags=0)
其中,各参数的意义与re.match函数的相同,请参阅。
图3-24所示工作表中,A2单元格中给定一个字符串,要求在整个字符串中查找"abc",不区分大小写,返回查找到的所有结果。
单元格C2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.findall('abc',a,re.I)
单击键盘上的Ctrl+Enter键,匹配成功,结果用列表的形式给出。显示该列表的内容,如图3-24所示。
图3-24 使用re.findall函数
- re.finditer函数
跟re.findall函数一样,re.finditer函数也是在给定字符串中找到正则表达式所匹配的所有子字符串。不同的是,前者将匹配结果以列表的形式给出,后者把匹配结果以迭代器的形式返回。该函数的语法格式为:
re.finditer(pattern, string, flags=0)
其中,各参数的意义与re.match函数的相同,请参阅。
图3-25所示工作表中,A2单元格中给定一个字符串,要求在整个字符串中查找"abc",不区分大小写,返回查找到的所有结果。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.finditer('abc',a,re.I)
单击键盘上的Ctrl+Enter键,匹配成功,返回一个callable_iterator对象。
单元格B4中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.finditer('abc',a,re.I)
lst=[]
for i in m:
lst.append(i)
lst
单击键盘上的Ctrl+Enter键,匹配成功,结果用列表的形式给出。列表中为所有包含匹配内容的Match对象。
图3-25 使用re.finditer函数
2) 替换
所谓替换,是找到要替换的对象后用给定的对象进行替换。使用re.sub函数和re.subn函数进行替换。
- re.sub函数
函数的语法格式为:
re.sub(pattern, repl, string, count=0, flags=0)
其中各参数的意义为:
pattern - 进行匹配的正则表达式。
repl - 用作替换的字符串,可以是一个函数。
string – 给定的原始字符串。
count – 进行替换的最大次数,默认0表示全部替换。
flags - 指定正则表达式匹配方式的标记。
图3-26所示工作表中,A2单元格中给定一个字符串,要求在整个字符串中查找"abc",不区分大小写,匹配结果全部替换为"xyz"。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.sub('abc','xyz',a,0,re.I)
单击键盘上的Ctrl+Enter键,结果如图3-26中所示,替换成功。
图3-26 使用re.sub函数
- re.subn函数
re.subn函数的作用与re.sub函数的相同,只是该函数的返回值是一个元组,元组有两个值,第一个值为实现替换后的字符串,第二个值为进行替换的次数。
该函数的语法格式为:
subn(pattern, repl, string, count=0, flags=0)
其中各参数的意义与re.sub函数的相同,请参阅。
图3-26所示工作表中,A2单元格中给定一个字符串,要求在整个字符串中查找"abc",不区分大小写,匹配结果全部替换为"xyz",使用re.subn函数。
单元格B4中在Python模式下在公式栏输入下面代码:
import re
b=xl("A2")
re.subn('abc','xyz',b,2,re.I)
单击键盘上的Ctrl+Enter键,结果如图3-26中所示,替换成功且只有前两个”abc”被替换。
分割
re.split函数将能够匹配到的子字符串作为分隔符分割原始字符串,将分割后的结果以列表形式返回。该函数的语法格式为:
re.split(pattern, string[, maxsplit=0, flags=0])
其中各参数的意义为:
pattern - 进行匹配的正则表达式。
string – 给定的原始字符串。
maxsplit – 最大分割次数,默认值为0,表示不限次数。
flags - 指定正则表达式匹配方式的标记。
图3-27所示工作表中,A2单元格中给定一个字符串,要求在整个字符串中查找"&",找到后用作分隔符对原始字符串进行分割,返回分割的结果。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.split('&',a)
单击键盘上的Ctrl+Enter键,返回一个列表,分割后的字符串称为该列表中的一个元素。显示列表的内容,如图3-27所示。
单元格D2中在Python模式下在公式栏输入下面代码:
import re
b=xl("A2")
re.split('&',b,2)
单击键盘上的Ctrl+Enter键,同样返回一个列表,但是列表中只有3个元素,因为代码中限定了分割次数为2次。
图3-27 使用re.split函数
Match对象
Match对象是通过re.match, re.search和re.finditer函数返回的,是进行匹配的结果,包含了与匹配有关的很多信息。使用该对象的属性和方法可以获取这些信息。
Match对象的属性
Match对象的属性提供很多跟匹配有关的信息,包括原始字符串、匹配表达式、匹配时指定的起始位置和终止位置等。
Match对象的属性包括:
string属性 – 原始字符串。
re属性 – 正则表达式。
pos属性 - 开始搜索位置的索引。
endpos属性 - 结束搜索位置的索引。
lastindex属性 - 最后一个捕获分组的索引。如果没有捕获分组,返回None。
lastgroup属性 - 最后一个捕获分组的别名。如果这个分组没有别名或者没有捕获分组,返回None。
图3-28所示工作表中,A2单元格中给定一个字符串,要求用re.search函数查找到匹配的Match对象,查看它的属性值。使用正则表达式"(\d+)(\w+)"进行查找,其中有两个分组,第1个分组由数字组成,第2个分组为字母、数字等组成。这里先不必弄懂正则表达式怎么写,后面会详细介绍语法规则,这里主要介绍给定一个正则表达式怎么用。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.search(r"(\d+)(\w+)",a)
单击键盘上的Ctrl+Enter键,返回一个Match对象m。单元格B3中在Python模式下在公式栏输入下面代码:
m.string
单击键盘上的Ctrl+Enter键,返回m的原始字符串。单元格B4中在Python模式下在公式栏输入下面代码:
m.re
单击键盘上的Ctrl+Enter键,返回m的匹配表达式。单元格B5中在Python模式下在公式栏输入下面代码:
m.pos
单击键盘上的Ctrl+Enter键,返回进行匹配的起始位置的索引号。单元格B6中在Python模式下在公式栏输入下面代码:
m.endpos
单击键盘上的Ctrl+Enter键,返回进行匹配的终止位置的索引号。单元格B7中在Python模式下在公式栏输入下面代码:
m.lastindex
单击键盘上的Ctrl+Enter键,返回最后一个捕获分组的索引号。单元格B8中在Python模式下在公式栏输入下面代码:
m.lastgroup
单击键盘上的Ctrl+Enter键,返回最后一个捕获分组的别名,这里没有。
图3-28 Match对象的属性
Match对象的方法
使用Match对象的方法可以获取匹配对象中各分组的详细信息,包括各分组的内容、起始位置、终止位置和范围等。
Match对象的方法包括:
group([group1, …]) - 获得一个或多个分组捕获的字符串,指定多个分组时结果以元组形式返回。group1可以使用索引号也可以使用别名。索引号0代表匹配的整个子字符串;不填写参数时,返回group(0)。没有捕获到字符串的分组返回None,捕获了多次的组返回最后一次捕获的子字符串。
groups([default]) - 以元组形式返回全部分组捕获的字符串。default表示没有捕获到字符串的分组以这个值替代,默认为None。
groupdict([default]) - 返回一个字典,该字典以有别名的分组的别名为键、以该分组捕获的字符串为值。没有别名的分组不包含在内。default含义同上。
start([group]) - 返回指定的分组捕获的子字符串在string中的起始位置索引号。group默认值为0。
end([group]) - 返回指定的分组捕获的子字符串在string中的结束位置索引号(子字符串最后一个字符的索引号+1)。group默认值为0。
span([group]) - 返回捕获的子字符串的起始位置索引号和终止位置索引号组成的元组,即(start(group), end(group))。
expand(template) - 将匹配到的分组代入template中然后返回。template中可以使用\id或\g<id>, \g<name>引用分组,但不能使用编号0。\id与\g<id>是等价的,用\g<1>0表示\1之后是字符"0 "。
下面给定原始字符串和匹配正则表达式,用re.search函数获取Match对象和它的属性值。正则表达式中有两个分组,第1个分组由数字组成,第2个分组为字母、数字等组成。
图3-29所示工作表中,A2单元格中给定一个字符串,要求用re.search函数查找到匹配的Match对象,调用和测试它的方法。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.search(r"(\d+)(\w+)",a)
单击键盘上的Ctrl+Enter键,返回一个Match对象m。单元格B3中在Python模式下在公式栏输入下面代码:
m.group(1)
单击键盘上的Ctrl+Enter键,返回获取第1个捕获分组。单元格B4中在Python模式下在公式栏输入下面代码:
m.group(2)
单击键盘上的Ctrl+Enter键,返回获取第2个捕获分组。单元格B5中在Python模式下在公式栏输入下面代码:
m.group(1,2)
单击键盘上的Ctrl+Enter键,返回获取第1个和第2个捕获分组。单元格B6中在Python模式下在公式栏输入下面代码:
m.groups()
单击键盘上的Ctrl+Enter键,返回获取全部捕获分组。单元格B7中在Python模式下在公式栏输入下面代码:
m.groupdict()
单击键盘上的Ctrl+Enter键,返回一个字典,分组别名为键,捕获的字符串为值。单元格B8中在Python模式下在公式栏输入下面代码:
m.start(1)
单击键盘上的Ctrl+Enter键,返回第1个分组捕获字符串的起始位置索引号。单元格B9中在Python模式下在公式栏输入下面代码:
m.end(1)
单击键盘上的Ctrl+Enter键,返回第1个分组捕获字符串的终止位置索引号。单元格B10中在Python模式下在公式栏输入下面代码:
m.start(2)
单击键盘上的Ctrl+Enter键,返回第2个分组捕获字符串的起始位置索引号。单元格B11中在Python模式下在公式栏输入下面代码:
m.end(2)
单击键盘上的Ctrl+Enter键,返回第2个分组捕获字符串的终止位置索引号。单元格B12中在Python模式下在公式栏输入下面代码:
m.span(1)
单击键盘上的Ctrl+Enter键,返回第1个分组捕获字符串的位置索引号范围。单元格B13中在Python模式下在公式栏输入下面代码:
m.span(2)
单击键盘上的Ctrl+Enter键,返回第2个分组捕获字符串的位置索引号范围。单元格B14中在Python模式下在公式栏输入下面代码:
m.expand(r'\2\1')
单击键盘上的Ctrl+Enter键,返回用分组的编号重构的字符串。
图3-29 Match对象的方法
Pattern对象
介绍re模块时,介绍了该模块的各种函数,其中有一个重要的函数还没有介绍,它就是re.compile函数。该函数的主要作用是创建Pattern对象,即正则表达式对象。利用该对象,也可以实现字符串的查找、替换和分割。
创建Pattern对象
Pattern对象即编译好的正则表达式对象,使用re模块的re.compile函数进行创建。该函数的语法格式为:
re.compile(pattern[, flags])
其中,pattern为进行匹配的正则表达式字符串,flags为指定正则表达式匹配方式的标记。
下面给定原始字符串和正则表达式,用re.compile函数将正则表达式字符串编译为正则表达式对象,然后用表达式对象的match方法从原始字符串打头位置开始进行匹配。不区分大小写。
图3-30所示工作表中,A2单元格中给定一个字符串,要求用re.compile函数编译好查找”abc”字符串的Pattern对象,查找时不区分大小写。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
p=re.compile('abc',re.I)
单击键盘上的Ctrl+Enter键,返回一个Pattern对象p。
图3-30 Pattern对象
Pattern对象的属性和方法
使用Pattern对象提供的属性可以获取正则表达式的相关信息:
pattern属性 - 正则表达式字符串。
flags属性 - 匹配方式,用数字表示。
groups属性 – 正则表达式中分组的个数。
groupindex属性 - 以正则表达式中有别名的分组的别名为键、以该分组的编号为值的字典,没有别名的分组不包含在内。
Pattern对象的方法跟re模块实现字符串查找、替换和分割的函数相对应,它们提供了re模块的另一种使用方式。
Pattern对象的方法如表3-3所示。
表3-3 Pattern对象的方法
| Pattern对象的方法 | 说 明 | 对应的re模块函数 |
|---|---|---|
| match(string[, pos[, endpos]]) | 从打头位置查找 | re.match(pattern, string[, flags]) |
| search(string[, pos[, endpos]]) | 从整个字符串查找第1个 | re.search(pattern, string[, flags]) |
| findall(string[, pos[, endpos]]) | 查找全部匹配结果,列表返回 | re.findall(pattern, string[, flags]) |
| finditer(string[, pos[, endpos]]) | 查找全部匹配结果,迭代器返回 | re.finditer(pattern, string[, flags]) |
| sub(repl, string[, count]) | 替换匹配结果 | re.sub(pattern, repl, string[, count]) |
| subn(repl, string[, count]) | 替换匹配结果,元组返回 | re.sub(pattern, repl, string[, count]) |
| split(string[, maxsplit]) | 匹配结果作为分隔符分割字符串 | re.split(pattern, string[, maxsplit]) |
注意:表3-3中,Pattern对象的方法跟对应的re模块函数比较,除split方法外其他方法比对应的re模块函数多两个参数,即pos参数和endpos参数,指定进行查找或替换时指定的原始字符串中的起始位置和终止位置。
继续使用创建Pattern对象的示例。图3-30所示工作表中,单元格B3中在Python模式下在公式栏输入下面代码:
p.pattern
单击键盘上的Ctrl+Enter键,返回正则表达式字符串。单元格B4中在Python模式下在公式栏输入下面代码:
p.flags
单击键盘上的Ctrl+Enter键,返回标记类型,用数字表示。单元格B5中在Python模式下在公式栏输入下面代码:
p.groups
单击键盘上的Ctrl+Enter键,返回正则表达式分组的个数。单元格B6中在Python模式下在公式栏输入下面代码:
p.groupindex
单击键盘上的Ctrl+Enter键,返回以正则表达式中有别名的分组的别名为键、以该分组的编号为值的字典,没有别名的分组不包含在内。。单元格B7中在Python模式下在公式栏输入下面代码:
p.match(a)
单击键盘上的Ctrl+Enter键,返回匹配的Match对象。单元格B8中在Python模式下在公式栏输入下面代码:
p.search(a)
单击键盘上的Ctrl+Enter键,返回第1个匹配的Match对象。单元格B9中在Python模式下在公式栏输入下面代码:
p.findall(a)
单击键盘上的Ctrl+Enter键,返回所有的匹配结果,用列表表示。单元格B10中在Python模式下在公式栏输入下面代码:
p.finditer(a)
单击键盘上的Ctrl+Enter键,返回匹配结果,用迭代器表示。单元格B11中在Python模式下在公式栏输入下面代码:
p.sub('xyz',a)
单击键盘上的Ctrl+Enter键,返回字符串替换结果。单元格B12中在Python模式下在公式栏输入下面代码:
p.sub('xyz',a,0)
单击键盘上的Ctrl+Enter键,返回字符串替换结果,元组形式返回。单元格B13中在Python模式下在公式栏输入下面代码:
p.split(a)
单击键盘上的Ctrl+Enter键,返回字符串分割结果,列表形式返回。
正则表达式的编写规则
3.4.1小节介绍了给定一个正则表达式,在Python中怎么使用它。这一小节介绍怎么编写正则表达式,即编写正则表达式的语法规则。
元字符
元字符是在正则表达式中具有特殊含义的字符,其含义超出了自己本身的含义。比如Python正则表达式中,用\d表示数字,用\s表示空白。常见的元字符如表3-4中所示。
表3-4 常见元字符
| 元字符 | 说 明 | 元字符 | 说 明 |
|---|---|---|---|
| . | 匹配除换行符以外的任意字符 | ^ | 匹配字符串的开始位置的字符 |
| \w | 匹配是字母、数字、下划线或汉字的字符 | $ | 匹配字符串的结束位置的字符 |
| \s | 匹配任意空白符 | \n | 匹配一个换行符 |
| \d | 匹配数字 | \r | 匹配一个回车符 |
| \b | 匹配单词的开始或结束位置的字符 | \t | 匹配一个制表符 |
一般情况下是指定要查找的字符或在指定的范围内进行查找,但有时情况会反过来,即排除指定的字符或在指定的字符范围之外进行查找。这种情况下使用表示反义的元字符,如用\D表示非数字的字符,用\S表示非空白的字符。常见的反义元字符如表3-5中所示。
表3-5 反义元字符
| 反义元字符 | 说 明 |
|---|---|
| \W | 匹配任意不是字母,数字,下划线,汉字的字符 |
| \S | 匹配任意不是空白符的字符 |
| \D | 匹配任意非数字的字符 |
| \B | 匹配不是单词开头或结束位置的字符 |
| [^x] | 匹配除了x以外的任意字符 |
| [^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
下面结合一些示例来深入理解元字符。
图3-31所示工作表中,A2单元格中给定一个字符串,用re.findall函数查找其中的全部数字,用re.sub函数将所有数字替换为空。单个的数字用元字符\d表示。
图3-31 使用元字符
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m0=re.findall(r'\d',a)
单击键盘上的Ctrl+Enter键,返回包含查找结果的列表。单元格E2中在Python模式下在公式栏输入下面代码:
import re
re.sub(r'\d','',a)
单击键盘上的Ctrl+Enter键,所有数字替换为空(删除)。
单元格B4中在Python模式下在公式栏输入下面代码测试元字符\b,它表示单词的开头或结尾。正则表达式为r"\bC\d",表示匹配字符串必须是原始字符串以C打头或C前面为空格,C的后面跟数字。匹配的字符串置换为空。
import re
a=xl("A4")
m=re.sub(r'\bC\d','',a)
单击键盘上的Ctrl+Enter键,返回字符串替换结果。因为第1个C5位于原始字符串的开头,满足C加数字的条件,匹配;第2个C5前面为空格,满足\b的条件。将它们置换为空后剩下的字符串即为'dC56 '。
单元格B6中在Python模式下在公式栏输入下面代码测试元字符^,^限制字符在原始字符串的最前面,如^\d表示原始字符串以数字打头。下面给定原始字符串,如果它以一个以上的数字打头,返回该数字。
import re
a=xl("A6")
m=re.findall(r'^\d+',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。因为12345位于原始字符串打头位置,匹配;而09虽然也是数字,但不再打头位置,不匹配。正则表达式中的加号是表示重复的元字符,前面为d,表示一个以上的数字。
单元格B8中在Python模式下在公式栏输入下面代码,代码中\D表示不是数字的字符,元字符$限制字符在原始字符串的结尾处,如C$表示最后一个字符是C。
import re
a=xl("A8")
m=re.findall(r'\d+\D',a)
单击键盘上的Ctrl+Enter键,返回全部匹配结果。正则表达式r"\d+\D"表示前面是1个以上的数字,后面跟的字符不是数字。
单元格B9中在Python模式下在公式栏输入下面代码:
import re
m=re.findall(r'\d+\D$',a)
单击键盘上的Ctrl+Enter键,返回全部匹配结果。正则表达式r"\d+\D$"在最后面添加了"$",表示匹配的字符串必须位于原始字符串的结尾处,所以只匹配到"09W"。
重复
进行查找或替换时有时需要连续查找或替换多个某种类型的字符,这就是重复。重复次数可以是确定的,也可以是不确定的。比如Python正则表达式中用\d+表示1个以上的数字,重复次数不确定;\d{5}表示5个数字,重复次数是确定的。
Python正则表达式中表示重复的元字符如表3-6中所示。
表3-6 表示重复的元字符
| 元字符 | 说 明 | 元字符 | 说 明 |
|---|---|---|---|
| * | 重复零次或更多次 | {n} | 重复n次 |
| + | 重复一次或更多次 | {n,} | 重复n次或更多次 |
| ? | 重复零次或一次 | {n,m} | 重复n到m次 |
元字符*表示前面定义的字符可以重复零次或更多次,相当于 {0,}。下面给定1个字符串,找出所有W打头,后面跟或不跟数字的子字符串。
图3-32 使用重复
图3-32所示工作表中,A2单元格中给定一个字符串,用re.findall函数查找其中全部以”W”打头,后面跟0个以上数字的字符串。单个的数字用元字符\d表示。
单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.findall(r'W\d*',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。注意列表中最后1个元素在W后面没有跟数字。
元字符+表示前面定义的字符可以重复1次或更多次,相当于 {1,}。下面给定1个字符串,找出所有W打头,后面跟1个或1个以上数字的子字符串。单元格B3中在Python模式下在公式栏输入下面代码:
re.findall(r'W\d+',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。注意最后一个W因为后面没有跟数字,在这里没有匹配。
元字符?表示前面定义的字符可以重复0次或1次,相当于{0,1}。下面给定1个字符串,找出所有前后都是数字,中间有或没有小数点的子字符串。单元格B5中在Python模式下在公式栏输入下面代码:
import re
a=xl("A5")
m=re.findall(r'\d+\.?\d+',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
使用{}可以设置重复次数。{n}表示前面定义的字符重复n次。下面给定1个字符串,找出其中连续3个都是数字的子字符串。单元格B7中在Python模式下在公式栏输入下面代码:
import re
a=xl("A7")
m=re.findall(r'\d{3}',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
{m,n}表示前面定义的字符的重复次数在1个指定的范围内取值,最小重复m次,最多重复n次。下面给定1个字符串,找出其中连续2个或3个都是数字的子字符串。单元格B8中在Python模式下在公式栏输入下面代码:
re.findall(r'\d{2,3}',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
{m,}表示前面定义的字符最少重复m次,相当于元字符+。下面给定1个字符串,找出其中连续2个以上都是数字的子字符串。单元格B9中在Python模式下在公式栏输入下面代码:
re.findall(r'\d{2,}',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
字符类
使用前面的方法可以查找指定的数字、字母、数字或空白,但是如果给定的是一个字符集,要求查找的字符只在这个集合中取或者在这个集合外取,就要用到中括号。中括号定义字符集的方式如表3-7中所示。
表3-7 中括号的用法
| 应用格式示例 | 说 明 |
|---|---|
| [adwkf] | 查找的字符是中括号内字符中的一个 |
| [^adwkf] | 查找的字符不是中括号内字符的就行 |
| [b-f] | 查找的字符是b到f中的一个 |
| [^b-f] | 查找的字符不是b到f中的一个 |
| [2-5] | 查找的字符是2到5中的一个 |
| [2-46-9] | 查找的字符是2到4或6到9中的一个 |
| [a-w2-5A-W] | 查找的字符是a到w, 2到5或A到W范围内的一个 |
| [一-龥]或[\u4e00-\u9fa5] | 查找的字符是中文字符 |
使用方括号[]包含个字符集,能够匹配其中任意1个字符。使用[^],则不匹配方括号内的字符,只能匹配该字符集之外的任意1个字符。
图3-33 使用字符类
图3-33所示工作表中,A2单元格中给定一个字符串,用re.findall函数找出字符串中与方括号中任意字符匹配的字符。单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.findall('[AEIOU]',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
用re.findall函数找出字符串中与方括号中任意字符不匹配的字符。单元格B3中在Python模式下在公式栏输入下面代码:
re.findall('[^AEIOU]',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
用re.findall函数找出字符串中落在方括号中指定字符范围的字符。单元格B4中在Python模式下在公式栏输入下面代码:
re.findall('[G-T]',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
用re.findall函数找出字符串中1-5的数字和G-T的字母。单元格B6中在Python模式下在公式栏输入下面代码:
import re
a=xl("A6")
n=re.findall('[1-5G-T]',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
查找字符串中的中文字符,正则表达式中用中括号指定中文字符范围。可以有两种指定方式,即[一-龥]和[\u4e00-\u9fa5]。后一种方式是以四位十六进制数表示的Unicode字符。中文字符一的编码是4e00,最后一个代码是9fa5。
用re.findall函数找出给定字符串中的中文字符。单元格B8中在Python模式下在公式栏输入下面代码:
import re
a=xl("A8")
r=re.findall('[\u4e00-\u9fa5]',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
用re.sub函数将找出的中文字符替换为空。单元格B9中在Python模式下在公式栏输入下面代码:
re.sub('[\u4e00-\u9fa5]','',a)
单击键盘上的Ctrl+Enter键,返回删除中文字符后的字符串。
分支条件
假设有几种规则,只要满足其中一种即可完成匹配,就要用到分支条件。使用 | 将不同的规则进行分隔。比如数字后面跟重量单位,有的记录为公斤,有的记录为千克,可以用"\d+(公斤|千克) "进行提取,相当于"\d+公斤|d+千克"。
图3-34所示工作表中,A2单元格中给定一个字符串,用re.findall函数查找子字符串"ABC"或以W打头后面跟数字的子字符串。单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.findall(r'ABC|W\d+',a)
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
图3-34 使用分支条件
用re.findall函数查找A4单元格文本中数字后面跟公斤、kg或千克的子字符串,用分支条件编写正则表达式进行查找。单元格B4中在Python模式下在公式栏输入下面代码:
import re
a=xl("A4")
m=re.finditer(r'\d+(公斤|千克|kg)',a)
lst=[]
for i in m:
lst.append(i.group(0))
lst
单击键盘上的Ctrl+Enter键,用列表返回所有匹配结果。
捕获分组和非捕获分组
正则表达式中存在有子表达式的情况,子表达式用小括号指定并作为一个整体进行操作。比如,正则表达式"((ABC){2})"将"ABC"作为一个整体重复2次。
图3-35所示工作表中,A2单元格中给定一个字符串,用re.search函数查找连续两个”ABC”组成的子字符串。单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.search('((ABC){2})',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。
图3-35 使用捕获分组和非捕获分组
使用小括号对正则表达式进行分组时,会自动分配组号。分配组号的原则是从左到右,从外到内。使用组号可以对对应的分组进行反向引用。
图3-35所示工作表中,A4单元格中给定一个字符串,用re.finditer函数查找能匹配正则表达式的字符串。单元格B4中在Python模式下在公式栏输入下面的代码。代码中正则表达式r"(WT)\d+\1"匹配给定字符串中前后都是"WT",中间是1个或多个数字的子字符串。注意其中的\1表示小括号内的"WT",这个分组自动分配组号1,使用\1进行反向引用。
import re
a=xl("A4")
m=re.finditer(r'(WT)\d+\1',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。匹配结果"WT12389WT "两端都是"WT",中间全是数字,满足匹配要求。
下面演示有更多分组的情况。A6单元格中给定一个字符串,用re.search函数查找能匹配正则表达式的字符串。单元格B6中在Python模式下在公式栏输入下面的代码。代码中正则表达式r"((WT){2})((PR){2})\d+\2\4"中一共有4对小括号,前面两层后面两层。
import re
a=xl("A6")
m=re.search(r'((WT){2})((PR){2})\d+\2\4',a)
m.group(1)
单击键盘上的Ctrl+Enter键,返回匹配结果。匹配结果"WT12389WT "两端都是"WT",
探查各小括号对应的分组的编号。单元格B7中在Python模式下在公式栏输入下面代码:
m.group(2)
单击键盘上的Ctrl+Enter键,返回匹配结果。
单元格B8中在Python模式下在公式栏输入下面代码:
m.group(3)
单击键盘上的Ctrl+Enter键,返回匹配结果。
单元格B9中在Python模式下在公式栏输入下面代码:
m.group(4)
单击键盘上的Ctrl+Enter键,返回匹配结果。
以上结果显示,按照从左到右的原则,首先给左边的两层小括号对应的分组编号,此时按照从外到内的顺序编号。外层小括号中为" (WT){2}",即匹配"WTWT";内存小括号中为"WT",它们对应于分组编号1和2。右边两层小括号的情况类似,匹配第3个分组"PRPR"和第4个分组"PR"。
上面用小括号定义的分组,每个分组都自动进行编号,并可以用Match对象的group方法进行捕获,匹配结果保存到内存,称为捕获分组。但有时候,我们并不关注匹配到的内容,即分组参与匹配,但没有必要进行捕获,不用在内存中保存匹配到的内容。此时仍然用小括号进行分组,但是在小括号里的最前端加上"?:",如(?:\d{3})。这种分组称为非捕获分组。非捕获分组不参与编号,不在内存保存匹配结果,所以能节省内存空间,提高工作效率。
图3-35所示工作表中,A11单元格中给定一个字符串,用re.finditer函数查找能匹配正则表达式的字符串。单元格B11中在Python模式下在公式栏输入下面的代码。代码中正则表达式为r"(?:ab)(CD)\d+\1",其中两个分组,第1个分组在小括号内的最前端有"?:",为非捕获分组。
import re
a=xl("A11")
m=re.finditer(r'(?:ab)(CD)\d+\1',a)
lst=[]
for i in m:
lst.append(i.group())
lst
单击键盘上的Ctrl+Enter键,返回匹配结果。代码用re.finditer函数获取匹配迭代器,用for循环获取匹配结果。结果显示,匹配字符串中是包括"ab"的。
接着用re.search函数进行查找,返回Match对象m,调用该对象的groups属性查看各分组的子字符串。单元格B12中在Python模式下在公式栏输入下面的代码:
m=re.search(r'(?:ab)(CD)\d+\1',a)
m.groups()
单击键盘上的Ctrl+Enter键,返回各分组的子字符串。仅返回1个分组结果"CD",第1个分组因为声明为非捕获分组,不参与编号,也不保存。
零宽断言
零宽断言用于查找指定内容之前或之后的内容,不包括指定内容。有两种类型,即
零宽度正预测先行断言:表达式为(?=exp),查找exp表示的内容之前的内容。
零宽度正回顾后发断言:表达式为(?<=exp),查找exp表示的内容之后的内容。
组合上面两种情况,可以查找指定内容之间的内容。
图3-36所示工作表中,A2单元格中给定一个字符串,用re.finditer函数提取出单位公斤前面的数字,只提取数字。使用零宽度正预测先行断言进行提取。单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.findall(r'\d+(?=公斤)',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。正则表达式r"\d+(?=公斤)"表示匹配"公斤"前面的数字,不包括"公斤"。结果显示匹配正确。
图3-36 使用零宽断言
A4单元格中给定一个字符串,要求提取出"同学"、"战友"、"师兄"等称谓后面的姓名。使用零宽度正回顾后发断言进行提取。单元格B4中在Python模式下在公式栏输入下面代码:
import re
a=xl("A4")
re.findall(r'(?<=同学|战友|师兄)\w+',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。正则表达式r"(?<=同学|战友|师兄)\w+"表示匹配"同学"、"战友"或"师兄"等称谓后面的子字符串。各称谓使用分支条件进行匹配。匹配的结果不包括称谓。
负向零宽断言
负向零宽断言用于断言指定位置的前面或后面不能匹配指定的表达式。有两种类型,即
零宽度负预测先行断言:表达式为(?:exp),断言此位置的后面不能匹配表达式exp。
零宽度负回顾后发断言:表达式为(?<!exp),断言此位置的前面不能匹配表达式exp。
图3-37所示工作表中,A2单元格中给定一个字符串,要求匹配数字123前面是字母、数字或下划线,后面不能跟大写字母。使用零宽度负预测先行断言进行匹配。单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
re.findall('\w123(?![A-Z])',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。可见,给定字符串中第1个"123"因为前面不是字母,后面跟了大写字母W,不能匹配。
图3-37 使用负向零宽断言
A4单元格中给定一个字符串,要求匹配不包含"who "的单词。使用零宽度负预测先行断言进行匹配。单元格B4中在Python模式下在公式栏输入下面代码:
import re
a=xl("A4")
m=re.finditer(r'\b((?!who)\w)+\b',a)
lst=[]
for i in m:
lst.append(i.group())
lst
单击键盘上的Ctrl+Enter键,返回匹配结果。
A6单元格中给定一个字符串,要求匹配前面不是小写字母的5位数字。使用零宽度负回顾后发断言进行匹配。单元格B6中在Python模式下在公式栏输入下面代码:
import re
a=xl("A6")
m=re.search(r'(?<![a-z])\d{5}',a)
m.group()
单击键盘上的Ctrl+Enter键,返回匹配结果。
注意,负向零宽断言进行不定长表达式的匹配时常常出错。所谓不定长表达式,是诸如\w+, \d*等长度不确定的表达,此时如果可以,改为定长的表达,例如\w{4}, \d{3}等。
贪婪与懒惰
前面介绍*和+时,是匹配尽可能多的字符,称为贪婪匹配。但有时候需要匹配尽可能少的字符,称为懒惰匹配,方法是在贪婪匹配的后面添加一个问号。
常见的懒惰匹配格式如表3-8中所示。
表3-8 懒惰匹配
| 懒惰匹配格式 | 说 明 |
|---|---|
| *? | 重复任意次,但尽可能少重复 |
| +? | 重复1次或更多次,但尽可能少重复 |
| ?? | 重复0次或1次,但尽可能少重复 |
| {n,m}? | 重复n到m次,但尽可能少重复 |
| {n,}? | 重复n次以上,但尽可能少重复 |
图3-38所示工作表中,A2单元格中给定一个字符串,要求分别使用贪婪匹配和懒惰匹配比较匹配结果。单元格B2中在Python模式下在公式栏输入下面代码:
import re
a=xl("A2")
m=re.findall('\s.+\s',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。正则表达式"\s.+\s"中没有?,此为贪婪匹配,在两个空白符之间匹配尽可能多的字符,所以匹配结果是整个字符串。
图3-38 使用贪婪和懒惰
单元格B4中在Python模式下在公式栏输入下面代码:
re.findall('\s.+?\s',a)
单击键盘上的Ctrl+Enter键,返回匹配结果。正则表达式"\s.+?\s"中+后面有?,此为懒惰匹配,在两个空白符之间匹配尽可能少的字符,所以匹配结果是空格间隔的三个子字符串。
在pandas中使用正则表达式
前面两个小节介绍了在Excel内置Python中使用正则表达式的方法,注意各示例针对的是处理单个字符串的情况。如果是一组字符串需要作同样的处理,在Excel内置Python中怎么做呢?下面举个例子进行介绍。
图3-39所示工作表中,A1单元格中给定一个字符串,要求提取出里面的手机号码。单元格B1中在Python模式下在公式栏输入下面代码:
import re
a=xl("A1")
re.findall('1(0-9){10}',a)
单击键盘上的Ctrl+Enter键,返回字符串中包含的手机号码。
现在A列有一组这样的字符串,要求都提取出手机号。在Excel工作表中可以像正常处理公式一样,单击B1单元格,然后双击该单元格右下角的圆点,实现向下填充。如图3-39所示,单元格B2至单元格B6每个单元格中会自动填充提取手机号码的Python代码并返回匹配结果。这样实现了对一组数据的处理。
图3-39 在Python中使用正则表达式
乍一看,这种处理方法还比较方便,能实现公式向下填充也很感人。但实际上,我们有更好的处理办法,那就是使用Python的pandas包。pandas包在NumPy包的基础上开发,特别适用于处理数组。优点是处理速度快,代码简洁。
图3-40所示工作表中,单元格B1中在Python模式下在公式栏输入下面代码:
df=xl("A1:A6")
df[0].str.extract(‘(1[0-9]{10})’)
单击键盘上的Ctrl+Enter键,B1单元格返回一个Sereis对象。以Excel值的方式显示,在B列返回所有提取出的手机号码,如图3-40所示。
图3-40 在pandas中使用正则表达式
比较后不难发现,使用pandas包进行处理,只需要在Excel中添加一个公式就可以对所有待处理的文本进行处理,速度非常快。正则表达式放在pandas函数或方法中预先指定的参数位置,不需要导入re包。
注意给要输出的内容加上小括号,只有分组的内容才能被输出。
本例中提取数据使用的是Series对象的str.extract方法,实际上,在pandas中,类似这样的方法还有很多,比如实现数据提取的str.extractall方法、实现查找的str.match方法、str.fullmatch方法、str.findall方法、str.contains方法、实现分割的str.split方法、实现替换的str.replace方法等。这些方法将在本节接下来的篇幅里结合实例进行介绍。
在pandas中使用正则表达式综合实例1
图3-41所示工作表中单元格区域A1:A7为给定数据,要求提取出里面前面是1个以上字母后面是1个以上数字的字符串。
单元格C1中在Python模式下在公式栏输入下面代码,用str.findall方法进行查找,注意给要返回的内容加上小括号:
import re
df=xl("A1:A7")
df[0].str.findall(r'(^[a-z]+\d+$)',flags=re.IGNORECASE)
单击键盘上的Ctrl+Enter键,C1单元格返回一个Series对象,对象中的元素为提取结果。以字符串形式显示该对象的内容,可见不匹配的字符串返回结果为空,匹配的返回正确结果。注意代码中使用了re.IGNORECASE标记,所以先导入re包。也可以用数字表示标记。
图3-41 查找和提取数据
单元格C3中在Python模式下在公式栏输入下面代码,用str.extractall方法进行提取:
df[0].str.extractall(r'(^[a-z]+\d+$)',flags=re.IGNORECASE)
单击键盘上的Ctrl+Enter键,C3单元格返回一个DataFrame对象,以Excel值的形式进行显示,可见该DataFrame对象有2层列索引,只包含与正则表达式匹配的字符串。
单元格G3中在Python模式下在公式栏输入下面代码,用str.extract方法进行提取:
df[0].str.extract(r'(^[a-z]+\d+$)',flags=re.IGNORECASE)
单击键盘上的Ctrl+Enter键,G3单元格返回一个Series对象,以Excel值的形式显示,可见结果与使用str.findall方法返回的类似,不匹配的结果返回#NUM!。
在pandas中使用正则表达式综合实例2
图3-42所示工作表中A2:A4中为多次采购食材的记录,现要求计算每次采购的食材的总重量。思路是使用零宽度正预测先行断言提取出重量单位前面的数字进行累加。
单元格C1中在Python模式下在公式栏输入下面代码,用str.extractall方法进行提取:
df=xl("A1:B4",headers=True)
df2=df['采购明细'].str.extractall(r'(\d+\.?\d*(?=(公斤|千克|kg)))')
单击键盘上的Ctrl+Enter键,C1单元格返回一个DataFrame对象df2,该对象有2层列索引,食材重量中的数字和单位被分为两列作为对象的值。
图3-42 计算食材总重量
单元格C13中在Python模式下在公式栏输入下面代码,对每行字符串中的数字进行求和。注意下面代码中用Series对象的sum方法进行求和时指定level参数的值为0,表示对2层列索引中的第1层进行求和,即对原数据中的每行文本中的数字进行求和。求和之前需要用Series对象的astype方法将数字文本转换为数字。
df2[0].astype(int).sum(level=0)
单击键盘上的Ctrl+Enter键,C13单元格返回一个Series对象,该对象包含每次采购食材的总重量。
在pandas中使用正则表达式综合实例3
图3-43所示工作表中A1:A3中为某店9月份的采购信息,其中包含每次采购的日期,现要求提取出日期。
单元格B1中在Python模式下在公式栏输入下面代码,用str.extract方法进行提取:
df=xl("A1:A3")
df[0].str.extract(r'(\d{4}-\d{2}-\d{2}|\d{4}\.\d{2}\.\d{2}|\d{4}/\d{2}/\d{2})')
单击键盘上的Ctrl+Enter键,B1单元格返回一个DataFrame对象,其中第1列即为每次采购的日期。
图3-43 提取日期
在pandas中使用正则表达式综合实例4
图3-44所示工作表中B2:B4中为各班成绩优秀、良好、中等、及格和不及格的人数,现要求根据这些人数计算出各班的总人数。观察数据后发现,将文本中的汉字和乘号删除后剩下一个数学运算表达式,计算该表达式即得到每班的总人数。
单元格C1中在Python模式下在公式栏输入下面代码,用str.replace方法将汉字和乘号替换为空,即删除它们:
df=xl("A1:B4",headers=True)
ser=df['成绩分布'].str.replace(r'[\u4e00-\u9fa5]+\*','')
单击键盘上的Ctrl+Enter键,C1单元格返回一个Series对象,其中的元素为去除汉字和乘号后剩下的由人数组成的数学运算表达式。在Python中用eval函数可以计算表达式的值。
单元格E1中在Python模式下在公式栏输入下面代码,用str.apply方法结合匿名函数和eval函数计算每个表达式的值。
ser.apply(lambda x:eval(x))
单击键盘上的Ctrl+Enter键,E1单元格返回一个Series对象,其中的元素为表达式的计算值,即各班的总人数。
图3-44 计算总人数
在pandas中使用正则表达式综合实例5
图3-45所示工作表中A1:A2中为两次食材采购信息,现要求将数据整理成H3:I11中所示的样子,即将食材名称和采购费用分两列显示。这是一个分列问题,但不是一般的用分隔符分列或固定宽度分列问题。因为食材名称都是汉字,费用都是数字且后面跟“元”字,考虑用正则表达式实现。
图3-45 整理数据
单元格B3中在Python模式下在公式栏输入下面代码,用str.extractall方法结合正则表达式将汉字和费用进行分割:
df=xl("A1:A2")
df2=df[0].str.extractall(r'(([一-龢]{1,}) (\d+\.?\d*元))')
单击键盘上的Ctrl+Enter键,B3单元格返回一个DataFrame对象df3。该对象有2层列索引,值有3列,如图3-45所示,还需要进一步整理。
单元格H3中在Python模式下在公式栏输入下面代码,通过索引提取出df3中的后2列数据,然后用DataFrame对象的reset_index方法去掉列索引。
df3=df2.loc[:,1:2]
df3.reset_index(drop=True)
单击键盘上的Ctrl+Enter键,H3单元格返回结果如图3-45所示,正是我们需要的结果。
在pandas中使用正则表达式综合实例6
图3-46所示工作表A1:G4中为不同班级语文、数学和英语3门课的平均成绩。现在要求筛选出名称以”1”打头的班级的数据。
单元格A6中在Python模式下在公式栏输入下面代码,用DataFrame对象的filter方法进行筛选。
df=xl("A1:G4",headers=True)
df.filter(regex='^1',axis=1)
单击键盘上的Ctrl+Enter键,A6单元格返回一个DataFrame对象,以Excel值的方式显示,结果如图3-46所示。
图3-46 筛选数据