目標:
1. 徹底理解五種常用括號型構造的語意與行為差異
2. 分辨「是否消耗字元」「是否產生群組結果」
3. 透過逐段可執行例子建立肌肉記憶
4. 避免常見誤用(把 (?!…) 當 (?:…)、findall 行為誤判等)
核心五兄弟:
1. 捕獲群組 ( … )
2. 非捕獲群組 (?: … )
3. 命名捕獲群組 (?P<name> … )
4. 正向前瞻 (?= … )
5. 負向前瞻 (?! … )
補充:
– \s vs \S,repr() (!r) 在除錯時的價值
– findall 與捕獲群組的互動規則
– 何時必須顧及維護性(命名 + 非捕獲)
閱讀建議:
先跑「共用工具」→ 再逐節貼例子 → 改動 pattern 親自觀察行為。
# =========================================================
# 共用工具:show_match
# ---------------------------------------------------------
# 用來快速檢視:
# - 是否匹配
# - group(0) (整體)
# - 各 group(n)
# - 命名群組內容
# 為什麼用 !r?
# → repr() 可顯示隱藏字元 (tab, 換行, 前後空白) 以免肉眼忽略。
# =========================================================
import re
def show_match(pattern, text, flags=0):
"""
pattern : 正則模式字串
text : 測試字串
flags : re.I / re.M / re.S / re.X 等,可用 | 組合
"""
regex = re.compile(pattern, flags)
m = regex.search(text)
print("=" * 60)
print(f"Pattern: {pattern!r}")
print(f"Text : {text!r}")
if flags:
print(f"Flags : {flags}")
if not m:
print("=> NO MATCH\n")
return
print("=> MATCH")
print(" full match :", m.group(0))
# 顯示所有捕獲群組(若存在)
if m.groups():
for i, g in enumerate(m.groups(), start=1):
print(f" group({i}) :", f"{g!r}")
if m.groupdict():
print(" named dict :", m.groupdict())
print()
1. 捕獲群組 ( … )
功能:
- 建立一個可回溯(group(1), group(2)…)的片段。
- 在 re.sub() / 反向參照(\1)/ groupdict() 中使用。
- 預設所有括號都是捕獲,除非你刻意改成非捕獲 (?:)。
注意:
- 多餘的捕獲會讓 group 編號「漂移」,維護時容易錯。
- findall 有捕獲群組時,回傳形狀會改變(只回捕獲內容或 tuple)。
情境:從 “姓名:張三 年齡:25” 拆兩欄。
text = "姓名:張三 年齡:25"
pattern = r"姓名:(\S+)\s+年齡:(\d+)"
show_match(pattern, text)
m = re.search(pattern, text)
print("Name =", m.group(1))
print("Age =", m.group(2))
結構拆解 ( # 捕獲群組開始,會成為 group(1)
\S+ # 一段「至少一個」非空白字元(greedy) ) # 捕獲群組結束
\S 的意義
\s 代表所有「空白字元」:包含空格(space)、Tab(\t)、換行(\n)、回車(\r)、垂直定位符(\v)、換頁(\f),以及在 Unicode 模式下的其他空白(例如全形空白 U+3000)。
\S 則是「非空白」(NOT whitespace) 的意思,即:[^\s]
所以 \S 可以匹配:
中文:張、三
英文、數字:A、b、1、9
標點符號:: , . ! ? @ # (只要不是空白)
甚至 emoji、特殊符號(例如 👍、★)
輸出:
1.1 反向參照示例
目的:
- 確保兩段文字一模一樣(例如重複詞)
模式說明: - (\w+) 捕獲第一個“字母數字底線”串
- \s+ 一段空白
- \1 回溯第一個群組原樣文字
line = "hello hello hi hi test testX"
pattern = r"(\w+)\s+\1"
print(re.findall(pattern, line)) # 回傳每個重複詞的第一部分
for m in re.finditer(pattern, line):
print("Match:", m.group(0), "| word:", m.group(1))
輸出:
2. 非捕獲群組 (?: … )
功能:
- 分組 + 套量詞 / 控制優先順序,但“不產生群組結果”。
- 避免污染 group 編號。
- 讓 findall 在不需要子群組的情況下,直接回整體。
對比重點:
- (cat|dog){2} → findall 會回最後一次 alternation 中的捕獲內容
- (?:cat|dog){2} → findall 回整段匹配字串(更直觀)
- {2} 是量词,表示“前面的子模式重複恰好 2 次”
- 如果模式里有捕獲組,findall 會返回“每次匹配中,各捕獲組的內容”。當捕獲組被重覆時,Python 只保留該組在這次整體匹配中的最後一次捕獲內容。
- 用非捕獲分組:(?:cat|dog){2}。沒有捕獲組,findall 返回整個匹配片段,如 “catdog”。
何時使用:
- Alternation 不需要知道是哪一支(cat / dog 只影響匹配成立)
- 重複的固定片段:(?:ABC){3}
- 大 pattern 要保持穩定 group 序號
data = "cat dog catdog catcat dogdog"
cap = r"(cat|dog){2}"
non = r"(?:cat|dog){2}"
print("Capturing :", re.findall(cap, data)) # 只顯示最後一段
print("Non-capturing:", re.findall(non, data)) # 顯示整段
輸出:
2.1 findall 形狀差異示範
規則:
- 沒有捕獲群組 → 回整段列表
- 有 1 個捕獲群組 → 回那個群組的列表
- 有多個捕獲群組 → 回 list[ tuple(…) ]
3. 命名捕獲群組 (?P … )
優勢:
- 可讀性:m.group(‘major’) 比 m.group(1) 清楚。
- 彈性:插入/刪除前面括號不會壞(不依賴序號)。
- re.sub 中可用 \g 做替換。
語法:
- 定義:(?Ppattern)
- 回溯:(?P=name)
- 取值:m.group(‘name’) 或 m[‘name’](Python 3.11+ 亦支援)
應用:解析 semantic version: major.minor.patch
### 3.1 命名回溯
示例:檢查重複字 (與匿名 \1 等價,但語意清楚)
4. 正向前瞻 (?= … )
性質:
- “零寬” → 不消耗字元,只檢查接下來是否滿足條件。
- 典型用途:只在後面跟著特定後綴時匹配前綴;多條件疊加(密碼強度)。
常見誤解:
- 以為 (?=…) 會把裡面內容也吃掉 → 不會,它只是檢查。
- 與 (?:…) 不同:後者會消耗字元。
案例:抓所有“檔名前綴”但僅限後面跟 .jpg。
# %%
files = "a.jpg b.png c.jpg d.jpeg e.jpgx"
pat = r"\b\w+(?=\.jpg\b)" # 注意:\b 保證 .jpg 是完整副檔名
"""
#\b 是“單詞邊界”(word boundary)的錨點。它不匹配實際字符,只匹配“位置”。
常見用法:
\bcat\b 只匹配獨立的單詞 cat,不會匹配 concat、category。
\bjpg\b 確保匹配到完整的擴展名 “jpg”,不會把 “jpeg/jpgx” 算進去。
"""
print(re.findall(pat, files))
# 顯示 span:匹配範圍只含前綴,不含 .jpg
for m in re.finditer(pat, files):
print("Match:", m.group(0), "| span:", m.span())
輸出:
4.1 多條件疊加(密碼必須同時含字母與數字)
說明:
- (?=.*[A-Za-z]):後方某處有字母
- (?=.*\d) :後方某處有數字
- 最後整體再檢查長度:^[A-Za-z\d]{8,}$
下列 pattern 先用兩個 lookahead 斷言“必須同時具備”,再進入正式匹配。
5. 負向前瞻 (?! … )
性質:
- 零寬斷言:要求“後面不能”匹配某模式。
- 常用於:排除特定後綴、避免過度匹配、黑名單過濾。
案例:抓三位英數,但後面不可接數字(避免被視為更長 token 的開頭)。
5.1 排除指定副檔名 (.jpg)
注意:
- 簡單寫 \b\w+\b(?!.jpg) 可能仍會匹配 jpg 前綴(因為 \w+ 在 ‘.’ 前結束)。
- 嚴格做法之一:整體匹配“檔名+副檔名”再排除 .jpg,或使用負向前瞻包副檔名。
示例:匹配 非 .jpg 的 “name.ext”
6. 五種括號結構行為總表(語意 vs 消耗 vs 捕獲)
語法 | 消耗字元 | 捕獲 | 主要用途 | 心智記憶 |
---|---|---|---|---|
( … ) | 是 | 是 | 取值 / 回溯 / 拆欄位 | 「我要拿這段」 |
(?: … ) | 是 | 否 | 分組 / alternation / 套量詞 | 「只分組,不要值」 |
(?P … ) | 是 | 是 | 語意化取值、穩定 | 「有名字可讀」 |
(?= … ) | 否 | 否 | 後面必須符合 | 「看一下,不吃」 |
(?! … ) | 否 | 否 | 後面不能符合 | 「確認禁止」 |
備註:
- “消耗”= 會讓正則指標往後移;Lookahead 只是檢查不移動。
- 捕獲群組會影響 match.groups() / findall 回傳形狀。
7. 常見陷阱與修正
誤用 | 問題 | 正確作法 |
---|---|---|
(?!abc)abc | 幾乎無法匹配(先要求後面不是 abc 又接 abc) | 想非捕獲 → (?:abc) |
找不到 group(2) | 中途新增了一個 () | 不取值用 (?:…) |
findall 只回片段 | 有捕獲群組 → 形狀改變 | 移除多餘捕獲或用非捕獲 |
以為 lookahead 會吃字元 | 語意錯誤 | 記住零寬 = 不消耗 |
命名重複 (?P)(?P) | re.error | 保持名稱唯一 |
8. 小測驗(請先猜,再執行驗證)
問題:
- “(foo)+” 與 “(?:foo)+” 在 findall 上結果有何差異?
- foo(?=bar) 在 “foobar” 中 group(0) 是什麼?會包含 bar 嗎?
- foo(?!bar) 為何不匹配 “foobar”?
- (?P\w+)\s+\1 中 \1 指的是什麼?若把 (… ) 改成 (?: … ) 再用 \1?
- 如何讓重複詞比對改為大小寫不敏感(”Hello hello”)?
(下面程式碼會列出結果,對照你的預測。)
9. 進階延伸(可自行探索)
- Lookbehind:(?<=USD)\d+ / (?<!\d)\d{2}
- 條件群組:(?(name)yes|no)
- Inline flags:(?i:abc) 僅區塊忽略大小寫
- 避免某字出現:^(?!.*(DROP|DELETE)).+$
10. 記憶口訣
- 要值才捕獲,不要值用 (?:…).
- 命名群組保維護:(?P…) + m[‘key’]。
- (?=…):後面一定要;(?!…):後面一定不要。
- Lookahead 不吃字 → 可以堆疊多個條件。
- findall 形狀被捕獲群組決定;意外 tuple → 查看括號。
恭喜走完。接下來可以挑戰:把你的實際文件模式改寫成「命名 + 非捕獲的乾淨版本」。
推薦hahow線上學習python: https://igrape.net/30afN