7.2节结合一些简单的实例介绍了在Python中如何实现使用正则表达式实现字符串的查找、替换和分割等。实际上,可以编写更加复杂的正则表达式,以实现更复杂更灵活的文本搜索和替换。本节介绍正则表达式的各种编写规则,这部分内容是正则表达式的核心内容,在不同计算机语言中,这部分内容基本上是相同的。[大谦Excel,dqexcel点com]
元字符
元字符是在正则表达式中具有特殊含义的字符,其含义超出了自己本身的含义。比如Python正则表达式中,用\d表示数字,用\s表示空白。常见的元字符如表7-4中所示。
表7-4 常见元字符
| 元字符 | 说 明 | 元字符 | 说 明 |
|---|---|---|---|
| . | 匹配除换行符以外的任意字符 | ^ | 匹配字符串的开始 |
| \w | 匹配字母、数字、下划线或汉字 | $ | 匹配字符串的结束 |
| \s | 匹配任意空白符 | \n | 匹配一个换行符 |
| \d | 匹配数字 | \r | 匹配一个回车符 |
| \b | 匹配单词的开始或结束 | \t | 匹配一个制表符 |
一般情况下是指定要查找的字符或在指定的范围内进行查找,但有时情况会反过来,即排除指定的字符或在指定的字符范围之外进行查找。这种情况下使用表示反义的元字符,如用\D表示非数字的字符,用\S表示非空白的字符。常见的反义元字符如表7-5中所示。
表7-5 反义元字符
| 反义元字符 | 说 明 |
|---|---|
| \W | 匹配任意不是字母,数字,下划线,汉字的字符 |
| \S | 匹配任意不是空白符的字符 |
| \D | 匹配任意非数字的字符 |
| \B | 匹配不是单词开头或结束的位置 |
| [^x] | 匹配除了x以外的任意字符 |
| [^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
下面结合一些示例来深入理解元字符。
下面给定原始字符串,用re.findall函数查找其中的全部数字,用re.sub函数将所有数字替换为空。单个的数字用元字符\d表示。
>>> impoirt re
>>> a="BC_101PW%" #原始字符串
>>> m0=re.findall(r"\d",a) #查找所有数字
>>> m0
['1', '0', '1']
>>> for i in m0: #逐个输出数字
print(i)
1
0
1
>>> ms=re.sub(r"\d","",a) #所有数字替换为空(删除)
>>> ms
'BC_PW%'
下面的示例测试元字符\b,它表示单词的开头或结尾。正则表达式为r"\bC\d",表示匹配字符串必须是原始字符串以C打头或C前面为空格,C的后面跟数字。匹配的字符串置换为空。
>>> impoirt re
>>> a="C5dC56 C5"
>>> m=re.sub(r"\bC\d","",a)
>>> m
'dC56 '
因为第1个C5位于原始字符串的开头,满足C加数字的条件,匹配;第2个C5前面为空格,满足\b的条件。将它们置换为空后剩下的字符串即为'dC56 '。
元字符^限制字符在原始字符串的最前面,如^\d表示原始字符串以数字打头。下面给定原始字符串,如果它以一个以上的数字打头,返回该数字。
>>> impoirt re
>>> a="12345my09"
>>> m=re.findall(r"^\d+",a)
>>> for i in m:
print(i)
12345
因为12345位于原始字符串打头位置,匹配;而09虽然也是数字,但不再打头位置,不匹配。正则表达式中的加号是表示重复的元字符,前面为d,表示一个以上的数字。
下面的代码中,\D表示不是数字的字符,元字符$限制字符在原始字符串的结尾处,如C$表示最后一个字符是C。
>>> impoirt re
>>> a="12345my09W"
>>> m=re.findall(r"\d+\D",a)
>>> m
['12345m', '09W']
>>> m=re.findall(r"\d+\D$",a)
>>> m
['09W']
第1个正则表达式r"\d+\D"表示前面是1个以上的数字,后面跟的字符不是数字;第2个正则表达式r"\d+\D$"在最后面添加了"$",表示匹配的字符串必须位于原始字符串的结尾处,所以只匹配到"09W"。
图7-1 提取指定格式的字符串
下面结合Excel表格数据介绍Python正则表达式的应用。对于图7-1中处理前工作表中第1列的数据,从中将前面为字母后面为数字的数据提取出来写入第2列。
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam05.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 arr=sht.range("A1", sht.cells(sht.cells(1,"A").end("down").row, "A")).value
9 n=0
10 for i in range(len(arr)): #遍历每条数据
11 mt=re.findall(r"^[a-z]+\d+$",arr[i],re.I) #前面为字母,后面为数字
12 for j in range(len(mt)): #在第2列写入匹配数据
13 n+=1
14 sht.cells(n,2).value=mt[j]
第1-3行导入xlwings, os和re包,分别用于Excel工作表数据读写、文件操作和正则表达式操作。
第4行获取当前目录,即本py文件所在的目录。
第5行用App函数创建Excel应用。该应用窗口可见,不添加工作簿。
第6行用books读写的open方法打开当前目录下的Sam05.xlsx文件。因为数据提取完以后需要写入工作表,设置read_only参数的值为False,即打开的工作簿可写。
第7-8行获取第1个工作表,用range对象的end方法获取数据的行数,把第1列的数据保存在列表arr中。
第9-14行用1个for循环提取数据并写入第2列。
第9行设置n的值为0,它记录当前提取出来的数据的个数。
第10行用for循环遍历arr列表中的每个数据。
第11行用re.findall函数找出当前数据中满足要求的字符串,写入列表mt。正则表达式r"^[a-z]+\d+$"中,^和$匹配字符串的开头和结尾,中括号表示前面取字母,后面跟加号,表示取1个以上的字母,\d+表示字母后面跟1个以上的数字。re.I表示字母没有大小写区分。
第12-14行用for循环输出匹配结果到第2列中第1个空白单元格。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,满足要求的数据被提取出来并逐个写入第2列,如图7-1中处理后工作表所示。
图7-2 计算各班的总人数
图7-2中处理前工作表中第2列的数据列出了各班成绩优秀、良好、中等、及格和不及格的人数,现要求根据这些人数计算出各班的总人数并写入第3列。
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam07.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 arr=sht.range("B2", sht.cells(sht.cells(1,"B").end("down").row, "B")).value
9 for i in range(len(arr)): #遍历每行数据
10 m=re.sub(r"[\u4e00-\u9fa5]+\*","",arr[i]) #中文和星号替换为空
11 v=eval(str(m)) #剩下计算公式,计算结果
12 sht.cells(i+2,3).value=v #结果写入工作表
第1-6行代码的说明与上例的相同,请参阅。
第7-8行获取第1个工作表,用range对象的end方法获取数据的行数,把第2列的数据保存在列表arr中。
第9-12行用1个for循环计算各班总人数并写入第3列。
第9行用for循环遍历arr列表中的每个数据。
第10行用re.sub函数找出匹配正则表达式的子字符串,用""替换(即删除)。正则表达式r"[\u4e00-\u9fa5]+\*"中,"[\u4e00-\u9fa5]+\*"表示匹配1个或多个中文字符,中文后面跟*,也就是说,将数据中的中文和星号都去掉。中文和星号去掉以后,剩下的就是数字和加号,比如B2单元格中就剩下"5+15+8+5+3",这是1个可以计算的算式。
第11行用eval函数计算各算式的值,得到各班的总人数。
第12行把各班的总人数写入同1行中第3列的单元格中。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,计算出各班的总人数并写入第3列,如图7-2中处理后工作表所示。
重复
进行查找或替换时有时需要连续查找或替换多个某种类型的字符,这就是重复。重复次数可以是确定的,也可以是不确定的。比如Python正则表达式中用\d+表示1个以上的数字,重复次数不确定;\d{5}表示5个数字,重复次数是确定的。
Python正则表达式中表示重复的元字符如表7-6中所示。
表7-6 表示重复的元字符
| 元字符 | 说 明 | 元字符 | 说 明 |
|---|---|---|---|
| * | 重复零次或更多次 | {n} | 重复n次 |
| + | 重复一次或更多次 | {n,} | 重复n次或更多次 |
| ? | 重复零次或一次 | {n,m} | 重复n到m次 |
元字符*表示前面定义的字符可以重复0次或任意次,相当于 {0,}。下面给定1个字符串,找出所有W打头,后面跟或不跟数字的子字符串。
>>> impoirt re
>>> a="W123YZW85CW0DFWU"
>>> m=re.findall(r"W\d*",a)
>>> m
['W123', 'W85', 'W0', 'W']
注意列表中最后1个元素在W后面没有跟数字。
元字符+表示前面定义的字符可以重复1次或任意次,相当于 {1,}。下面给定1个字符串,找出所有W打头,后面跟1个或1个以上数字的子字符串。
>>> impoirt re
>>> a="W123YZW85CW0DFWU"
>>> m=re.findall(r"W\d+",a)
>>> m
['W123', 'W85', 'W0']
元字符?表示前面定义的字符可以重复0次或1次,相当于{0,1}。下面给定1个字符串,找出所有前后都是数字,中间有或没有小数点的子字符串。
>>> impoirt re
>>> a="W10.23RWA908C5..1"
>>> m=re.findall(r"\d+\.?\d+",a)
>>> m
['10.23', '908']
所有合法的数字被查找出来。
使用{}可以设置重复次数。{n}表示前面定义的字符重复n次。下面给定1个字符串,找出其中连续3个都是数字的子字符串。
>>> impoirt re
>>> a="WT123Pq89C"
>>> m=re.findall(r"\d{3}",a)
>>> m
['123']
{m,n}表示前面定义的字符的重复次数在1个指定的范围内取值,最小重复m次,最多重复n次。下面给定1个字符串,找出其中连续2个或3个都是数字的子字符串。
>>> impoirt re
>>> a="WT123Pq89C"
>>> m=re.findall(r"\d{2,3}",a)
>>> m
['123', '89']
{m,}表示前面定义的字符最少重复m次,相当于元字符+。下面给定1个字符串,找出其中连续2个以上都是数字的子字符串。
>>> impoirt re
>>> a="WT123Pq89C"
>>> m=re.findall(r"\d{2,}",a)
>>> m
['123', '89']
图7-3 提取日期
图7-3中处理前工作表中第1列的数据为某店9月份的采购信息,现要求提取日期并写入第2列。
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam13.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 p=r"\d{4}-\d{2}-\d{2}|\d{4}\.\d{2}\.\d{2}|\d{4}/\d{2}/\d{2}" #日期
9 arr=sht.range("A1", sht.cells(sht.cells(1,"A").end("down").row, "A")).value
10 for i in range(len(arr)):
11 m=re.search(p,arr[i])
12 sht.cells(i+1,2).value=m.group(0)
第1-6行代码的说明与7.3.1小节中Excel文件数据处理示例的相同,请参阅。
第7-9行获取第1个工作表,定义正则表达式p,用range对象的end方法获取数据的行数,把第1列的数据保存在列表arr中。正则表达式中用多个大括号定义4位和2位的数字来表示年份、月份和日期,用 | 间隔不同的日期格式。
第10-12行用1个for循环提取日期并写入第2列。
第10行用for循环遍历arr列表中的每个数据。
第11行用re.search函数找出第1个匹配正则表达式的子字符串,以Match对象返回给m。
第12行用m.group(0)获取匹配结果,并写入同1行中第2列的单元格中。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,提取日期并写入第2列,如图7-3中处理后工作表所示。
图7-4中处理前工作表中第1列的文字排版不规整,希望整理成处理后工作表中第2列的形式。观察发现,调整的关键在于定位各条目编号,在条目前面进行换行即可。条目编号前面不是数字,后面跟小数点,小数点后面跟的不是数字。
图7-4 整理数据
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam23.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 p=r"\D\d+\.\D" #正则表达式
9 arr=sht.range("A1", sht.cells(sht.cells(1,"A").end("down").row, "A")).value
10 for i in range(len(arr)): #遍历每条数据
11 m=re.findall(p,arr[i]) #所有匹配项,返回给列表m
12 sp=re.split(p,arr[i]) #匹配项为分隔符进行分割,结果返回给列表sp
13 s=sp[0]
14 for j in range(len(m)): #遍历所有匹配性
15 s+=m[j][0]+"\n"+m[j][1]+m[j][2]+sp[j+1] #拼接sp和m
16 sht.cells(i+1,2).value=s
第1-6行代码的说明与7.3.1小节中Excel文件数据处理示例的相同,请参阅。
第7-9行获取第1个工作表,定义正则表达式p,用range对象的end方法获取数据的行数,把第1列的数据保存在列表arr中。正则表达式定义搜索规则为数字前面不是数字,后面跟小数点,再跟非数字。
第10-18行用for循环实现数据整理。
第10行用for循环遍历arr列表中的每个数据。
第11行用re.findall函数找出匹配规则的所有子字符串,以列表形式返回给m。输出m。以第1条数据为例,此时输出的m为:
['。2.打', '。3.点', '。4.弹', '。5.点']
第12行用re.split函数进行分割,分隔符为匹配子字符串,结果以列表形式返回给sp。输出sp。以第1条数据为例,此时输出的sp为:
['1.找到我们的excel,找到之后打开', '开之后我们看到最最左下角有一个选项的地方,点击选项', '击选项之后我们默认就会进入到设置里面了,如果是在编辑状态的话点击菜单', '出其他菜单的选项,这里我们找到其他命令', '击其他命令即可进入。 ']
第13-15行拼接sp列表元素和m列表元素,注意第15行,在条目编号前添加了\n,进行换行。
第16行将拼接的结果写入同1行中第2列的单元格中。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,整理数据并将结果写入第2列,如图7-4中处理后工作表所示。
字符类
使用前面的方法可以查找指定的数字、字母、数字或空白,但是如果给定的是一个字符集,要求查找的字符只在这个集合中取或者在这个集合外取,就要用到中括号。中括号定义字符集的方式如表7-7中所示。
表7-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个字符。
下面给定1个字符串,用re.findall函数找出字符串中与方括号中任意字符匹配的字符。
>>> impoirt re
>>> a="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
>>> m=re.findall("[AEIOU]",a)
>>> m
['A', 'E', 'I', 'O', 'U']
用re.findall函数找出字符串中与方括号中任意字符不匹配的字符。
>>> impoirt re
>>> a="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
>>> m=re.findall("[^AEIOU]",a)
>>> m
['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z']
给定1个字符串,用re.findall函数找出字符串中落在方括号中指定字符范围的字符。
>>> impoirt re
>>> a="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
>>> m=re.findall("[G-T]",a)
>>> m
['G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T']
给定1个字符串,用re.findall函数找出字符串中1-5的数字和G-T的字母。
>>> impoirt re
>>> a="ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
>>> m=re.findall("[1-5G-T]",a)
>>> m
['G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', '1', '2', '3', '4', '5']
查找字符串中的汉字,正则表达式中用中括号指定汉字范围。可以有两种指定方式,即[一-龥]和[\u4e00-\u9fa5]。后一种方式是以四位十六进制数表示的Unicode字符。汉字一的编码是4e00,最后一个代码是9fa5。
下面给定一个包含汉字的字符串,用re.findall函数找出其中的汉字,用re.sub函数将找出的汉字替换为""。
>>> impoirt re
>>> a="123 中 hwo 文 tr89 字符"
>>> m=re.findall("[\u4e00-\u9fa5]",a)
>>> m
['中', '文', '字', '符']
>>> m=re.sub("[\u4e00-\u9fa5]","",a)
>>> m
'123 hwo tr89 '
分支条件
假设有几种规则,只要满足其中一种即可完成匹配,就要用到分支条件。使用 | 将不同的规则进行分隔。比如数字后面跟重量单位,有的记录为公斤,有的记录为千克,可以用"\d+(公斤|千克) "进行提取,相当于"\d+公斤|d+千克"。
下面给定1个字符串,用re.findall函数查找子字符串"ABC"或以W打头后面跟数字的子字符串。
>>> impoirt re
>>> a="ABC1234W89T"
>>> m=re.findall(r"ABC|W\d+",a)
>>> m
['ABC', 'W89']
下面给定的字符串中数字后面跟公斤、kg或千克,用分支条件编写正则表达式进行查找。
>>> impoirt re
>>> a="10公斤 20kg 30千克"
>>> m=re.finditer(r"\d+(公斤|千克|kg)",a)
>>> for i in m:
print(i.group(0))
10公斤
20kg
30千克
捕获分组和非捕获分组
正则表达式中存在有子表达式的情况,子表达式用小括号指定并作为一个整体进行操作。比如,下面代码中的正则表达式"((ABC){2})"将"ABC"作为一个整体重复2次。
>>> impoirt re
>>> a="ABCABCWTU238"
>>> m=re.search("((ABC){2})",a)
>>> m.group()
'ABCABC'
使用小括号对正则表达式进行分组时,会自动分配组号。分配组号的原则是从左到右,从外到内。使用组号可以对对应的分组进行反向引用。
下面的代码中,正则表达式r"(WT)\d+\1"匹配原始字符串中前后都是"WT",中间是1个或多个数字的子字符串。注意其中的\1表示小括号内的"WT",这个分组自动分配组号1,使用\1进行反向引用。
>>> impoirt re
>>> a="abcWT12389WT"
>>> m=re.finditer(r"(WT)\d+\1",a)
>>> for i in m:
print(i.group())
WT12389WT
匹配结果"WT12389WT "两端都是"WT",中间全是数字,满足匹配要求。
下面的示例演示有更多分组的情况。正则表达式r"((WT){2})((PR){2})\d+\2\4"中一共有4对小括号,前面两层后面两层,下面探查各小括号对应的分组的编号。
>>> impoirt re
>>> a="abWTWTPRPR123WTPR56"
>>> m=re.search(r"((WT){2})((PR){2})\d+\2\4",a)
>>> m.group(1)
'WTWT'
>>> m.group(2)
'WT'
>>> m.group(3)
'PRPR'
>>> m.group(4)
'PR'
结果显示,按照从左到右的原则,首先给左边的两层小括号对应的分组编号,此时按照从外到内的顺序编号。外层小括号中为" (WT){2}",即匹配"WTWT";内存小括号中为"WT",它们对应于分组编号1和2。右边两层小括号的情况类似,匹配第3个分组"PRPR"和第4个分组"PR"。
上面用小括号定义的分组,每个分组都自动进行编号,并可以用Match对象的group方法进行捕获,匹配结果保存到内存,称为捕获分组。但有时候,我们并不关注匹配到的内容,即分组参与匹配,但没有必要进行捕获,不用在内存中保存匹配到的内容。此时仍然用小括号进行分组,但是在小括号里的最前端加上"?:",如(?:\d{3})。这种分组称为非捕获分组。非捕获分组不参与编号,不在内存保存匹配结果,所以能节省内存空间,提高工作效率。
下面给定一个原始字符串,正则表达式为r"(?:ab)(CD)\d+\1",其中两个分组,第1个分组在小括号内的最前端有"?:",为非捕获分组。
>>> impoirt re
>>> a="abCD123CDbc"
>>> m=re.finditer(r"(?:ab)(CD)\d+\1",a)
>>> for i in m:
print(i.group())
abCD123CD
用re.finditer函数获取匹配迭代器,用for循环获取匹配结果。结果显示,匹配字符串中是包括"ab"的。
下面用re.search函数进行查找,返回Match对象m,调用该对象的groups属性查看各分组的子字符串。
>>> m=re.search(r"(?:ab)(CD)\d+\1",a)
>>> m.groups()
('CD',)
仅返回1个分组结果"CD"。此结果说明第1个分组因为声明为非捕获分组,不参与编号,也不保存。
将图7-5中处理前工作表中第1列的数据整理成处理后工作表中的形式,将连续重复的字符组成的子字符串提取出来放到右侧各单元格中。思路是提取字母,对它应用捕获分组,然后通过反向引用重复它,即r"([a-z])\1*"。不区分大小写。
图7-5 用捕获分组和反向引用提取数据
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam24.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 p=r"([a-z])\1*" #正则表达式,连续重复的字符
9 arr=sht.range("A1", sht.cells(sht.cells(1,"A").end("down").row, "A")).value
10 for i in range(len(arr)): #遍历每个字符串
11 m=re.finditer(p,arr[i],re.I) #找到全部匹配文本,不区分大小写
12 num=1
13 for j in m:
14 num+=1
15 sht.cells(i+1,num).value=j.group(0) #匹配文本写入工作表
第1-6行代码的说明与7.3.1小节中Excel文件数据处理示例的相同,请参阅。
第7-9行获取第1个工作表,定义正则表达式p,用range对象的end方法获取数据的行数,把第1列的数据保存在列表arr中。正则表达式中提取字母,对它应用捕获分组,然后通过反向引用重复它。
第10-15行用1个for循环提取重复字符组成的子字符串并写入后面个单元格。
第10行用for循环遍历arr列表中的每个数据。
第11行用re.finditer函数找出匹配正则表达式的对象组成的迭代器m。
第12-15行用group()获取迭代器中各匹配结果,并写入右侧各单元格中,num记录当前迭代器中匹配结果的个数。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,提取重复字符组成的子字符串并写入后面各单元格,如图7-5中处理后工作表所示。
图7-6中处理前工作表中A1单元格中的数据为某次食材采购的记录,现要求整理成处理后工作表中第2列和第3列所示的比较整齐的形式。处理思路第1步是将各食材和它们的采购金额提取出来,在正则表达式中对食材名称和采购金额进行捕获分组,这样输出的时候可以将食材名称和采购金额用分组区分开来并分两列写入。
图7-6 用捕获分组整理数据
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam25.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 p=r"([一-龢]{1,}) (\d+\.?\d*元)" #一个以上汉字后跟数字再跟元,有分组
9 arr=sht.range("A1").value #原始字符串
10 m=re.finditer(p,arr) #查找匹配文本,可迭代对象形式返回
11 num=1
12 for i in m: #遍历全部匹配文本
13 num+=1
14 sht.cells(num,2).value=i.group(1) #写入分组1,食材名称
15 sht.cells(num,3).value=i.group(2) #写入分组2,采购金额
第1-6行代码的说明与7.3.1小节中Excel文件数据处理示例的相同,请参阅。
第7-9行获取第1个工作表,定义正则表达式p,用range属性获取A1单元格的数据保存在arr中。正则表达式中将食材名称和采购金额进行分组,食材名称为1个以上的汉字,采购金额为数字,带或不带小数点,后面跟单位"元"。
第10行用re.finditer函数获取匹配对象组成的迭代器m。
第11-15行用for循环通过提取各匹配对象中的分组获取食材名称和采购金额,并写入第2列和第3列。num是计数变量,记录匹配对象的个数,即采购食材的个数。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,提取食材名称和采购金额并写入第2列和第3列,如图7-6中处理后工作表所示。
零宽断言
零宽断言用于查找指定内容之前或之后的内容,不包括指定内容。有两种类型,即
零宽度正预测先行断言:表达式为(?=exp),查找exp表示的内容之前的内容。
零宽度正回顾后发断言:表达式为(?<=exp),查找exp表示的内容之后的内容。
组合上面两种情况,可以查找指定内容之间的内容。
下面给定原始字符串,要求提取出单位公斤前面的数字,只提取数字。使用零宽度正预测先行断言进行提取。
>>> impoirt re
>>> a="10公斤 20公斤 30公斤"
>>> m=re.finditer(r"\d+(?=公斤)",a) #只取单位之前的数字
>>> for i in m:
print(i.group())
10
20
30
正则表达式r"\d+(?=公斤)"表示匹配"公斤"前面的数字,不包括"公斤"。结果显示匹配正确。
下面给定原始字符串,要求提取出"同学"、"战友"、"师兄"等称谓后面的姓名。使用零宽度正回顾后发断言进行提取。
>>> impoirt re
>>> a="同学李海 战友王刚 师兄张三"
>>> m=re.finditer(r"(?<=同学|战友|师兄)\w+",a) #只取称呼后面的姓名
>>> for i in m:
print(i.group())
李海
王刚
张三
正则表达式r"(?<=同学|战友|师兄)\w+"表示匹配"同学"、"战友"或"师兄"等称谓后面的子字符串。各称谓使用分支条件进行匹配。匹配的结果不包括称谓。
图7-7中处理前工作表中第2列的数据为多次采购食材的记录,现要求计算每次采购的食材的总重量。思路是使用零宽度正预测先行断言提取出重量单位前面的数字进行累加。
图7-7 用零宽断言进行数据汇总
1 import xlwings as xw
2 import os
3 import re
4 root = os.getcwd()
5 app = xw.App(visible=True, add_book=False)
6 wb=app.books.open(fullname=root+r"\Sam29.xlsx",read_only=False)
7 sht=wb.sheets(1)
8 p=r"\d+\.?\d*(?=(公斤|千克|kg))" #匹配单位前的数字
9 arr=sht.range("B2", sht.cells(sht.cells(1,"B").end("down").row, "B")).value
10 for i in range(len(arr)): #遍历每条数据
11 sm=0
12 m=re.finditer(p,arr[i]) #找到所有匹配数据
13 for j in m: #遍历匹配数据
14 sm+=int(j.group(0)) #求它们的和,就是总重量
15 sht.cells(i+2,3).value=sm
第1-6行代码的说明与7.3.1小节中Excel文件数据处理示例的相同,请参阅。
第7-9行获取第1个工作表,定义正则表达式p,用range对象的end方法获取数据的行数,把第2列表头外的数据保存在列表arr中。正则表达式中小括号内使用了零宽度正预测先行断言,不同的重量单位用分支条件进行匹配,小括号前面为数字,包含或不包含小数点。该表达式提取各食材的重量数据。
第10-15行用for循环提取食材重量数据,将累加结果写入右边的单元格。
第10行用for循环遍历arr列表中的每个数据。
第12行用re.finditer函数找出匹配正则表达式的子字符串,即各重量单位前的数字,以迭代器对象返回给m。
第13-14行用for循环对各食材的重量进行累加。
第15行将累加结果写入右边的单元格中。
在Python IDLE文件脚本窗口,在"Run"菜单中单击"Run Module"选项,计算各次采购食材的总重量并写入第3列,如图7-7中处理后工作表所示。
负向零宽断言
负向零宽断言用于断言指定位置的前面或后面不能匹配指定的表达式。有两种类型,即
零宽度负预测先行断言:表达式为(?:exp),断言此位置的后面不能匹配表达式exp。
零宽度负回顾后发断言:表达式为(?<!exp),断言此位置的前面不能匹配表达式exp。
下面给定原始字符串,要求匹配数字123前面是字母、数字或下划线,后面不能跟大写字母。使用零宽度负预测先行断言进行匹配。
>>> impoirt re
>>> a="5123Wgh123hp123456"
>>> m=re.finditer("\w123(?![A-Z])",a)
>>> for i in m:
print(i.group())
h123
p123
可见,给定字符串中第1个"123"因为后面跟了大写字母W,不能匹配。
下面给定原始字符串,要求匹配不包含"who "的单词。使用零宽度负预测先行断言进行匹配。
>>> impoirt re
>>> a="dwho efgh whow"
>>> m=re.finditer(r"\b((?!who)\w)+\b",a)
>>> for i in m:
print(i.group())
efgh
下面给定原始字符串,要求匹配前面不是小写字母的5位数字。使用零宽度负回顾后发断言进行匹配。
>>> impoirt re
>>> a="abcD1234567"
>>> m=re.search(r"(?<![a-z])\d{5}",a)
>>> m.group()
'12345'
注意,负向零宽断言进行不定长表达式的匹配时常常出错。所谓不定长表达式,是诸如\w+, \d*等长度不确定的表达,此时如果可以,改为定长的表达,例如\w{4}, \d{3}等。
下面给定原始字符串,要求匹配不以"ing"结尾的单词。使用零宽度负预测先行断言进行匹配。
>>> impoirt re
>>> a="eating get climb"
>>> m=re.finditer(r"\b\w+(?!ing\b) ",a)
>>> for i in m:
print(i.group())
eating
get
climb
正则表达式中将\w+改为\w{3},查看匹配结果。
>>> m=re.finditer(r"\b\w{3}(?!ing\b)",a)
>>> for i in m:
print(i.group())
get
cli
此时匹配结果虽不精确,但能指示正确的方向。注意,这种方法也只能匹配ing前面正好有3个字符的情况,如果前面有4个字符,则匹配失败。所以,使用负向零宽断言时要注意这个情况。
贪婪与懒惰
前面介绍*和+时,是匹配尽可能多的字符,称为贪婪匹配。但有时候需要匹配尽可能少的字符,称为懒惰匹配,方法是在贪婪匹配的后面添加一个问号。
常见的懒惰匹配格式如表7-8中所示。
表7-8 懒惰匹配
| 懒惰匹配格式 | 说 明 |
|---|---|
| *? | 重复任意次,但尽可能少重复 |
| +? | 重复1次或更多次,但尽可能少重复 |
| ?? | 重复0次或1次,但尽可能少重复 |
| {n,m}? | 重复n到m次,但尽可能少重复 |
| {n,}? | 重复n次以上,但尽可能少重复 |
下面给定原始字符串,分别使用贪婪匹配和懒惰匹配比较匹配结果。
>>> impoirt re
>>> a=" 123 abc53 59wt "
>>> m=re.finditer("\s.+\s",a)
>>> for i in m:
print(i.group())
123 abc53 59wt
正则表达式"\s.+\s"中没有?,此为贪婪匹配,在两个空白符之间匹配尽可能多的字符,所以匹配结果是整个字符串。
>>> m=re.finditer("\s.+?\s",a)
>>> for i in m:
print(i.group())
123
abc53
59wt
正则表达式"\s.+?\s"中+后面有?,此为懒惰匹配,在两个空白符之间匹配尽可能少的字符,所以匹配结果是空格间隔的三个子字符串。