前言

正規表達式(Regex)是文字處理中的重要工具,無論是搜尋、驗證表單還是數據清理都非常常見,或是客製化 config 檔時也能透過正則表達式設計更複雜的邏輯。

而進階技巧如 Lookbehind 和 Lookahead 則讓開發者能夠撰寫更精確的表達式。這些特性幫助我們在匹配特定內容時可以加上前後文條件,卻不將這些條件納入匹配結果中

本文將紀錄 Lookbehind 和 Lookahead 的概念、其運作方式,並提供一些實際範例來紀錄這些進階用法。

Zero-Width Assertions

首先,在介紹 Lookahead 和 Lookbehind 這兩個表達式以前,要先知道一個叫做 Zero-Width Assertions 的概念 (中文不知道怎麼翻譯,就直接用英文表示)。

Zero-Width Assertions 是指那些僅匹配文本中的特定位置,而非具體字符的語法或表達式。 它們會在想要匹配的字串中的某個位置檢查特定條件,但不會消耗任何字符,也不會使正則表達式引擎的游標向前推進。因此,這類斷言經常被稱為「隱形」條件,能夠在不影響最終匹配結果的情況下,讓我們定義更複雜的匹配模式。

聽起來有點抽象,簡單來說概念就像是 regex 世界的 if/else,可以做一些邏輯的判定。

常見的 Zero-Width Assertions 有底下三種:

  1. Lookahead
  2. Lookbehind
  3. Anchor (^, $)

前面兩個我們先忽略,因為等等後面會一一介紹。第三個 Anchor 仔細看會發現就是我們平常在用的字串起始(^)與字串結尾($)

快速舉個例子來說, ^Helloween$ 這個表達式可以匹配到 “Helloween” 這個字串,但不會匹配到 “~Helloween” 和 “Helloween~” 這兩個字串,原因是我們明確指定字串要以 H 開頭且以 n 結尾。

當使用 Anchor 這類的 Zero-Width Assertions 表達式,並不會消耗我們的字元,意思是他只會拿來做為邏輯的判定(是否以某字元做起始? 是否以某字元做結尾?),這樣會帶來幾點好處:

  1. 可以根據上下文設定匹配的邏輯判定(某文字之前或之後),而不會影響實際匹配的內容
  2. 消除不必要的 backtracking 來避免效能的減損 (Catastrophic Backtracking)

ahead vs behind

要介紹 Lookahead 和 Lookbehind 之前,要先定義到底什麼是 ahead? 什麼是 behind?

我自己的理解是右邊視為前方,左邊視為後方,這樣有助於後面的理解。譬如有個字串 “Hello, World!",World 的後方(behind)就是 Hello,而 Hello 的前方(ahead)就是 World

定義好何謂 ahead 和何謂 behind 後再來看 Lookahead 和 Lookbehind 會比較有感覺

Lookahead

接下來介紹也是 Zero-Width Assertions 的 Lookahead,簡單來說他可以讓我們只在某個模式前方存在特定條件時進行匹配,可以把它想像成一個在匹配主體前面要滿足的條件。

Lookahead 有兩種,一種是正向(Positive),另一種是負向(Negative)

  • Positive Lookahead

    • 語法是 (?=...),其中 ... 是我們希望後續出現的模式
    • 它會檢查當前位置之前是否特定模式(…),但不將該部分包含在匹配結果中

    舉例來說,用這個表達式 \w+(?=ing) 來匹配 “I’m cooking” 這個字串,會匹配到 “cook”,因為這段表達式的意思是請匹配出任何字元前方跟著 ing 的文字,但匹配的內容不會包含 ing,而整段句子中只有 cooking 的結尾是 ing

  • Negative Lookahead

    • 語法是 (?!...),其中 ... 是我們希望後續出現的模式
    • 它會檢查當前位置之前是否沒有特定模式(…),但不將該部分包含在匹配結果中

    舉例來說,用這個表達式 \w(?!ing) 來匹配 “She is cooking” 這個字串,會匹配到 “She is cooing” 這些字,為什麼少了一個 “k”? 那是因為這段表達式的意思是請匹配出任何字元前方跟著 ing 的文字,而只有 “k” 前方跟著 ing,所以不會被匹配到

Lookbehind

接下來介紹同樣是 Zero-Width Assertions 的 Lookbehind,簡單來說,它可以讓我們在某個模式的後方存在特定條件時進行匹配,可以把它想像成一個在匹配主體後面要滿足的條件。

Lookbehind 也分為兩種,一種是正向 (Positive),另一種是負向 (Negative)。

  • Positive Lookbehind

    • 語法是 (?<=...),其中 ... 是我們希望匹配位置之後存在的模式
    • 它會檢查當前位置之後是否特定模式(…),但不將該部分包含在匹配結果中

    舉例來說,用這個表達式 (?<=Dr\.)\w+ 來匹配 “Dr.Smith is here” 這個字串,會匹配到 “Smith”,因為這段表達式的意思是請匹配出任何後方是 “Dr.” 的文字,且匹配的內容不會包含 “Dr.",而整段句子中只有 “Smith” 的後方是 Dr.

  • Negative Lookbehind

    • 語法是 (?<!...),其中 ... 是我們希望匹配位置之後出現的模式
    • 它會檢查當前位置之後是否沒有特定模式(…),但不將該部分包含在匹配結果中

    舉例來說,用這個表達式 (?<!\$)\d+ 來匹配 "$50 and 30 dollars" 這個字串,會匹配到 “30” 而不會匹配到 “50”,因為這段表達式的意思是請匹配任何數字後方不是 $

結論

總結來說,掌握底下幾個規則就可以快速理解 Lookahead 和 Lookbehind

  • 文字右邊視為前方(ahead),左邊視為後方(behind)
  • Lookahead
    • Positive lookahead: (?=…): 前方跟著…
    • Negative lookahead: (?!…): 前方不跟著…
  • Lookbehind
    • Positive lookbehind: (?<=…): 後方跟著…
    • Negative lookbehind: (?<!…): 後方不跟著…

最後再提供幾個例子

  1. 想要匹配 “500TWD and 120TWD” 中所有數字 (500 200)

    \d+(?=TWD)
    
  2. 想要匹配 “$500 and $120” 中所有數字 (500 200)

    (?<=\$)\d+
    
  3. 想要匹配 “Good morning, Mr.Smith, Dr.Hsu, Ms.Lian and Mrs.Petty.” 中所有人名 (Smith Hsu Lian Petty)

    (?<=Mr\.|Dr\.|Ms\.|Mrs\.)[a-zA-Z]+
    

以上就是 Lookahead 和 Lookbehind 的介紹,希望在這個 AI 快速產生答案的時代,依然保有一點對基礎的理解,才能真正提升自己的技術能力

參考資料