1. 引言
正则表达式就是一个表达式(也是一串字符),它定义了某种字符串模式——利用正则表达式,可以
对大段的文字进行复杂的查找、替换等。本文将以 Matlab 为编程语言,讲解正则表达式的概念和使用方法,
并将在文末以实例说明正则表达式的实践应用。
Matlab 提供的正则表达式函数有三个:
regexp——用于对字符串进行查找,大小写敏感;
regexpi——用于对字符串进行查找,大小写不敏感;
regexprep——用于对字符串进行查找并替换。
简要介绍一下这三个函数,以 regexpi 为例 —— 读者可以先跳过这里,看过全文之后再来看这里。
用法 1:
[start end extents match tokens names] = regexpi(\'str\', \'expr\')
start 为匹配字符串的起始位置;end 为匹配字符串的终止位置;extents 为扩展内容,和\'tokens\'指示符
一起用,指示出现 tokens 的位置;match 即找到的匹配字串;tokens 匹配正则表达式中标记(tokens)的字串;
names 为匹配到的命名标记的标记名。
用法 2:
若不需要所有的输出,可以用下面的方式有选择的输出。
[v1 v2 ...] = regexpi(\'str\', \'expr\', \'q1\', \'q2\', ...)
\'q1\'、\'q2\' ...... 为 \'start\'、\'end\'、\'tokens\'、\'tokensExtents\'、\'match\'、\'names\' 之一,意义与前文相同。v1、
v2...... 的输出顺序与 q1、q2...... 一致。
2. 单个字符的匹配
我们先从简单的开始 —— 以 regexpi 函数为例,不区分字符的大小写。假设你要搜索 \'cat\',搜索用
的正则表达式就是 \'cat\',这与文本编辑工具里常用的 CTRL+F 是一样的,即正则表达式 \'cat\' 匹配 \'cat\'、
\'Cat\'、\'cAt\'、\'CAt\'、\'caT\'、\'CaT\'、\'cAT\'、\'CAT\'。
为了方便,下面的叙述中字符串和正则表达式的\'\'都省略不写。
2.1 句点符号
. —— 匹配任意一个(只有一个)字符(包括空格)。
假设你在玩英文拼字游戏,想要找出三个字母的单词,而且这些单词必须以 \'t\' 字母开头,以 \'n\' 字母结束;另外,有一本英文字典,你可以用正则表达式搜索它的全部内容。要构造出这个正则表达式,你可以使用一个通配符 —— 句点符号 \'.\' 。这样,完整的表达式就是 t.n,它匹配 tan、ten、tin 和 ton,还匹配 t#n、tpn 甚至 t n,还有其他许多无意义的组合。这是因为句点符号匹配所有字符,包括空格,即:正则表达式 t.n 匹配 ten、tin、ton、t n、tpn、t#n、t@n 等。
Matlab 程序实例:
clear;clc
str=\'ten,&8yn2tin6ui>&ton, t n,-356tpn,$$$$t#n,4@).,t@nT&nY\';
pat=\'t.n\';
o1=regexpi(str,pat,\'start\') %用\'start\'指定输出 o1 为匹配正则表达式的子串的起始位置
o2=regexpi(str,pat,\'end\') %用\'end\'指定输出 o2 为匹配正则表达式的子串的结束位置
o3=regexpi(str,pat,\'match\') %用\'match\'指定输出 o3 为匹配正则表达式的子串
[o11,o22,o33]=regexpi(str,pat,\'start\',\'end\',\'match\') %同时输出起始位置和字串
输出为:
o22 = 3 8 13 18 23 28 33 36
o33 = \'ten\' \'tin\' \'ton\' \'t n\' \'tpn\' \'t#n\' \'t@n\' \'T&n\'
o1 = 1 10 18 23 31 39 48 51
o2 = 3 12 20 25 33 41 50 53
o3 = \'ten\' \'tin\' \'ton\' \'t n\' \'tpn\' \'t#n\' \'t@n\' \'T&n\'
o11 = 1 10 18 23 31 39 48 51
o22 = 3 12 20 25 33 41 50 53
o33 = \'ten\' \'tin\' \'ton\' \'t n\' \'tpn\' \'t#n\' \'t@n\' \'T&n\'
2.2 方括号符号
[oum] —— 匹配方括号中的任意一个。
为了解决句点符号匹配范围过于广泛这一问题,你可以在方括号([])里面指定看来有意义的字符。此时,只有方括号里面指定的字符才参与匹配。也就是说,正则表达式 t[aeio]n 只匹配 tan、Ten、tin 和 toN等。但 Tmn、taen 不匹配,因为在方括号之内你只能匹配单个字符。
Matlab 程序实例:
clear;clc
str=\'ten,&8yn2tin6ui>&ton, t n,-356tpn,$$$$t#n,4@).,t@nT&nY\';
pat=\'t[aeiou]n\';
[o11,o22,o33]=regexpi(str,pat,\'start\',\'end\',\'match\') %í?ê±ê?3??eê?????oí×ó′?
o11 = 1 10 18
o22 = 3 12 20
o33 = \'ten\' \'tin\' \'ton\'
2.3 方括号中的连接符
\'[c1-c2]\' —— 匹配从字符 c1 开始到字符 c2 结束的字母序列(按字母表中的顺序)中的任意一个。 例如 [a-c] 匹配 a、b、c、A、B、C,即正则表达式 t[a-z]n 匹配 tan、tbn、tcn、tdn、ten、……、txn、tyn、tzn。
Matlab 程序实例:
clear;clc
str=\'ten,&8yn2tin6ui>&ton, t n,-356tpn,$$$$t#n,4@).,t@nT&nY\';
pat=\'t[a-z]n\';
[o11,o22,o33]=regexpi(str,pat,\'start\',\'end\',\'match\')
o11 = 1 10 18 31
o22 = 3 12 20 33
o33 = \'ten\' \'tin\' \'ton\' \'tpn\'
2.4 特殊字符
\.等 —— 即由 \'\\' 引导的,代表有特殊意义或不能直接输入的单个字符。
在使用 fprintf 函数输出时我们常用 \'\n\' 来代替回车符,这里也是同样的道理,用 \n 在正则表达式中表示回车符。类似的还有 \t 横向制表符,\'\*\' 表示 \'*\' 等。后一种情况用在查询在正则表达式中有语法作用的字符,详见下文。
下面是一些匹配单个字符的转义字符正则表达式及所匹配的值。
\xN 或\x{N} 匹配八进制数值为 N 的字符
\oN 或\o{N} 匹配十六进制数值为 N 的字符
\a Alarm(beep)
\b Backspace
\t 水平 Tab
\n New line
\v 垂直 Tab
\f 换页符
\r 回车符
\e Escape
\c 某些在正则表达式中有语法功能或特殊意义的字符 c,要用 \c 来匹配,而不能直接用 c 匹配,例如 . 用正则表达式 \. 匹配,而 \ 用正则表达式 \\ 匹配。
Matlab 程序实例:
clear;clc
str=\'l.[a-c]i.$.a\';
pat1=\'.\';pat2=\'\.\';
o=regexpi(str,pat1,\'match\')
o1=regexpi(str,pat2,\'match\')
输出为:
o = \'l\' \'.\' \'[\' \'a\' \'-\' \'c\' \']\' \'i\' \'.\' \'$\' \'.\' \'a\'
o1 = \'.\' \'.\' \'.\'
2.5 类表达式
\w、\s 和 \d 等 —— 匹配某一类字符中的一个。
和上面的 \n 等表中的转义字符有所不同,\w、\s、\d 等匹配的不是某个特定的字符,而是某一类字符。具体说明如下:
\w 匹配任意的单个文字字符,相当于 [a-zA-Z0-9_];
\s 匹配任意的单个空白字符,相当于 [\t\f\n\r];
\d 匹配任意单个数字,相当于 [0-9];
\S 匹配除空白符以外的任意单个字符,相当于 [^\t\f\n\r] —— 方括号中的^表示取反;
\W 匹配任意单个字符,相当于 [^a-zA-Z0-9_];
\D 匹配除数字字符外的任意单个字符,相当于 [^0-9]。
Matlab 程序实例:
s=\'This city has a population of more than 1,000,000.\';
ptn=\'\d\';
regexp(s,ptn,\'match\')
输出为:
ans = \'1\' \'0\' \'0\' \'0\' \'0\' \'0\' \'0\'
3.字符串的匹配
3.1 多次匹配
例如需要匹配 \'ppp\',那么就可以用正则表达式 \'ppp\',还有一种更简单一点的记法 \'p{3}\'。正则表达式中的 \'{}\' 用来表示匹配前面的表达式的出现次数,即 \'p{2,3}\' 匹配 \'pp\' 和 \'ppp\'。除了 \'{}\',还有几个
字符,用在表示单个字符的正则表达式后面表示次数,如下所述:
expr? 与 expr 匹配的元素出现 0 或 1 次,相当于{0,1}
expr* 与 expr 匹配的元素出现 0 次或更多,相当于{0,}
expr+ 与 expr 匹配的元素出现 1 次或更多,相当于{1,}
expr{n} 与 expr 匹配的元素出现 n 次,相当于{n,n}
expr{n,} 与 expr 匹配的元素至少出现 n 次
expr{n,m} 与 expr 匹配的元素出现 n 次但不多于 m 次
假设我们要在文本文件中搜索美国的社会安全号码。这个号码的格式是 999-99-9999。用来匹配它的正则表达式为 [0-9]{3}\-[0-9]{2}\-[0-9]{4}。在正则表达式中,连字符(“-”)有着特殊的意义,因此,它的前面要加上一个转义字符 \。
如果希望连字符号可以出现,也可以不出现 —— 即 999-99-9999 和 999999999 都属于正确的格式。这时,你可以在连字符号后面加上 \'?\' 数量限定符。这样正则表达式为 [0-9]{3}\-?[0-9]{2}\-?[0-9]{4}。 另外,当我们使用 expr* 时,Matlab 将尽可能的匹配最长的字符子串。如:
>>str = \'<tr valign=top><td><a name="19184"></a>xyz\';
>>regexp(hstr, \'<.*>\', \'match\')
ans = \'<tr valign=top><td><a name="19184"></a>\'
如果我们希望匹配尽可能短的字符子串时,可以在上面我们使用的字符串后使用 \'?\',也就是 expr*?,
例如:
>>str = \'<tr valign=top><td><a name="19184"></a>xyz\';
>>regexp(hstr, \'<.*?>\', \'match\')
ans = \'<tr valign=top>\' \'<td>\' \'<a name="19184">\' \'</a>\'
这个表达式的执行过程是这样的,先执行 expr*,“游标”(如果有的话)就指到了与 expr* 匹配的字符子串的最末端,然后从那里开始再检查下一个字符与后面的表达式是否匹配,如果匹配就继续向前(如果一直成功则返回最长的字符串),如果不匹配则直接返回空。例如:
>>str = \'<tr valign=top><td><a name="19184"></a>xyz\';
>>regexp(hstr, \'<.*+>\', \'match\')
ans = {}
>>regexp(hstr, \'<.*+\', \'match\')
ans = \'<tr valign=top><td><a name="19184"></a>xyz\'
3.2 逻辑运算符
exp|exp2 表示或者满足 exp 或者满足 exp2。
(expr) 将 expr 标记为一组,匹配 expr,并将匹配的字符子串标记起来以供后面使用。关于这部分内容下面还会有更详细介绍。
(?:expr) 表示 expr 为一组,相当于数学表达式中的()
例如:
lstr=\'A body or collection of such stories\';
regexp(lstr,\'(?:[^aeiou][aeiou]){2,}\',\'match\')
ans = \'tori\'
上面的表达式中 {2,} 对 [^aeiou][aeiou] 起作用,如果去掉分组,则只对 [aeiou] 起作用,如下所示:
>>regexp(lstr,\'[^aeiou][aeiou]{2,}\',\'match\')
ans = \'tio\' \'rie\'
(?>expr) expr 中的每个元素是一个分组。(?#expr) 放在(?#和)之间的是注释。如:
>>regexp(lstr, \'(?# Match words in caps)[A-Z]\w*\', \'match\')
ans = \'A\'
expr1|expr2 匹配 expr1 或者 expr2 两者之一即可。
>>regexp(lstr, \'[^aeiou\s]o|[^aeiou\s]i\', \'match\')
ans = \'bo\' \'co\' \'ti\' \'to\' \'ri\'
^expr 匹配 expr,并且出现在原字符串最前端的子串。expr$ 匹配 expr,并且出现在原字符串最末端的子串。
>>pi(lstr, \'^a\w*|\w*s$\', \'match\')
ans = \'A\' \'stories\'
\<expr 匹配 expr,并且出现在一个单词最前端的子串。
>> regexpi(lstr, \'\<s\w*\', \'match\')
ans = \'such\' \'stories\'
expr\> 匹配 expr,并且出现在一个单词最末端的子串。
>> regexpi(lstr, \'\w*tion\>\', \'match\')
ans = \'collection\'
\<expr\> 更严格的单词匹配,如:以 s 开头,并且以 h 结尾的单词。
>>regexpi(lstr, \'\<s\w*h\>\', \'match\')
ans = \'such\'
3.3 左顾右盼 —— 利用上下文匹配
利用上下文的匹配来找到我们要找的内容。expr1(?=expr2) 找到匹配 expr1 的子串,如果其后的字符串也匹配 expr2。如下面的例子查找所有在\',\'之前的单词。
s=\'Grammar Of, relating to, or being a noun or pronoun case that indicates possession.\';
ptn=\'\w*(?=,)\';
regexp(s,ptn,\'match\')
ans = \'Of\' \'to\'
expr1(?!expr2) 找到匹配 expr1 的子串如果其后的字符串不匹配 expr2。下面的例子匹配所有不在\',\'之前的单词:
>>regexpi(s, \'\w*(?!=,)\', \'match\')
ans = \'Grammar\' \'Of\' \'relating\' \'to\' \'or\' \'being\' \'a\' \'noun\' \'or\'
\'pronoun\' \'case\' \'that\' \'indicates\' \'possession\'
(?<=expr1)expr2 找到匹配 expr2 的子串,如果其前面的字符串也匹配 expr1。下面的例子查找所有在 \',\'之后的单词,注意 \',\' 之后可能有空格。
>>regexpi(s,\'(?<=,\s*)\w*\',\'match\')
ans = \'relating\' \'or\'
(?<!expr1)expr2 找到匹配 expr2 的子串,如果其后的字符串不匹配 expr1。下面的例子查找所有不在\',\'之后的单词:
>>regexpi(s,\'(?<!,\s*)\w*\',\'match\')
ans = \'Grammar\' \'Of\' \'elating\' \'to\' \'r\' \'being\' \'a\' \'noun\' \'or\'
\'pronoun\' \'case\' \'that\' \'indicates\' \'possession\'
4.标记(tokens)
这部分是比较难的一部分,但是应用得当可以实现非常强大的功能。
4.1 什么是标记(tokens)
任何的正则表达式都可以用圆括号括起来作为一个标记。例如,创建一个记录钱数的标记,就可以用($\d+)。这样与之匹配的字符串就会被记录下来,根据这个标记出现的顺序,可以使用 \n 来引用匹配这个标记的字符串。如 \3 来引用与标记相匹配的第三个字符串。(如果在替换函数 regexprep 中,需要用 $3
来引用。) 下面是一个例子,\S 查找任意的非空白字符,\1 用来说明要匹配第一个 tokens 的内容,也就是要立即再次查找刚刚匹配到的同一个字符,并且要紧挨着第一个。\'tokens\' 选项用来向 tok 输出所有匹配到的标记;而 \'tokenExtents\' 则用来表示匹配标记的起始位置。
s=\'Grammar Of, relating to, or being a noun or pronoun case that indicates possession.\';
[mat,tok,ext]=regexpi(s, \'(\S)\1\',\'match\',\'tokens\',\'tokenExtents\')
>>mat
mat = \'mm\' \'ss\' \'ss\'
>>tok{:}
ans = \'m\'
ans = \'s\'
ans = \'s\'
>>ext{:}
ans = 4 4
ans = 75 75
ans = 78 78
4.2 如何使用标记?
(expr) 记录所有匹配表达式的字符,并做为一个标记,以备后面使用。如上面的例子,利用标记实现查找连续的重复字母。
\N 匹配同一条正则表达式里的第 N 个标记中的字符串,例如 \1 匹配第一个标记。下面的例子可以查找 html 语句中类似<a>abc</a>的部分:
hstr = \'<!comment><tr nam="7507"></tr><table>Default</table><br>\';
expr = \'<(\w+).*?>.*?</\1>\';
[mat tok] = regexp(hstr, expr, \'match\', \'tokens\');
>> mat{:}
ans = <tr nam="7507"></tr>
ans = <table>Default</table>
>> tok{:}
ans = \'tr\'
ans = \'table\'
$N 在一个替换字符串中插入与第 N 个标记相匹配的字符串(只用于 regexprep 函数)。下面的例子可以将匹配到的第一个 token 和第二个 token 的位置互换:
>> regexprep(\'Norma Jean Baker\', \'(\w+\s\w+)\s(\w+)\', \'$2, $1\')
ans = Baker, Norma Jean
(?<name>expr) 记录所有匹配表达式 expr 的字符,做为一个标记,并设定一个名字 name。\k<name>
与名为 name 的标记相匹配。下面这个例子和这部分第一个例子是一样的,只不过使用了命名的标记。
>>poestr = [\'While I nodded, nearly napping, \' ...
\'suddenly there came a tapping,\'];
>>regexp(poestr, \'(?<nonwhitechar>\S)\k<nonwhitechar>\', \'match\')
ans = \'dd\' \'pp\' \'dd\' \'pp\'
(?(tok)expr) 如果标记 tok 已经产生,则匹配表达式 expr。if-then 结构。其中的标记可以是数字标记,也可以是命名标记。 (?(tok)expr1|expr2) 如果标记 tok 已经产生,则匹配表达式 expr1,否则匹配表达式 expr2。if-then-else结构 下面的例子用来检查一个句子中的性别用词是否匹配,表达式的意思就是,如果前面用的是 \'Mrs\' 那
么后面就匹配 \'her\',如果前面用的是\'Mr\',也就是没有匹配到 \'Mr\' 后面的 \'s\',则后面匹配 \'his\'。
>>expr = \'Mr(s?)\..*?(?(1)her|his) son\';
>>[mat tok] = regexp(\'Mr. Clark went to see his son\', expr, \'match\', \'tokens\')
mat = \'Mr. Clark went to see his son\'
tok = {1x2 cell}
>>tok{:}
ans = \'\' \'his\'
如果把句子中的 his 改成 her,则没有与之匹配的结果。
>>[mat tok] = regexp(\'Mr. Clark went to see her son\', expr, \'match\', \'tokens\')
mat = {}
tok = {}
5.多行字符串与多正则表达式
5.1 多字符串与单个正则表达式匹配
多个字符串存在一个元胞数组里之后,每一个字符串与正则表达式匹配,返回值的维数与元胞数组相同。
cstr = { ...
\'Whose woods these are I think I know.\' ; ...
\'His house is in the village though;\' ; ...
\'He will not see me stopping here\' ; ...
\'To watch his woods fill up with snow.\'};
>>idx{:}
ans = % \'Whose woods these are I think I know.\'
8 % |8
ans = % \'His house is in the village though;\'
23 % |23
ans = % \'He will not see me stopping here\'
6 14 23 % |6 |14 |23
ans = % \'To watch his woods fill up with snow.\'
15 22 % |15 |22
5.2 多个字符串与多个正则表达式匹配
这种情况下,应该满足字符串元胞数组中字符串的个数和正则表达式的个数相等——但维数不一定要相等——如可以用 4*1 的元胞数组与 1*4 的正则表达式相匹配。
expr = {\'i\s\', \'hou\', \'(.)\1\', \'\<w[aeiou]\'};
idx = regexpi(cstr, expr);
idx{:}
ans = % \'Whose woods these are I think I know.\'
23 31 % |23 |31
ans = % \'His house is in the village though;\'
5 30 % |5 |30
ans = % \'He will not see me stopping here\'
6 14 23 % |6 |14 |23
ans = % \'To watch his woods fill up with snow.\'
4 14 28 % |4 |14 |28
5.3 多字符串的替换
这个功能是在匹配的基础上,在正则表达式后面加入要替换的字符串即可。下面这个是 matlab 中的例子,很容易理解。
>>s = regexprep(cstr, \'(.)\1\', \'--\', \'ignorecase\')
s = \'Whose w--ds these are I think I know.\'
\'His house is in the vi--age though;\'
\'He wi-- not s-- me sto--ing here\'
\'To watch his w--ds fi-- up with snow.
6.应用实例
问题 1:查找包含某个字串的串。例如:
在 str = {\'apple_food\' , \'chocolates_food\', \'ipod_electronics\', \'dvd_player_electronics\', \'water_melon_food\'}
中查找字串\'food\',得到结果 [1 1 0 0 1]。
str = {\'apple_food\' , \'chocolates_food\', \'ipod_electronics\', \'dvd_player_electronics\', \'water_melon_food\'} ;
ptn=\'food\';
m1=regexp(str,ptn,\'match\');
ix=~cellfun(\'isempty\',m1);
问题 2:如何将 Matlab 中的 ^ 转换成 C 语言?如将 a^b 转换成 a**b,或者 pow(a,b)。以下式为例:
s=1/2*w/(1+Pf^2*Pc-Pf^2*Pc*w1-w1*Pf^2-Pf*Pc-Pf^2*w^2+2*w1*Pf-2*Pf)
Matlab 提供了 ccode 命令,用于将 Matlab 转换为 c,这里仅为一例:
s=\'1/2*w/(1+Pf^2*Pc-Pf^2*Pc*w1-w1*Pf^2-Pf*Pc-Pf^2*w^2+2*w1*Pf-2*Pf)\';
ptn=\'(\w{1,2})\^(\d{1})\';
regexp(s,ptn,\'tokens\');
s1=regexprep(s,ptn,[\'pow(\',\'$1\',\',\',\'$2\',\')\'])
问题 3:删掉<和/>和它们之间的部分,例如:
处理前:Hello <a href="world">world</a>. 2 < 5
处理后:Hello world. 2 < 5
ss=\'Hello <a href="world">world</a>. 2 < 5\';
b=\'<.*?>\';
sr=regexprep(ss,b,\'\')
问题 4:游程平滑算法:将连续的且个数小于某个阈值的 0 全部替换成 1,例如:
平滑前:1111100000111100011
平滑后:1111100000111111111
a = [1 0 0 1 0 0 0 0 1 1 1 1 1 0 0 0 0 0 1 1 1 1 0 0 0 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1];
T = 4;
b = sprintf(\'%d\',a);
b1 = regexprep(b,\'(?<!0)0{1,3}(?!0)\', repmat(\'1\', size(\'$0\')));
a1=b1-48