将一个数值字串,从右向左,每三位用逗号分开,比如,将“123456789”替换为“123,456,789”。
替换算法的本质是将字串从右向左,每3个字符为一组进行分组,然后在组与组之间插入逗号。
正则表达式引入了“预测”和“回顾”这两个概念,它们不是匹配字串,而是匹配一个“位置”,其中预测是正向(自左向右)查看,回顾是反向(自右向左)查看。
正则表达式 | 名称 | 说明 | 示例 | |
---|---|---|---|---|
(?=...) | 预测 | 断言子表达式在此位置的右侧 | abcd(?=/d) | 匹配右侧紧跟数字的abcd |
(?!...) | 否定预测 | 断言子表达式不在此位置的右侧 | abcd(?!/d) | 匹配右侧紧跟的不是数字的abcd |
(?<=...) | 回顾 | 断言子表达式在此位置的左侧 | (?<=/d)abcd | 匹配左侧是数字的abcd |
(?<!...) | 否定回顾 | 断言子表达式不在此位置的左侧 | (?<!/d)abcd | 匹配左侧不是数字的abcd |
预测和回顾只是匹配一个位置,其中的子表达式并不被匹配。比如有字符“ABCDEFG”,则正则表达式
(?=EFG)(?<=ABCD)或(?<=ABCD)(?=EFG)
就匹配D与E之间的那个位置,而不是匹配EFG,也不匹配ABCD,或其他任何一个字串。 注意,由于只是断言一个位置,所以上述预测和回顾的先后次序并不重要,二者同样都是匹配D与E之间的那个位置。前者的含义是“匹配一个位置,它的右边是 EFG,左边是ABCD”,后者的含义是“匹配一个位置,它的左边是ABCD,右边是EFG”,两者都是同一个意思。下面的Perl在ABCD和EFG之 间插入一个逗号:
$foo = "ABCDEFG";
$foo =~ s/(?<=ABCD)(?=EFG)/,/;
print $foo;
假定要把某段文本中所有的Jeffs都替换为Jeff's(匹配到单词边界),那么,就会有如下几种方案:
方案
|
说明
|
---|---|
s//bJeffs/b/Jeff's/g | 这是最直观的方法,直接把匹配到的Jeffs替换为Jeff's |
s//b(Jeff)(s)/b/$1'$2/g | 分组构造,在两个组之间插入单引号 |
s//bJeff(?=s/b)/Jeff'/g | 预测,仅匹配“后面紧跟s并在单词边界的Jeff”的右侧那个位置 (其它的如Jeffrey中的Jeff不被匹配) |
s/(?<=/bJeff)(?=s/b)/'/g | 同时使用预测和回顾,在匹配位置处插入单引号 |
s/(?=s/b)(?<=/bJeff)/'/g | 与上面的行为完全相同,由于预测和回顾只是匹配位置而不是匹配字串,所以先后次序不重要了。 |
利用预测和回顾,就可以解决“在数值中插入逗号”的问题了。插入算法是:从右向左把数字,每3个分为一组,在组与组之间插入逗号。也就是说,只要匹配到这些位置,就可以在这些位置上插入逗号。
$foo = "123456789";
$foo =~ s/(?=/d/d/d)/,/g;
print $foo;
上面的结果是“,1,2,3,4,5,6,789”,显然不是想要的。
正则表达式 | 结果 | 说明 |
---|---|---|
s/(?=/d/d/d)/,/g s/(?=(/d/d/d)+)/,/g |
,1,2,3,4,5,6,789 | 1左边的位置被匹配,因为该位置右边有3个数字(123) 同理,1右边的位置也被匹配,该位置右边有3个数字(234)…… 而7的右边位置不再被匹配,因为该位置右边不再有3个数字 |
s/(?=/d/d/d$)/,/g | 123456,789 | 只有7左边的位置被匹配,因为该点符合以下两个条件: 其右边是3个数字, 且这3个数字在行尾(其右侧什么也没有了)。 |
s/(?=(/d/d/d)+$)/,/g | ,123,456,789 | 1左边的位置符合条件“其右侧有N组(每组3个数字),且最后一组在行尾”; 2左边的位置不符合该条件,因为其后面是234、567和89,最后的89无法构成一组; 其它同理…… |
s/(?=(/d/d/d)+$)(?<=/d)/,/g | 123,456,789 | 1左边的位置不再被匹配,因为(?<=/d)的存在,它指示该位置的左侧必须是数字, 而1左边的位置显然不符合。 |
这个正则表达式替换单个的数字串没问题,但如果想要将“The population of 281421906 is growing”中的281421906替换成281,421,906就无能为力了,原因是$限定了最后一组(3个)数字必须在行尾,因此需要把条件修正为“最后一组数字的右边不是数字”,这就要用到“否定预测”:
s/(?=(/d/d/d)+(?!/d))(?<=/d)/,/g(微软VBScript.DLL提供的COM对象IRegExp2,文档里说支持“回顾”语法,但实际运用却会抛出一个异常,不知何故)