分組捕捉

June 29, 2022

可以使用 () 來將規則表示式分組,除了作為子規則表示式之外,還可以搭配量詞使用。

分組與參考

例如想要驗證電子郵件格式,允許的使用者名稱開頭要是大小寫英文字元,之後可搭配數字,規則表示式可以寫為 ^[a-zA-Z]+\d*,因為 @ 後網域名稱可以有數層,必須是大小寫英文字元或數字,規則表示式可以寫為 ([a-zA-Z0-9]+\.)+,其中使用 () 群組了規則表示式,之後的 + 表示這個群組的表示式符合一次或多次,最後要是 com 結尾,整個結合起來的規則表示式就是 ^[a-zA-Z]+\d*@([a-zA-Z0-9]+\.)+com

若有字串符合了被分組的規則表示式,字串會被捕捉(Capture),以便在稍後回頭參考(Back reference),在這之前,必須知道分組計數,如果有個規則表示式 ((A)(B(C))),其中有四個分組,這是遇到的左括號來計數,所以四個分組分別是:

  1. ((A)(B(C)))
  2. (A)
  3. (B(C))
  4. (C)

分組回頭參考時,是在 \ 後加上分組計數,表示參考第幾個分組的比對結果。

例如,\d\d 要求比對兩個數字,(\d\d)\1 的話,表示要輸入四個數字,輸入的前兩個數字與後兩個數字必須相同,例如輸入 1212 會符合,12 因為符合 (\d\d) 而被捕捉至分組 1,\1要求接下來輸入也要是分組 1 的內容,也就是 12;若輸入 1234 則不符合,因為 12 雖然符合 (\d\d) 而被捕捉,然而 \1 要求接下來的輸入也要是 12,然而接下來的數字是 34,因而不符合。

再來看個實用的例子,["'][^"']*["'] 比對單引號或雙引號中 0 或多個字元,但沒有比對兩個都要是單引號或雙引號,(["'])[^"']*\1 則比對出前後引號必須一致。

擴充標記

規則表示式中的 (?…) 代表擴充標記(Extension notation),括號中首個字元必須是?,而這之後的字元(也就是…的部份),進一步決定了規則表示式的組成意義。

如果不需要分組計數,只是想使用 () 來定義某個子規則,可以使用 (?:…) 來表示不捕捉分組。

例如,若只是想比對郵件位址格式,不打算捕捉分組,可以使用 ^[a-zA-Z]+\d*@(?:[a-zA-Z0-9]+\.)+com。 在規則表示式複雜之時,善用 (?:…) 來避免不必要的捕捉分組,對於效能也會有很大的改進。

如果想比對出的對象,之後必須跟隨或沒有跟隨著特定文字,可以使用 (?=…)(?!…),分別稱為 lookahead 與 negative lookahead。例如分別比對出來的名稱最後必須有 Lin:

分組捕捉

如果將上圖中的 (?=Lin) 改為 (?!Lin),就會比對出 Monica。

相對地,如果想比對出的對象,前面必須有或沒有著特定文字,可以使用 (?<=…)(?<!…),分別稱為 lookbehind 與 negative lookbehind。

分組捕捉

JavaScript 實際上是在 ECMAScript 2018(ES9)中才規範了 (?<=…)(?<!…),然而,最新版的 Chrome 與 Node.js 已經支援。

分組命名

有的工具或語言支援分組命名,在概念上,要捕捉的分組數量眾多時,以號碼來區別分組並不方便,這時為分組命名,之後就可以使用名稱取用分組。

不過,不同的工具或語言,分組命名時的語法有些差異,以 Expresso 來說,使用 (?<name> …) 來為分組命名,在同一個規則表示式中使用 \k<name>\k'n' 取用分組。

例如先前談到的 (\d\d)\1 是使用號碼取用分組,若想以名稱取用分組,也可以使用 (?<tens>\d\d)\k<tens>,當分組眾多時,適時為分組命名,就不用為了分組計數而煩惱。

分組捕捉

Python 的話,使用 (?P<name>…) 來為分組命名,在同一個規則表示式中使用 (?P=name) 取用分組。

Java 的話,使用 (?<name>…) 來為分組命名,在同一個規則表示式中使用 \k<name> 取用分組。

JavaScript 的話,ECMAScript 2018(ES9)支援分組命名,使用 (?<name>…) 為分組命名,在同一個規則表示式中使用 \k<name> 取用分組。

有些語言或工具支援規則表示式的條件式,像是 Expresso、Python(而 Java、JavaScript 不支援),可使用 (?(id/name)yes-pattern|no-pattern) 來根據先前是否有符合的分組,動態地組成整個規則表示式。

若只使用工具程式,只能輸入規則表示式的情況下,就可能需要這個功能。

例如,希望驗證郵件位址是否被對稱的 <> 包括,或者是完全沒被 <> 包括,在 Expresso 中可以撰寫 ^(?<arrow><)?(\w+@\w+(?:\.\w+)+)(?(arrow)>|$)

分組捕捉

(?(arrow)>|$) 的部份表示,如果有文字符合了命名為 arrow 的分組,也就是 (?<arrow><)? 的部份,那麼會使用 > 來組成規則表示式,否則就使用 $ 來組成規則表示式。

就上例來說,如果有文字符合了命名為 arrow 的分組,規則表示式等同於 ^(?<arrow><)?(\w+@\w+(?:\.\w+)+)>,否則規則表示式等同於 ^(?<arrow><)?(\w+@\w+(?:\.\w+)+)$

如果是程式語言,基本上不使用這個功能,也可以使用程式編寫方式,來實現這個功能,而且比較易懂,以 Python 為例:

import re

def validate(email):
    mailre = r'\w+@\w+(?:\.\w+)+'
    regex = f'<{mailre}>' if email[0] == '<' else f'^{mailre}$'
    return re.findall(regex, email) != []

print(validate('<user@host.com>')) # 顯示 True
print(validate('user@host.com'))   # 顯示 True 
print(validate('<user@host.com'))  # 顯示 False
print(validate('user@host.com>'))  # 顯示 False

這個範例結合了 Python 3.6 的 f-strings,因而運用程式流程來動態組成規則表示式方便許多,程式碼可讀性上也還不錯。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter