量詞 (*, +, ?, {n,m}) 預設採用「貪婪」策略:在滿足整體模式匹配的前提下,盡可能多地吞噬字元。非貪婪 (懶惰) 模式在量詞後加 ?,改為盡可能少地吞噬,只匹配滿足條件的最短字串。理解兩者差異對撰寫高效、正確的正則至關重要。
| 量詞 | 貪婪 | 非貪婪 | 描述 |
|---|---|---|---|
? | ? | ?? | 0 或 1 次 |
+ | + | +? | 1 次以上 |
* | * | *? | 0 次以上 |
{n} | {n} | {n}? | 剛好 n 次 (非貪婪無意義) |
{n,m} | {n,m} | {n,m}? | n 到 m 次 |
{n,} | {n,} | {n,}? | n 次以上 |
var
reg: TPerlRegEx;
begin
reg := TPerlRegEx.Create(nil);
try
reg.Subject := '標題 內容';
// 貪婪:<.*> 從第一個 < 吞到最後一個 >
reg.RegEx := '<.*>';
reg.Replacement := '◆';
reg.ReplaceAll;
ShowMessage(reg.Subject); // 結果: ◆ (整串被一個標籤吞掉)
reg.Subject := '標題 內容';
// 非貪婪:<.*?> 遇到第一個 > 就停止
reg.RegEx := '<.*?>';
reg.Replacement := '◆';
reg.ReplaceAll;
ShowMessage(reg.Subject); // 結果: ◆◆◆標題◆◆◆內容◆◆ (每個標籤分別匹配)
finally
reg.Free;
end;
end;
正則引擎採用回溯演算法:貪婪量詞先吞噬整個字串,若後續模式無法匹配,再逐字「吐出」回溯。對於 `<.*>`,.* 吞到字串末尾,發現後面沒有 > (已被吞掉),回溯到倒數第二個 > 成功,導致匹配跨越多個標籤。
.*?。例如匹配雙引號字串:"[^"]*" (無逸出) 或 "(\\"|[^"])*" (支援逸出) 比 ".*?" 快數倍,且不會跨行匹配。| 情境 | 建議寫法 | 理由 |
|---|---|---|
| 匹配雙引號字串 (無換行) | "[^"]*" | 排除類別直達結尾,零回溯 |
| 匹配單引號字串 (支援逸出) | '(\\'|[^'])*' | 明確處理逸出,避免非貪婪回溯 |
| 匹配 HTML 標籤內容 | <[^>]+> | 標籤不含 >,排除類別最快 |
| 分隔符不明確/多變 | .*? 或 .+? | 只能用非貪婪,注意效能 |
| 固定次數重複 | {n} | 無貪婪/非貪婪之分 |
模式如 (a+)+b 輸入 aaaaaaaaac 會導致指數級回溯。非貪婪 (a+?)+b 同樣會回溯。解決辦法:
a++、*+、?+、{n,m}+ (PCRE/Perl 5.10+ 支援,Delphi TPerlRegEx 視版本而定)。(?>a+)。reg.Options := reg.Options + [preSingleLine]; // . 匹配包含換行
reg.Options := reg.Options - [preGreedy]; // 全域關閉貪婪 (較少用)
建議明確在量詞後加 ? 控制非貪婪,而非依賴全域選項,以保持模式可讀性與可攜性。
?)。[^...]) 替代非貪婪,兼具精確與高效。下一篇將介紹「邊界匹配」:單字邊界、字串開頭結尾、Unicode 屬性邊界等定位技巧。