分組捕捉
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)))
,其中有四個分組,這是遇到的左括號來計數,所以四個分組分別是:
((A)(B(C)))
(A)
(B(C))
(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,因而運用程式流程來動態組成規則表示式方便許多,程式碼可讀性上也還不錯。