一、問題
將一個數值字串,從右向左,每三位用逗號分開,比如,將“123456789”替換為“123,456,789”。
二、分析
替換演算法的本質是將字串從右向左,每3個字元為一組進行分組,然後在組與組之間插入逗號。
三、文法
Regex引入了“預測”和“回顧”這兩個概念,它們不是匹配字串,而是匹配一個“位置”,其中預測是正向(自左向右)查看,回顧是反向(自右向左)查看。
(下表中圓括弧及其括住的內容,只匹配位置,不匹配文本)
| Regex |
名稱 |
說明 |
樣本 |
| (?=...) |
預測 |
斷言子運算式在此位置的右側 |
abcd(?=/d) |
匹配右側緊跟數位abcd |
| (?!...) |
否定預測 |
斷言子運算式不在此位置的右側 |
abcd(?!/d) |
匹配右側緊跟的不是數位abcd |
| (?<=...) |
回顧 |
斷言子運算式在此位置的左側 |
(?<=/d)abcd |
匹配左側是數位abcd |
| (?<!...) |
否定回顧 |
斷言子運算式不在此位置的左側 |
(?<!/d)abcd |
匹配左側不是數位abcd |
預測和回顧只是匹配一個位置,其中的子運算式並不被匹配。比如有字元“ABCDEFG”,則Regex
(?=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”,顯然不是想要的。
| Regex |
結果 |
說明 |
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左邊的位置顯然不符合。 |
這個Regex替換單個的數字串沒問題,但如果想要將“The population of 281421906 is growing”中的281421906替換成281,421,906就無能為力了,原因是$限定了最後一組(3個)數字必須在行尾,因此需要把條件修正為“最後一組數位右邊不是數字”,這就要用到“否定預測”:
s/(?=(/d/d/d)+(?!/d))(?<=/d)/,/g
(微軟VBScript.DLL提供的COM對象IRegExp2,文檔裡說支援“回顧”文法,但實際運用卻會拋出一個異常,不知何故)