编写正则表达式

17.2节介绍了给定一个正则表达式时在MATLAB和Python中怎样使用它完成文本处理任务。本节介绍正则表达式的语法规则,介绍怎样写出能解决问题的正则表达式。[大谦MATLAB,dqmatlab点com]

元字符

元字符是在正则表达式中具有特殊含义的字符,其含义超出了自己本身的含义。比如用\d表示数字,用\s表示空白。

【MATLAB】

MATLAB中常见的元字符如表17-4中所示。

表17-4 常见元字符

元字符 说 明 元字符 说 明
. 匹配除换行符以外的任意字符 \n 匹配换行符
\w 匹配字母、数字、下划线或汉字 \a 匹配警报(蜂鸣)
\s 匹配任意空白符 \b 匹配退格符
\d 匹配数字 \f 匹配换页符
^ 匹配字符串的开始 \r 匹配回车符
$ 匹配字符串的结束 \t 匹配水平制表符
\< 匹配单词开头 \v 匹配垂直制表符
\> 匹配单词结尾 \char 从字面上匹配具有特殊含义的任意字符

【Python】

元字符是在正则表达式中具有特殊含义的字符,其含义超出了自己本身的含义。比如Python正则表达式中,用\d表示数字,用\s表示空白。常见的元字符如表17-5中所示。

表17-5 常见元字符

元字符 说 明 元字符 说 明
. 匹配除换行符以外的任意字符 ^ 匹配字符串的开始
\w 匹配字母、数字、下划线或汉字 $ 匹配字符串的结束
\s 匹配任意空白符 \n 匹配一个换行符
\d 匹配数字 \r 匹配一个回车符
\b 匹配单词的开始或结束 \t 匹配一个制表符

【MATLAB】【Python】

一般情况下是指定要查找的字符或在指定的范围内进行查找,但有时情况会反过来,即排除指定的字符或在指定的字符范围之外进行查找。这种情况下使用表示反义的元字符,如用\D表示非数字的字符,用\S表示非空白的字符。常见的反义元字符如表17-6中所示。

表17-6 反义元字符

反义元字符 说 明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

【MATLAB】

下面给定原始字符串,用regexp函数查找其中的全部数字,用regexprep函数将所有数字替换为空。单个的数字用元字符\d表示。

code.matlab
>> a='BC_101PW%';  %原始字符串
>> m0=regexp(a, '\d&#x27;,'match')  %查找所有数字
m0=
  1×3 string 数组
    '1'    '0'    '1'
>> for i=1:l
  str2num(m0(i))
end
ans =
     1
ans =
     0
ans =
     1
>> ms=regexprep(a,7;\d','')  %所有数字替换为空(删除)
ms=
    'BC_PW%'

【Python】

code.python
>>> import re
>>> a='BC_101PW%'  #原始字符串
>>> m0=re.findall(r'\d&#x27;,a)  #查找所有数字
>>> m0
['1', '0', '1']
>>> for i in m0:  #逐个输出数字
		print(i)
1
0
1
>>> ms=re.sub(r'\d7;,'',a)  #所有数字替换为空(删除)
>>> ms
'BC_PW%'

下面的示例测试元字符\<,它表示单词的开头。正则表达式为'\<C\d',表示匹配字符串必须是原始字符串以C打头或C前面为空格,C的后面跟数字。匹配的字符串置换为空。

【MATLAB】

code.matlab
>> a='C5dC56 C5'
>> m=regexprep(a,'\<C\d','')
m =
    'dC56 '

【Python】

code.python
>>> import re
>>> a='C5dC56 C5'
>>> m=re.sub(r'\bC\d','',a)
>>> m
'dC56 '

因为第1个C5位于原始字符串的开头,满足C加数字的条件,匹配;第2个C5前面为空格,满足\<的条件。将它们置换为空后剩下的字符串即为'dC56 '。

元字符^限制字符在原始字符串的最前面,如^\d表示原始字符串以数字打头。下面给定原始字符串,如果它以一个以上的数字打头,返回该数字。

【MATLAB】

code.matlab
>> a='12345my09';
>> m=regexp(a,'^\d+','match')
m =
  1×1 cell 数组
    {'12345'}

【Python】

code.python
>>> import re
>>> a='12345my09'
>>> m=re.findall(r'^\d+',a)
>>> for i in m:
		print(i)
12345

因为12345位于原始字符串打头位置,匹配;而09虽然也是数字,但不在打头位置,不匹配。正则表达式中的加号是表示重复的元字符,前面为d,表示一个以上的数字。

下面的代码中,\D表示不是数字的字符,元字符$限制字符在原始字符串的结尾处,如C$表示最后一个字符是C。

【MATLAB】

code.matlab
>> a='12345my09W';
>> m=regexp(a,'\d+\D','match')
m =
  1×2 cell 数组
    {'12345m'}    {'09W'}
>> m=regexp(a,'\d+\D$','match')
m =
  1×1 cell 数组
    {'09W'}

【Python】

code.python
>>> import re
>>> a='12345my09W'
>>> m=re.findall(r'\d+\D',a)
>>> m
['12345m', '09W']
>>> m=re.findall(r'\d+\D$',a)
>>> m
['09W']

重复

进行查找或替换时有时需要连续查找或替换多个某种类型的字符,这就是重复。重复次数可以是确定的,也可以是不确定的。比如用\d+表示1个以上的数字,重复次数不确定;\d{5}表示5个数字,重复次数是确定的。

正则表达式中表示重复的元字符如表17-7中所示。

表17-7 表示重复的元字符

元字符 说 明 元字符 说 明
* 重复零次或更多次 {n} 重复n次
+ 重复一次或更多次 {n,} 重复n次或更多次
? 重复零次或一次 {n,m} 重复n到m次

元字符*表示前面定义的字符可以重复0次或任意次,相当于 {0,}。下面给定1个字符串,找出所有W打头,后面跟或不跟数字的子字符串。

【MATLAB】

code.matlab
>> a='W123YZW85CW0DFWU';
>> m=regexp(a,'W\d*','match')
m =
  1×4 cell 数组
    {'W123'}    {'W85'}    {'W0'}    {'W'}

【Python】

code.python
>>> import re
>>> a='W123YZW85CW0DFWU'
>>> m=re.findall(r'W\d*',a)
>>> m
['W123', 'W85', 'W0', 'W']

注意列表中最后1个元素在W后面没有跟数字。

元字符+表示前面定义的字符可以重复1次或任意次,相当于 {1,}。下面给定1个字符串,找出所有W打头,后面跟1个或1个以上数字的子字符串。

【MATLAB】

code.matlab
>> a='W123YZW85CW0DFWU';
>> m=regexp(a,'W\d+','match')
m =
  1×3 cell 数组
    {'W123'}    {'W85'}    {'W0'}

【Python】

code.python
>>> import re
>>> a='W123YZW85CW0DFWU'
>>> m=re.findall(r'W\d+',a)
>>> m
['W123', 'W85', 'W0']

元字符?表示前面定义的字符可以重复0次或1次,相当于{0,1}。下面给定1个字符串,找出所有前后都是数字,中间有或没有小数点的子字符串。

【MATLAB】

code.matlab
>> a='W10.23RWA908C5..1';
>> m=regexp(a,'\d+\.?\d+','match')
m =
  1×2 cell 数组
    {'10.23'}    {'908'}

【Python】

code.python
>>> import re
>>> a='W10.23RWA908C5..1'
>>> m=re.findall(r'\d+\.?\d+',a)
>>> m
['10.23', '908']

所有合法的数字被查找出来。

使用{}可以设置重复次数。{n}表示前面定义的字符重复n次。下面给定1个字符串,找出其中连续3个都是数字的子字符串。

【MATLAB】

code.matlab
>> a='WT123Pq89C';
>> m=regexp(a,'\d{3}','match')
m =
  1×1 cell 数组
    {'123'}

【Python】

code.python
>>> import re
>>> a='WT123Pq89C'
>>> m=re.findall(r'\d{3}',a)
>>> m
['123']

{m,n}表示前面定义的字符的重复次数在1个指定的范围内取值,最小重复m次,最多重复n次。下面给定1个字符串,找出其中连续2个或3个都是数字的子字符串。

【MATLAB】

code.matlab
>> a='WT123Pq89C';
>> m=regexp(a,'\d{2,3}','match')
m =
  1×2 cell 数组
    {'123'}    {'89'}

【Python】

code.python
>>> import re
>>> a='WT123Pq89C'
>>> m=re.findall(r'\d{2,3}',a)
>>> m
['123', '89']

{m,}表示前面定义的字符最少重复m次,相当于元字符+。下面给定1个字符串,找出其中连续2个以上都是数字的子字符串。

【MATLAB】

code.matlab
>> m=regexp(a,'\d{2,}','match')
m =
  1×2 cell 数组
    {'123'}    {'89'}

【Python】

code.python
>>> import re
>>> a='WT123Pq89C'
>>> m=re.findall(r'\d{2,}',a)
>>> m
['123', '89']

字符类

使用前面的方法可以查找指定的数字、字母、数字或空白,但是如果给定的是一个字符集,要求查找的字符只在这个集合中取或者在这个集合外取,就要用到中括号。中括号定义字符集的方式如表17-8中所示。

表17-8 中括号的用法

应用格式示例 说 明
[adwkf] 查找的字符是中括号内字符中的一个
[^adwkf] 查找的字符不是中括号内字符的就行
[b-f] 查找的字符是b到f中的一个
[^b-f] 查找的字符不是b到f中的一个
[17-5] 查找的字符是2到5中的一个
[17-46-9] 查找的字符是2到4或6到9中的一个
[a-w17-5A-W] 查找的字符是a到w, 2到5或A到W范围内的一个
[^一-龥]或[^\u4e00-\u9fa5] 查找的字符是中文字符

使用方括号[]包含个字符集,能够匹配其中任意1个字符。使用[^],则不匹配方括号内的字符,只能匹配该字符集之外的任意1个字符。

下面给定1个字符串,用regexp函数找出字符串中与方括号中任意字符匹配的字符。

【MATLAB】

code.matlab
>> a='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
>> m=regexp(a,'[AEIOU]','match')
m =
  1×5 cell 数组
    {'A'}    {'E'}    {'I'}    {'O'}    {'U'}

【Python】

code.python
>>> import re
>>> a='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
>>> m=re.findall('[AEIOU]',a)
>>> m
['A', 'E', 'I', 'O', 'U']

用regexp函数找出字符串中与方括号中任意字符不匹配的字符。

【MATLAB】

code.matlab
>> a='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
>> m=regexp(a,'[^AEIOU]','match')
m =
  1×21 cell 数组
  列 17
    {'B'}    {'C'}    {'D'}    {'F'}    {'G'}    {'H'}    {'J'}
  列 814
    {'K'}    {'L'}    {'M'}    {'N'}    {'P'}    {'Q'}    {'R'}
  列 1521
    {'S'}    {'T'}    {'V'}    {'W'}    {'X'}    {'Y'}    {'Z'}

【Python】

code.python
>>> import 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个字符串,用regexp函数找出字符串中落在方括号中指定字符范围的字符。

【MATLAB】

code.matlab
>> a='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
>> m=regexp(a,'[G-T]','match')
m =
 1×14 cell 数组
  列 17
    {'G'}    {'H'}    {'I'}    {'J'}    {'K'}    {'L'}    {'M'}
  列 814
    {'N'}    {'O'}    {'P'}    {'Q'}    {'R'}    {'S'}    {'T'}

【Python】

code.python
>>> import 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个字符串,用regexp函数找出字符串中1-5的数字和G-T的字母。

【MATLAB】

code.matlab
>> a='ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
>> m=regexp(a,'[1-3R-T]','match')
m =
  1×6 cell 数组
    {'R'}    {'S'}    {'T'}    {'1'}    {'2'}    {'3'}

【Python】

code.python
>>> import re
>>> a='ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
>>> m=re.findall('[1-3R-T]',a)
>>> m
['R', 'S', 'T', '1', '2', '3']

查找字符串中的汉字,正则表达式中用中括号指定汉字范围为[一-龥]。

下面给定一个包含汉字的字符串,用regexp函数找出其中的汉字,用regexprep函数将找出的汉字替换为空字符串。

【MATLAB】

code.matlab
>> a='123 中 hwo 文 tr89 字符';
>> m=regexp(a,'[一-龥]','match')
m =
  1×4 cell 数组
    {'中'}    {'文'}    {'字'}    {'符'}
>> m=regexprep(a,'[一-龥]','')
m =
    '123  hwo  tr89 '

【Python】

code.python
>>> import 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个字符串,查找子字符串'ABC'或以W打头后面跟数字的子字符串。

【MATLAB】

code.matlab
>> a='ABC1234W89T';
>> m=regexp(a,'ABC|W\d+','match')
m =
  1×2 cell 数组
    {'ABC'}    {'W89'}

【Python】

code.python
>>> import re
>>> a='ABC1234W89T'
>>> m=re.findall(r'ABC|W\d+',a)
>>> m
['ABC', 'W89']

下面给定的字符串中数字后面跟公斤、kg或千克,用分支条件编写正则表达式进行查找。

【MATLAB】

code.matlab
>> a='10公斤 20kg 30千克';
>> m=regexp(a,'\d+(公斤|千克|kg)','match')
m =
  1×3 cell 数组
    {'10公斤'}    {'20kg'}    {'30千克'}

【Python】

code.python
>>> import 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次。

【MATLAB】

code.matlab
>> a='ABCABCWTU238';
>> m=regexp(a,'((ABC){2})','match')
m =
  1×1 cell 数组
    {'ABCABC'}

【Python】

code.python
>>> import re
>>> a='ABCABCWTU238'
>>> m=re.search('((ABC){2})',a)
>>> m.group()
'ABCABC'

使用小括号对正则表达式进行分组时,会自动分配组号。分配组号的原则是从左到右,从外到内。使用组号可以对对应的分组进行反向引用。

下面的代码中,正则表达式r'(WT)\d+\1'匹配原始字符串中前后都是'WT',中间是1个或多个数字的子字符串。注意其中的\1表示小括号内的'WT',这个分组自动分配组号1,使用\1进行反向引用。

【MATLAB】

code.matlab
>> a='abcWT12389WT';
>> m=regexp(a,'(WT)\d+\1','match')
m =
  1×1 cell 数组
    {'WT12389WT'}

【Python】

code.python
>>> import re
>>> a='abcWT12389WT'
>>> m=re.finditer(r'(WT)\d+\1',a)
>>> for i in m:
		print(i.group())
WT12389WT

匹配结果'WT12389WT '两端都是'WT',中间全是数字,满足匹配要求。

【MATLAB】

code.matlab
>> a='abCD123CDbc';
>> m=regexp(a,'(ab)(CD)','tokens')
m =
  1×1 cell 数组
    {1×2 cell}
>> m{1}{1}
ans =
    'ab'
>> m{1}{2}
ans =
   'CD'

【Python】

code.python
>>> import 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'

上面用小括号定义的分组,每个分组都自动进行编号,匹配结果保存到内存,称为捕获分组。但有时候,我们并不关注匹配到的内容,即分组参与匹配,但没有必要进行捕获,不用在内存中保存匹配到的内容。此时仍然用小括号进行分组,但是在小括号里的最前端加上'?:',这种分组称为非捕获分组。非捕获分组不参与编号,不在内存保存匹配结果,所以能节省内存空间,提高工作效率。

下面给定一个原始字符串,正则表达式为'(?:ab)(CD)',其中两个分组,第1个分组在小括号内的最前端有'?:',为非捕获分组。

【MATLAB】

code.matlab
>> a='abCD123CDbc';
>> m=regexp(a,'(?:ab)(CD)','tokens')
m =
  1×1 cell 数组
    {1×1 cell}
>> m{1}{1}
ans =
    'CD'

【Python】

code.python
>>> import re
>>> a='abCD123CDbc'
>>> m=re.findall(r'(?:ab)(CD)\d+\1',a)
>>> m
['CD']

仅返回1个分组结果'CD'。此结果说明第1个分组因为声明为非捕获分组,不参与编号,也不保存。

零宽断言

零宽断言用于查找指定内容之前或之后的内容,不包括指定内容。有两种类型,即

零宽度正预测先行断言:表达式为(?=exp),查找exp表示的内容之前的内容。

零宽度正回顾后发断言:表达式为(?<=exp),查找exp表示的内容之后的内容。

组合上面两种情况,可以查找指定内容之间的内容。

下面给定原始字符串,要求提取出单位公斤前面的数字,只提取数字。使用零宽度正预测先行断言进行提取。

【MATLAB】

code.matlab
>> a='10公斤 20公斤 30公斤';
>> m=regexp(a,'\d+(?=公斤)&#x27;,'match')  %只取单位之前的数字
m =
  1×3 cell 数组
    {'10'}    {'20'}    {'30'}

【Python】

code.python
>>> import re
>>> a='10公斤 20公斤 30公斤'
>>> m=re.findall(r'\d+(?=公斤)',a)
>>> m
['10', '20', '30']

正则表达式r'\d+(?=公斤)'表示匹配'公斤'前面的数字,不包括'公斤'。结果显示匹配正确。

下面给定原始字符串,要求提取出'同学'、'战友'、'师兄'等称谓后面的姓名。使用零宽度正回顾后发断言进行提取。

【MATLAB】

code.matlab
>> a='同学李海 战友王刚 师兄张三';
>> m=regexp(a,'(?<=同学|战友|师兄)\w+','match')  %只取称呼后面的姓名
m =
  1×3 cell 数组
    {'李海'}    {'王刚'}    {'张三'}

【Python】

code.python
>>> import re
>>> a='同学李海 战友王刚 师兄张三'
>>> m=re.findall(r'(?<=同学|战友|师兄)\w+',a)
>>> m
['李海', '王刚', '张三']

正则表达式r'(?<=同学|战友|师兄)\w+'表示匹配'同学'、'战友'或'师兄'等称谓后面的子字符串。各称谓使用分支条件进行匹配。匹配的结果不包括称谓。

负向零宽断言

负向零宽断言用于断言指定位置的前面或后面不能匹配指定的表达式。有两种类型,即

零宽度负预测先行断言:表达式为(?:exp),断言此位置的后面不能匹配表达式exp。

零宽度负回顾后发断言:表达式为(?<!exp),断言此位置的前面不能匹配表达式exp。

下面给定原始字符串,要求匹配数字123前面是字母、数字或下划线,后面不能跟大写字母。使用零宽度负预测先行断言进行匹配。

【MATLAB】

code.matlab
>> a='5123Wgh123hp123456';
>> m=regexp(a,'\w123(?![A-Z])','match')
m =
  1×2 cell 数组
    {'h123'}    {'p123'}

【Python】

code.python
>>> import re
>>> a='5123Wgh123hp123456'
>>> m=re.findall('\w123(?![A-Z])',a)
>>> m
['h123', 'p123']

可见,给定字符串中第1个'123'因为后面跟了大写字母W,不能匹配。

下面给定原始字符串,要求匹配前面不是小写字母的5位数字。使用零宽度负回顾后发断言进行匹配。

【MATLAB】

code.matlab
>> a='abcD1234567';
>> m=regexp(a,'(?<![a-z])\d{5}','match')
m =
  1×1 cell 数组
    {'12345'}

【Python】

code.python
>>> import re
>>> a='abcD1234567'
>>> m=re.findall(r'(?<![a-z])\d{5}',a)
>>> m
['12345']

贪婪与懒惰

前面介绍*和+时,是匹配尽可能多的字符,称为贪婪匹配。但有时候需要匹配尽可能少的字符,称为懒惰匹配,方法是在贪婪匹配的后面添加一个问号。

常见的懒惰匹配格式如表17-9中所示。

表17-9 懒惰匹配

懒惰匹配格式 说 明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

下面给定原始字符串,分别使用贪婪匹配和懒惰匹配比较匹配结果。

【MATLAB】

code.matlab
>> a=' 123  abc53  59wt ';
>> m=regexp(a,'\s.+\s','match')
m =
  1×1 cell 数组
    {' 123  abc53  59wt '}

【Python】

code.python
>>> import re
>>> a=' 123  abc53  59wt '
>>> m=re.findall('\s.+\s',a)
>>> m
[' 123  abc53  59wt ']

正则表达式'\s.+\s'中没有?,此为贪婪匹配,在两个空白符之间匹配尽可能多的字符,所以匹配结果是整个字符串。

【MATLAB】

code.matlab
>> m=regexp(a,'\s.+?\s','match')
m =
  1×3 cell 数组
    {' 123 '}    {' abc53 '}    {' 59wt '}

【Python】

code.python
>>> m=re.findall('\s.+?\s',a)
>>> m
[' 123 ', ' abc53 ', ' 59wt ']

正则表达式'\s.+?\s'中+后面有?,此为懒惰匹配,在两个空白符之间匹配尽可能少的字符,所以匹配结果是空格间隔的三个子字符串。