攝影或3C

Python 正則表示法:零寬斷言實戰指南 (?=pattern) 正向先行 ; (?!pattern) 負向先行 ; (?<=pattern) 正向回顧 ; (?<!pattern) 負向回顧 ; (? 往後(右)看 ; (?< 往前(左)看 ; =必須符合 ; !不可符合

核心觀念

零寬斷言只「檢查」相鄰條件,
不消耗字元(匹配位置不前進)。
常見四種(p = pattern):
(?=p) 正向先行:後面要是 p
(?!p) 負向先行:後面不可是 p
(?<=p) 正向回顧:前面要是 p(Python 要固定寬度)
(?<!p) 負向回顧:前面不可是 p(Python 要固定寬度)
下列程式可直接跑,會印出全數 PASS 讓你驗證。

因為正則引擎是由左往右匹配,先行斷言是在「當前位置往右邊」先檢查接下來的內容是否符合條件,才決定是否繼續匹配;它不消耗字元,只是「預先查看」所以叫「先行」。

先行 lookahead (?=p)/(?!p):從當前位置往右看「即將到來」的字元是否符合/不符合 p。
回顧 lookbehind (?<=p)/(?<!p):從當前位置往左看「已經過去」的字元是否符合/不符合 p。

一次驗證所有觀念的測試程式

import re

def run_tests():
    # 1) 負向先行 (?!\d):「後面不是數字」;常用來避免小數點被誤切
    filename = "14.5GHZ.csv"
    parts = re.split(r"\.(?!\d)", filename)  # 只在非小數點的點切
    assert parts == ["14.5GHZ", "csv"]

    #  (?!.*\.) 鎖定最後一個點來去掉副檔名
    def base_name(fn: str) -> str:
        # 移除最後一個點後面的副檔名
        return re.sub(r"\.(?!.*\.)[^.]*$", "", fn)

    assert base_name("14.5GHZ.csv") == "14.5GHZ"
    assert base_name("v1.2.3.tar.gz") == "v1.2.3.tar"
    assert base_name(".env") == ""  # 隱藏檔案特例必要時自行保留

    # 2) \d+(?!\d):不被下一個數字延伸的整數邊界零寬不吃掉後字元
    s = "id=123a 456 78b90"
    nums = re.findall(r"\d+(?!\d)", s)
    assert nums == ["123", "456", "78", "90"]

    # 對比:\d+[^\d] 吃掉後面的非數字不是我們要的邊界檢查
    bad = re.findall(r"\d+[^\d]", s)
    assert bad == ["123a", "456 ", "78b"]  # 不符合只取數字的需求

    # 3) 正向先行 (?=bar):匹配 foo 但要求後面緊跟 bar不消耗 bar
    t = "foobar fooqux foobar"
    hits = re.findall(r"foo(?=bar)", t)  # 只回傳 foo 的匹配
    assert hits == ["foo", "foo"]  # 第二個 "fooqux" 不算

    # 4) tempered dot避免跨越 END非貪婪 .*? 也可但此法更穩定
    text = "START A END B END"
    # 說法 START 重複下一步不是 END 才吃一個字元」,直到遇到第一個 END
    m = re.search(r"START(?:(?!END).)*END", text)
    assert m and m.group(0) == "START A END"

    # 對比貪婪 .* 會吃到最後一個 END
    m2 = re.search(r"START.*END", text)
    assert m2 and m2.group(0) == "START A END B END"

    # 5) 正向回顧 (?<=\$):抓被 $ 前綴的金額固定寬度回顧
    money = "Price $12.50, tax 3, amount $7"
    cash = re.findall(r"(?<=\$)\d+(?:\.\d+)?", money)
    assert cash == ["12.50", "7"]

    # 6) 邊界類需求只想匹配完整單字 USD
    # 方案 A:\b 單字邊界簡潔
    txt = "USD USD1 XUSD USDX"
    a = re.findall(r"\bUSD\b", txt)
    assert a == ["USD"]

    # 方案 B lookaround 模擬更顯式):前後都不能是 \w
    b = re.findall(r"(?<!\w)USD(?!\w)", txt)
    assert b == ["USD"]

    print("ALL PASS")

if __name__ == "__main__":
    run_tests()

輸出結果:

推薦hahow線上學習python: https://igrape.net/30afN

  • 橫軸= / !
    • = 表示「要符合」
    • ! 表示「不要符合」
  • 縱軸? / ?<
    • ? 表示「往後看」
    • ?< 表示「往前看」

這樣就會變成:

白話版

regex 掃描文字本來就是由左到右
所以「看後面」是預設方向,只寫成 ?
只有「往回看前面」這種比較特別的方向,才額外寫成 ?<
因此沒有對稱的 ?> 來表示「往右看」。

因為 regex 本來就是沿字串由左向右匹配,
所以「看右邊」(lookahead)是預設,不另外寫成 ?>
只有「看左邊」(lookbehind)時,才加 < 變成 ?<

右看是預設,左看才加箭頭。

有一個 regex 語法真的長得像 ?>

(?>...)

但這不是 lookahead 的「向右箭頭版」
它通常是 atomic group(原子群組),意思完全不同。

所以更要避免把 ?> 想成「本來應該存在但沒採用的向右 lookaround」。


一句話總結

不是因為「英文」所以沒有 ?>
而是因為 regex 的預設匹配方向本來就是往右,
因此 lookahead 不必額外標方向;
只有往左看的 lookbehind 才需要 ?<

這個表格背後的組合邏輯

你可以這樣記:

第一步:先決定方向

  • ?:看後面
  • ?<:看前面

第二步:再決定條件

  • =:要符合
  • !:不要符合

所以四格就自然組出來:

  • ? + =(?=p)
  • ? + !(?!p)
  • ?< + =(?<=p)
  • ?< + !(?<!p)

其中:

  • ? = 往後看
  • ?< = 往前看
  • = = 要符合
  • ! = 不要符合

再濃縮成一句口訣

先看方向,再看肯否。

  • ? 看後面,?< 看前面
  • = 要符合,! 不可符合

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡