快速入門
教學
工具和語言
範例
參考
書籍評論
正規表示式教學
簡介
目錄
特殊字元
不可列印字元
正規表示式引擎內部
字元類別
字元類別減法
字元類別交集
簡寫字元類別
錨定
字詞邊界
交替
選用項目
重複
群組和擷取
反向參照
反向參照,第 2 部分
命名群組
相對反向參照
分支重設群組
自由間距和註解
Unicode
模式修改器
原子群組
所有格量詞
前瞻和後顧
環顧,第 2 部分
將文字排除在比對之外
條件
平衡群組
遞迴
子常式
無限遞迴
遞迴和量詞
遞迴和擷取
遞迴和反向參照
遞迴和回溯
POSIX 方括號表示式
零長度比對
繼續比對
本網站的其他內容
簡介
正規表示式快速入門
正規表示式教學
替換字串教學
應用程式和語言
正規表示式範例
正規表示式參考
替換字串參考
書籍評論
可列印 PDF
關於本網站
RSS Feed 和部落格
RegexBuddy—Better than a regular expression tutorial!

子常式呼叫可能會擷取,也可能不會

本教學介紹正規表示式子常式,並使用我們想要精確比對的範例

Name: John Doe
Born: 17-Jan-1964
Admitted: 30-Jul-2013
Released: 3-Aug-2013

RubyPCRE 中,我們可以使用這個正規表示式

^姓名: (.*)\n
出生: (?'date'(?:3[01]|[12][0-9]|[1-9])
               
-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
               
-(?:19|20)[0-9][0-9])\n
入院: \g'date'\n
出院: \g'date'$

Perl 需要稍微不同的語法,這也適用於 PCRE

^姓名:(.*)\n
出生:(?'date'(?:3[01]|[12][0-9]|[1-9])
               
-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
               
-(?:19|20)[0-9][0-9])\n
入院:(?&date)\n
出院:\ (?&date)$

不幸的是,這三種正規表示法在處理子常式呼叫時,除了語法之外,還有不同的處理方式。首先,在 Ruby 中,子常式呼叫會讓擷取群組儲存子常式呼叫期間配對到的文字。在 Perl、PCRE 和 Boost 中,子常式呼叫不會影響被呼叫的群組。

當 Ruby 解決方案配對上述範例時,擷取群組「date」的內容會是 3-Aug-2013,這是由最後一次對該群組的子常式呼叫所配對到的。當 Perl 解決方案配對相同範例時,擷取 $+{date} 會是 17-Jan-1964。在 Perl 中,子常式呼叫完全沒有擷取任何東西。但「出生」日期是用一般的 命名擷取群組 所配對到的,它會正常儲存配對到的文字。對群組的任何子常式呼叫都不會改變這一點。PCRE 在這種情況下會像 Perl 一樣,即使你在 PCRE 中使用 Ruby 語法也是如此。

JGsoft V2 在您使用第一個正規表示式時,表現得像 Ruby。您可以透過 \g 語法是 Ruby 的發明,後來被 PCRE 複製,這個事實來記住這一點。JGsoft V2 在您使用第二個正規表示式時,表現得像 Perl。您可以透過 Perl 在程序碼中也使用&符號來呼叫子程式,這個事實來記住這一點。

如果您要從配對中擷取日期,最好的解決方案是為每個日期新增另一個擷取群組。然後您可以忽略「日期」群組儲存的文字,以及這些風格之間的這個特定差異。在 Ruby 或 PCRE 中

^姓名:(.*)\n
出生日期:(?'born'(?'date'(?:3[01]|[12][0-9]|[1-9])
                       
-(?:一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月)
                       
-(?:19|20)[0-9][0-9]))\n
入院日期:(?'admitted'\g'date')\n
出院日期:(?'released'\g'date')$

Perl 需要稍微不同的語法,這也適用於 PCRE

^姓名:(.*)\n
出生日期:(?'born'(?'date'(?:3[01]|[12][0-9]|[1-9])
                       
-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
                       
-(?:19|20)[0-9][0-9]))\n
入院日期:(?'admitted'(?&date))\n
出院日期:\ (?'released'(?&date))$

擷取遞迴或子常式呼叫內的群組

當您的正規表示式對包含其他擷取群組的擷取群組進行子常式呼叫或遞迴呼叫時,Perl、PCRE 和 Ruby 之間還有進一步的差異。如果它包含任何擷取群組,則相同的問題也會影響整個正規表示式的遞迴。對於本主題的其餘部分,術語「遞迴」同樣適用於整個正規表示式的遞迴、遞迴到擷取群組或對擷取群組的子常式呼叫。

PCRE 和 Boost 在進入和退出遞迴時備份並還原擷取群組。當正規表示式引擎進入遞迴時,它會在內部複製所有擷取群組。這不會影響擷取群組。遞迴內的反向參照會比對在遞迴之前擷取的文字,除非且直到它們引用的群組在遞迴期間擷取某些內容。在遞迴之後,所有擷取群組都會被替換為在遞迴開始時建立的內部副本。在遞迴期間擷取的文字會被捨棄。這表示您無法使用擷取群組來擷取在遞迴期間比對的文字部分。

Perl 5.10,第一個有遞迴功能的版本,從版本 5.10 到 5.18,會在每個遞迴層次之間孤立擷取群組。當 Perl 5.10 的正規表示式引擎進入遞迴時,所有擷取群組都會顯示為尚未參與比對。最初,所有反向參照都會失敗。在遞迴期間,擷取群組會正常擷取。反向參照會比對在同一遞迴期間擷取的文字,就像正常情況一樣。當正規表示式引擎退出遞迴時,所有擷取群組都會還原到遞迴前的狀態。Perl 5.20 將 Perl 的行為變更為備份和還原擷取群組,就像 PCRE 所做的那樣。

然而,對於大多數實際用途來說,你只會在對應的擷取群組之後使用反向參照。那麼 Perl 5.10 到 5.18 在遞迴期間處理擷取群組的方式,以及 PCRE 和後續版本的 Perl 處理擷取群組的方式之間的差異就是學術性的了。

Ruby 的行為完全不同。當 Ruby 的正規表示式引擎進入或退出遞迴時,它完全不會變更擷取群組儲存的文字。反向參照會比對群組最近一次比對期間擷取群組儲存的文字,而不論可能發生的任何遞迴。在找到整體比對後,每個擷取群組仍會儲存其最近一次比對的文字,即使那是發生在遞迴期間。這表示你可以使用擷取群組來擷取在最後一次遞迴期間比對的文字的一部分。

當你使用從 Ruby 借來的 \g 語法時,JGsoft V2 的行為就像 Ruby。當你使用任何其他語法時,它的行為就像 Perl 5.20 和 PCRE。

Perl 和 PCRE 中的奇數長度迴文

在 Perl 和 PCRE 中,你可以使用 \b(?'word'(?'letter'[a-z])(?&word)\k'letter'|[a-z])\b 來比對迴文單字,例如 adadradarracecarredivider。這個正規表示式只會比對長度為奇數個字母的迴文單字。這涵蓋了英文中大多數的迴文單字。要將正規表示式擴充為也能處理長度為偶數個字元的迴文單字,我們必須擔心 Perl 和 PCRE 在 遞迴嘗試失敗後如何回溯 的差異,這將在本教學課程後續討論。我們在此略過這些差異,因為它們只會在主旨字串不是迴文且找不到比對時才會發揮作用。

讓我們看看這個正規表示式如何比對 radar字詞邊界 \b 比對字串的開頭。正規表示式引擎進入兩個擷取群組。[a-z] 比對 r,然後儲存在擷取群組「letter」中。現在正規表示式引擎進入群組「word」的第一個遞迴。在這個時候,Perl 會忘記「letter」群組比對到 r。PCRE 則不會。不過這並不重要。(?'letter'[a-z]) 比對並擷取 a。正規表示式進入群組「word」的第二個遞迴。(?'letter'[a-z]) 擷取 d。在接下來的兩個遞迴中,群組擷取 ar。第五個遞迴失敗,因為字串中沒有字元供 [a-z] 比對。正規表示式引擎必須回溯。

由於 (?&word) 沒有比對到,(?'letter'[a-z]) 必須放棄它的比對。群組回到 a,這是群組在遞迴開始時所擁有的文字。(在 Perl 5.18 及之前版本中,它會變成空值。)同樣地,這並不重要,因為正規表示式引擎現在必須嘗試群組「word」中不包含反向參照的第二個選項。第二個 [a-z] 比對字串中最後一個 r。引擎現在退出成功的遞迴。群組「letter」儲存的文字會還原到它在進入第四個遞迴之前所擷取的內容,也就是 a

在配對 (?&word) 之後,引擎會到達 \k'letter'。反向參照會失敗,因為正規表示式引擎已經到達主旨字串的尾端。因此,它會再回溯一次,讓擷取群組放棄 a。第二個選項現在會配對 a。正規表示式引擎會離開第三次遞迴。群組「letter」會還原為在第二次遞迴期間配對到的 d

正規表示式引擎再次配對 (?&word)。反向參照會再次失敗,因為群組儲存 d,而字串中的下一個字元是 r。再次回溯後,第二個選項會配對 d,而群組會還原為在第一次遞迴期間配對到的 a

現在,\k'letter' 會配對字串中的第二個 a。這是因為正規表示式引擎已經回到第一次遞迴,在第一次遞迴期間,擷取群組配對到第一個 a。正規表示式引擎會離開第一次遞迴。擷取群組會還原為在第一次遞迴之前配對到的 r

最後,反向參照會配對到第二個 r。由於引擎不再位於任何遞迴中,因此它會繼續處理群組之後的正規表示式剩餘部分。\b 會在字串尾端配對。正規表示式尾端已到達,radar 會作為整體配對結果傳回。如果您在配對後查詢群組「word」和「letter」,您會得到 radarr。這是這些群組在所有遞迴之外配對到的文字。

為什麼這個正規表示式在 Ruby 中無法運作

要在 Ruby 中以這種方式配對迴文,您需要使用一個特殊的 反向參照,用於指定遞迴層級。如果您使用一般反向參照,例如 \b(?'word'(?'letter'[a-z])\g'word'\k'letter'|[a-z])\b,Ruby 就不會抱怨。但它也不會配對到長度超過三個字元的迴文。相反地,這個正規表示式會配對到 adadradaaracecccrediviiii 等字串。

讓我們看看為什麼這個正規表示式無法在 Ruby 中配對到 radar。Ruby 的開頭就像 Perl 和 PCRE,會進入遞迴,直到字串中沒有字元可以讓 [a-z] 配對。

由於 \g'word' 未能配對,(?'letter'[a-z]) 必須放棄其配對。Ruby 將其還原為 a,這是群組最近配對的文字。第二個 [a-z] 配對字串中的最後一個 r。引擎現在退出成功的遞迴。群組「letter」繼續保留其最近的配對 a

配對 \g'word' 之後,引擎到達 \k'letter'。由於正規表示式引擎已經到達主旨字串的結尾,因此反向參照失敗。所以它再次回溯,將群組還原為先前配對的 d。第二個選項現在配對 a。正規表示式引擎退出第三次遞迴。

正規表示式引擎再次配對 \g'word'。反向參照再次失敗,因為群組儲存 d,而字串中的下一個字元是 r。再次回溯,群組還原為 a,第二個選項配對 d

現在,\k'letter' 配對字串中的第二個 a。正規表示式引擎退出成功配對 ada 的第一次遞迴。擷取群組繼續保留 a,這是其最近的配對,且未回溯。

正規表示式引擎現在位於字串中的最後一個字元。這個字元是 r。反向參照失敗,因為群組仍保留 a。引擎可以再次回溯,強制 (?'letter'[a-z])\g'word'\k'letter' 放棄到目前為止配對的 rada。正規表示式引擎現在回到字串的開頭。它仍然可以嘗試群組中的第二個選項。這會配對字串中的第一個 r。由於引擎不再位於任何遞迴中,因此它會繼續處理群組之後的正規表示式剩餘部分。\b 在第一個 r 之後無法配對。正規表示式引擎沒有進一步的排列組合可以嘗試。配對嘗試已失敗。

如果主旨字串是 radaa,Ruby 的引擎會經歷與上述描述幾乎相同的配對程序。只有最後一段中描述的事件會改變。當正規表示式引擎到達字串中的最後一個字元時,該字元現在是 a。這次,反向參照配對成功。由於引擎不再位於任何遞迴中,因此它會繼續處理群組之後的正規表示式剩餘部分。\b 在字串結尾配對成功。到達正規表示式的結尾,並傳回 radaa 作為整體配對。如果您在配對後查詢群組「word」和「letter」,您將得到 radaaa。這些是這些群組最近的配對,且未回溯。

基本上,在 Ruby 中,這個正規表示式會比對任何奇數個字母長的字詞,且字詞中間字母右邊的所有字元都和中間字母左邊的字元相同。這是因為 Ruby 只有在回溯時才會還原擷取群組,而不會在退出遞迴時還原。

針對 Ruby 的解決方案是使用 指定遞迴層級的反向參照,而不是在本頁正規表示式中使用的常規反向參照。