当前位置: 代码迷 >> 综合 >> 读书笔记《Mastering Regular Expressions》(四)
  详细解决方案

读书笔记《Mastering Regular Expressions》(四)

热度:99   发布时间:2024-01-16 15:48:24.0
一、问题

将一个数值字串,从右向左,每三位用逗号分开,比如,将“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,文档里说支持“回顾”语法,但实际运用却会抛出一个异常,不知何故)