攝影或3C

Python 正則表達式完全指南:貪婪 (Greedy) vs 非貪婪 (Lazy) ; .* vs .*?

在資料處理、網頁爬蟲或 LLM 輸出解析中,
我們常看到 .*? 這樣的寫法。
這篇筆記將透過實際程式碼,帶您徹底搞懂它的原理。

0. 環境準備與範例資料

首先,我們準備一段模擬 HTML 的字串。
我們的目標是提取每個 <div> 標籤中的內容。

import re

# 範例字串模擬一段 HTML 代碼
# 目標我們想分別提取 '你好'  '世界'
text = "<div>你好</div><div>世界</div>"

print(f"原始文本: {text}")

1. 貪婪匹配 (Greedy Match): .*

符號意義

  • . : 匹配任意字元 (換行除外)
  • * : 匹配 0 到無限多次

行為特性

「盡可能吃到越多越好」。它會從第一個開始的標籤,一路吃到最後一個結束標籤才肯停下來。

# 貪婪模式直接用 .*
# 行為它會從第一個 <div> 吃到最後一個</div>
pattern_greedy = r"<div>(.*)</div>"

match = re.findall(pattern_greedy, text)

print("貪婪匹配結果:", match)
# 預期結果: ['你好</div><div>世界'] 
# 失敗原因: 它把中間的 </div> 也當作內容吞掉了

2. 非貪婪/懶惰匹配 (Lazy Match): .*?

符號意義

  • ? : 當它跟在 * 後面時,意思是「一旦滿足條件就停止」

行為特性

「夠用就好」。它會尋找最近的一個結束標籤,滿足條件後就停止,然後繼續找下一個。

# 懶惰模式加上 ? 變成 .*?
# 行為遇到第一個</div> 就停止然後繼續找下一個
pattern_lazy = r"<div>(.*?)</div>"

match = re.findall(pattern_lazy, text)

print("懶惰匹配結果:", match)
# 預期結果: ['你好', '世界'] 
# 成功原因: 每次遇到 </div> 就結案不貪心

3. 總結比較表

寫法名稱行為描述記憶口訣
.*貪婪匹配 (Greedy)吃到撐死為止 (吃到最後一個結束符號)能吃多少吃多少
.*?非貪婪匹配 (Lazy)只要吃飽就停 (遇到第一個結束符號就停)夠用就好

4. 實戰應用:提取 LLM 回覆中的 JSON

在處理 AI (如 ChatGPT, Ollama) 的回應時,它們常會包含一些對話文字。我們可以使用 .*? 搭配 re.DOTALL (讓 . 也能匹配換行) 來精準提取程式碼區塊。

# 模擬一個 LLM 的混合輸出包含廢話和 JSON
llm_output = """
Here is the result you asked for:

```json
{
    "id": 1,
    "name": "GPT",
    "role": "Assistant"
}
```

I hope this helps! Let me know if you need anything else.
"""

# 使用 .*? 鎖定 ```json 和 ``` 之間的內
# flags=re.DOTALL 重要 . 可以匹配換行符號
#
# 解析正則表達式 r"```json\s*(.*?)\s*```":
# 1. ```json  : 匹配字面上的 "```json" 字串,這是 Markdown 代碼區塊的開頭
# 2. \s*      : 匹配 0 到多個空白字元 (包含換行 \n),目的是吃掉 "```json" 後面跟著的任何換行
# 3. (.*?)    : 非貪婪捕獲組這就是我們要的內容它會一直匹配任意字元直到遇到下面的結束標記
# 4. \s*      : 匹配 0 到多個空白字元目的是吃掉內容結尾處可能多餘的換行或空白
# 5. ```      : 匹配字面上的 "```",這是 Markdown 代碼區塊的結尾
json_pattern = r"```json\s*(.*?)\s*```"

json_match = re.findall(json_pattern, llm_output, flags=re.DOTALL)

print("提取到的 JSON 內容:")
for m in json_match:
    print(m)
    
# 進階直接轉成 Python Dict
import json
if json_match:
    data = json.loads(json_match[0])
    print(f"\n解析成功! Name: {data['name']}")

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

記憶口訣:問號 (?) 的本質是「猶豫」與「客氣」
我們可以把 ? 想像成一個很客氣、不確定、猶豫的人。

  1. 當 ? 獨自出現時 (量詞模式):0 或 1 次
    情境:colou?r
    心態:它在猶豫「這裡到底有沒有 ‘u’ 呢?」。
    “有沒有都可以啦,沒有也行 (0),有也就這一個 (1)。”
    口訣:「可有可無」。
  2. 當 ? 跟在 * 或 + 後面時 (懶惰模式):非貪婪
    情境:.? 心態:它在吃東西 (匹配字串) 時很客氣、很猶豫。 貪婪 (.) 像個餓死鬼:「我要把所有能吃的都吃光!」
    懶惰 (.*?) 則是很客氣:「呃… 我是不是該停了?這裡就可以停了嗎?好,那我就停這裡吧。」
    它只要一看到能讓條件成立的機會,就會「猶豫」著要不要繼續吃,最後選擇最少的量就停手。
    口訣:「見好就收」。
    圖像化記憶法

只要記得一個核心概念

? 的本質就是「最小化 (Minimize)」

  • 作為量詞:最小化數量 (最少 0)。
  • 作為修飾符:最小化範圍 (最短匹配)。

希望這個譬喻能幫助您一輩子記住它!

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

儲蓄保險王

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