正則表達式完整教學:50 個真實案例 + 線上測試器使用法

寫給:每次 grep 都查 cheatsheet 的工程師、想 validate 表單欄位的前端、寫爬蟲的資料工程師,以及永遠對 \w+ 跟 \w* 哪個是貪婪不確定的所有人。

正則表達式(Regular Expression,簡稱 regex 或 regexp)是描述字串模式的小型語言。1956 年由數學家 Stephen Kleene 提出,1970 年代被 ed / grep / sed 採用,至今仍是文字處理最強的工具之一。但 regex 的學習曲線陡峭——本文用 15 分鐘整理 5 個核心觀念、50 個實戰範例,並示範如何用 SuperTools 線上 Regex 測試器互動式練習。

1. 正則表達式是什麼

Regex 是一種用字串描述「字串模式」的迷你 DSL。例如 \d{3}-\d{4} 描述「三個數字、一個減號、四個數字」這個模式,可以一行匹配電話號碼。

主流引擎可分三大家族:

家族 代表實作 特色
POSIX BRE/ERE grep, sed 最古早、語法精簡
PCRE / PCRE2 PHP, nginx, Python regex lib 功能最完整
ECMAScript JavaScript 瀏覽器原生,部分功能缺失

Python re 介於 PCRE 與 POSIX 之間,多數 PCRE 高階特性(lookbehind 變長度、recursive pattern)要用第三方 regex 套件。

regex 適合做的事:

  • 文字搜尋 / 取代
  • 表單驗證
  • log 解析
  • 簡單字串切割

不適合:解析巢狀結構(HTML、JSON、code)—— 這些請用 parser。

2. 5 個核心觀念

2.1 字元類別(character class)

Pattern 意義
. 任意字元(不含換行)
\d 數字 [0-9]
\D 非數字
\w 字母+數字+底線 [A-Za-z0-9_]
\W 非 word 字元
\s 空白(含 tab、換行)
\S 非空白
[abc] a、b、c 任一個
[^abc] 不是 a、b、c
[a-z] a 到 z

踩坑:\w 在 Python 預設含 Unicode 字母(中文),JS 預設不含。要跨語言一致請用 [A-Za-z0-9_]。

2.2 量詞(quantifier)

Pattern 意義
* 0 次以上
+ 1 次以上
? 0 或 1 次
{n} 剛好 n 次
{n,} n 次以上
{n,m} n 到 m 次

貪婪 vs 懶惰:預設貪婪(盡量多吃),加 ? 變懶惰(盡量少吃):

import re
text = "<b>foo</b><b>bar</b>"
re.findall(r"<b>.*</b>", text)   # ['<b>foo</b><b>bar</b>'] 貪婪
re.findall(r"<b>.*?</b>", text)  # ['<b>foo</b>', '<b>bar</b>'] 懶惰

2.3 錨點(anchor)與邊界

Pattern 意義
^ 行首
$ 行尾
\b word 邊界
\B 非 word 邊界
\A 字串開頭
\z 字串結尾

注意 ^$ 跟 \A\z 差別:multiline flag 開啟時,^$ 配對「每一行」開頭與結尾;\A\z 永遠是整個字串。

re.findall(r"^hello", "hello\nhello", re.MULTILINE)  # ['hello', 'hello']
re.findall(r"\Ahello", "hello\nhello")               # ['hello']

2.4 群組與捕獲

# 捕獲群組
m = re.match(r"(\w+)@(\w+\.\w+)", "alice@example.com")
m.group(0)  # 'alice@example.com' 整段
m.group(1)  # 'alice'
m.group(2)  # 'example.com'

# 命名群組
m = re.match(r"(?P<user>\w+)@(?P<domain>\w+\.\w+)", "alice@example.com")
m.group("user")    # 'alice'
m.group("domain")  # 'example.com'

# 非捕獲群組(只分組不抓內容)
re.findall(r"(?:abc)+", "abcabcabc")  # ['abcabcabc']

2.5 前後查找(lookaround)

Pattern 意義
(?=X) 後面是 X(不消耗字元)
(?!X) 後面不是 X
(?<=X) 前面是 X
(?<!X) 前面不是 X

用途:抓取符合上下文但不想包進結果的字串。

# 抓 $ 後的數字(不包 $)
re.findall(r"(?<=\$)\d+", "price $100 and $200")
# ['100', '200']

# 抓不是後接 .png 的檔名
re.findall(r"\w+(?!\.png)", "a.png b.jpg c.png")

3. 實戰範例

下面 50 個 pattern 涵蓋常見場景,可以直接複製使用。建議搭配 SuperTools 線上測試器逐一試跑。

範例 1-10:表單驗證

# 1. Email(簡化版,RFC 5322 完整版超過 6000 字元)
^[\w.+-]+@[\w-]+(\.[\w-]+)+$

# 2. URL
^https?:\/\/[\w.-]+(:\d+)?(\/[\w._~:/?#[\]@!$&'()*+,;=-]*)?$

# 3. IPv4
^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$

# 4. IPv6(簡化版)
^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$

# 5. 台灣手機
^09\d{8}$

# 6. 台灣市話(含區碼)
^0[2-9]-?\d{6,8}$

# 7. 身分證字號
^[A-Z][12]\d{8}$

# 8. 信用卡(VISA / Master / JCB / Amex 通用)
^(?:4\d{15}|5[1-5]\d{14}|3[47]\d{13}|35\d{14})$

# 9. 強密碼(8+ 字、含大小寫、數字、特殊字元)
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[\W_]).{8,}$

# 10. 統一發票
^[A-Z]{2}-?\d{8}$

範例 11-20:日期時間

# 11. YYYY-MM-DD
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$

# 12. HH:MM 24 小時
^([01]\d|2[0-3]):[0-5]\d$

# 13. ISO 8601(YYYY-MM-DDTHH:MM:SSZ)
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$

# 14. 民國年(112-05-18)
^1\d{2}-\d{2}-\d{2}$

# 15. Unix timestamp(10 位)
^\d{10}$

# 16. 月份英文簡寫
^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$

# 17. 星期英文
^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)(day)?$

# 18. 12 小時制(HH:MM AM/PM)
^(0?[1-9]|1[0-2]):[0-5]\d\s?(AM|PM|am|pm)$

# 19. 持續時間(HH:MM:SS)
^\d{2}:\d{2}:\d{2}$

# 20. 月日(MM/DD)
^(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])$

範例 21-30:HTML / Markdown

# 21. HTML 標籤(簡單版)
<(\w+)([^>]*)>(.*?)</\1>

# 22. img src
<img[^>]+src=["']([^"']+)["']

# 23. 超連結 href
<a[^>]+href=["']([^"']+)["']

# 24. Markdown 連結
\[([^\]]+)\]\(([^)]+)\)

# 25. Markdown 圖片
!\[([^\]]*)\]\(([^)]+)\)

# 26. Markdown H1
^#\s+(.+)$

# 27. Markdown 程式碼塊
```(\w*)\n([\s\S]*?)```

# 28. HTML 註解
<!--[\s\S]*?-->

# 29. CSS 選擇器(簡單版)
^[.#]?[a-zA-Z][\w-]*(\s*[>+~]\s*[.#]?[a-zA-Z][\w-]*)*$

# 30. HEX 顏色
^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$

範例 31-40:log 解析

# 31. Apache access log(commonl format)
^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) ([^"]+) ([^"]+)" (\d+) (\d+|-)$

# 32. nginx error log
^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+)$

# 33. JSON log line
^\{.*"level":"(\w+)".*\}$

# 34. python traceback file:line
File "([^"]+)", line (\d+), in (\w+)

# 35. log 中的 UUID
\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b

# 36. 錯誤等級
\b(DEBUG|INFO|WARN|WARNING|ERROR|FATAL|CRITICAL)\b

# 37. 抓出 stack trace 第一行
^Exception in (\S+): (.+)$

# 38. log 中的 IP + status
^(\S+) .+ "(\d{3})"

# 39. duration 毫秒
(\d+(\.\d+)?)\s?ms

# 40. 帶 timestamp 的多行 log 切割
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}

範例 41-50:資料清洗

# 41. 移除 HTML 標籤
<[^>]+>

# 42. 多重空白合一
\s+

# 43. 開頭結尾空白
^\s+|\s+$

# 44. 全形空白
 

# 45. 中文(CJK 統一漢字)
[一-鿿]

# 46. emoji(簡化版)
[\u{1f300}-\u{1f9ff}]

# 47. 中文標點符號
[ -〿＀-￯]

# 48. base64 字串(含 padding)
^[A-Za-z0-9+/]+={0,2}$

# 49. JWT
^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$

# 50. 重複空行壓成一行
\n{2,}

4. 進階技巧

4.1 反向引用(backreference)

群組捕獲後可以用 \1、\2 引用:

# 找出重複的單字
re.findall(r"\b(\w+)\s+\1\b", "the the quick brown fox fox")
# ['the', 'fox']

# 取代 HTML 標籤包裹的文字
re.sub(r"<(\w+)>(.*?)</\1>", r"[\2]", "<b>hello</b>")
# '[hello]'

4.2 條件式(PCRE)

# 如果群組 1 有匹配,就要求 X,否則要求 Y
(?(1)X|Y)

JavaScript 不支援,Python re 也不支援,要用 regex lib。

4.3 原子群組 + 占有量詞(防回溯災難)

某些 regex 在惡意輸入下會 catastrophic backtracking 卡死 CPU:

# 災難 pattern
^(a+)+$

對輸入 "aaaaaaaaaaaaaaaaaaaa!" 會跑數秒。改用原子群組:

^(?>a+)+$

或用占有量詞 a++(不回溯)。Java、PCRE 支援;JS、Python 不支援。

4.4 多行 flag 與 dot-all

Flag 意義
i 忽略大小寫
m multiline(^$ 配對每行)
s dot-all(. 包含換行)
x extended(忽略 whitespace 與 # 註解)
g global(全部匹配,僅 JS)
re.findall(r"foo.bar", "foo\nbar")            # []
re.findall(r"foo.bar", "foo\nbar", re.DOTALL)  # ['foo\nbar']

4.5 Unicode property(pcre / python regex)

\p{L}      # 任何字母(含中文、日文、阿拉伯文)
\p{Han}    # 漢字
\p{N}      # 數字
\p{P}      # 標點

JS ES2018+ 用 u flag 後支援 \p{L}。

5. 常見錯誤與解法

錯誤 1:用 regex 解析 HTML

症狀:抓 <a> 連結時錯抓註解 / 字串內容。 根因:HTML 是巢狀結構,不是 regular language。 解法:用 BeautifulSoup / Cheerio / lxml 等 HTML parser。

錯誤 2:catastrophic backtracking 卡死

症狀:CPU 100%,特定輸入 regex 跑數秒不停。 根因:巢狀量詞如 (a+)+。 解法:簡化 pattern、使用原子群組、改用線性時間引擎(Google RE2)。

錯誤 3:忘記跳脫特殊字元

症狀:想匹配 1.2.3 卻匹配到任何三字元。 根因:. 是 metacharacter,要用 \.。 解法:把字面值 string 過 re.escape() 再嵌入 pattern。

import re
target = "1.2.3"
pattern = re.escape(target)  # '1\\.2\\.3'

錯誤 4:JS 跟 Python 結果不同

症狀:同一個 regex 在 Python 過、在瀏覽器不過。 根因:lookbehind 變長度、命名群組語法、Unicode 處理三大引擎不同。 解法:跨語言部署的 regex 限定用 ECMAScript 共用子集;複雜邏輯放後端 Python / Go。

錯誤 5:貪婪 vs 懶惰搞錯

症狀:抓 <b>foo</b><b>bar</b> 時整段都被抓。 根因:.* 是貪婪。 解法:改 .*? 懶惰,或用 [^<]+ 限制字元集。

6. 用 SuperTools 線上版完成相同任務

不想開 Python 互動式 shell?開 SuperTools Regex 測試器:

  • 即時 highlight:輸入 pattern 立刻在 test string 上 highlight 所有 match
  • 群組 panel:右側顯示每個捕獲群組的內容
  • flag toggle:i / m / s / g / u 一鍵切換
  • 替換預覽:輸入 replacement template 看取代結果
  • explain mode:把 pattern 拆解成自然語言描述(適合學習)
  • 多語言切換:JavaScript / Python / Go 三引擎隨選

工具完全 client-side(瀏覽器原生 RegExp + WASM Python re),測試字串不會上傳。

實際使用流程:

  1. 從 log / API 複製需要解析的字串
  2. 開 supertools.tw/dev/regex 貼到右側 test string
  3. 在左側 pattern 區邊寫邊看 highlight
  4. 寫對後點「Copy as Python / JS」貼回程式碼

立即使用 SuperTools Regex 測試器:https://supertools.tw/dev/regex

7. 其他語言實作

Python

import re
re.match(r"^\d+$", "123")         # 從頭開始匹配
re.search(r"\d+", "abc123def")    # 找第一個
re.findall(r"\d+", "1 2 3")       # ['1', '2', '3']
re.sub(r"\d", "*", "a1b2c3")      # 'a*b*c*'

JavaScript

"abc123".match(/\d+/);              // ['123']
"a1b2".replace(/\d/g, "*");         // 'a*b*'
/^[a-z]+$/i.test("Hello");          // true

Go

import "regexp"
r := regexp.MustCompile(`\d+`)
r.FindAllString("1 2 3", -1)        // ["1", "2", "3"]
r.ReplaceAllString("a1b2", "*")     // "a*b*"

Rust

use regex::Regex;
let re = Regex::new(r"\d+").unwrap();
for m in re.find_iter("1 2 3") { println!("{}", m.as_str()); }

Bash

echo "abc123" | grep -oE "[0-9]+"   # 123
echo "abc123" | sed -E 's/[0-9]+/X/' # abcX

8. FAQ

Q1:要不要學 PCRE 跟 POSIX 全部? 不用。先學 PCRE / ECMAScript 共通子集,覆蓋 90% 場景。

Q2:regex 跟 glob 差別? glob 是 shell 萬用字元(*.txt),語意更簡單;regex 是完整模式語言。

Q3:catastrophic backtracking 怎麼預防? 避免巢狀量詞、用原子群組、用 RE2 引擎、給 regex 設 timeout。

Q4:要學 lookahead 嗎? 強烈建議。它是密碼強度、token 驗證等場景的關鍵。

Q5:能用 regex 做 lexer 嗎? 可以,但只適合簡單語法。複雜語言請用 ANTLR / yacc / 手寫 parser。

9. 實戰案例研究

案例 1:Cloudflare 2019 全球當機 27 分鐘

2019 年 7 月 2 日,Cloudflare 推送一條新 WAF 規則含特定 regex:

(?:(?:\"|'|\]|\}|\\|\d|(?:nan|infinity|true|false|null|undefined|symbol|math)|\`|\-|\+)+[)]*;?((?:\s|-|~|!|{}|\|\||\+)*.*(?:.*=.*)))

這條 pattern 對特定 user request 造成 catastrophic backtracking,全球 edge server CPU 飆到 100%,27 分鐘全球無法存取網站,連 Cloudflare 自己網站都掛。

事後檢討:

  • 全球 CDN 任何 regex 改動必經多輪壓測
  • 改用 Rust RE2(線性時間引擎)
  • 規則上線前 staging 7 天

教訓:regex 在 production 系統有真實 SLO 影響。

案例 2:log 解析從 20 分鐘 → 2 秒

某團隊用 Python 解析 1GB nginx log 提取所有 IP,原本:

for line in open("access.log"):
    ip = re.match(r"^(\d+\.\d+\.\d+\.\d+)", line).group(1)

每行 re.compile 一次(其實 Python 有 cache,但仍 overhead)+ 沒用 re.compile 預編。

優化:

import re
IP_RE = re.compile(rb"^(\d+\.\d+\.\d+\.\d+)")  # 預編 + binary mode
with open("access.log", "rb") as f:
    ips = [m.group(1) for line in f if (m := IP_RE.match(line))]

加 binary mode(不 decode)+ 預編 + walrus operator,從 20 分鐘 → 2 秒(600x 加速)。

案例 3:表單驗證從 50 個 regex 變 1 個 JSON Schema

某入會表單原本有 20 個欄位,每個欄位有 1-3 個 regex(email、phone、id_card 等),總共 50+ regex 散在前後端。同步維護地獄。

改用 JSON Schema:

{
  "type": "object",
  "properties": {
    "email": {"type": "string", "format": "email"},
    "phone": {"type": "string", "pattern": "^09\\d{8}$"},
    "id_card": {"type": "string", "pattern": "^[A-Z][12]\\d{8}$"}
  }
}

前後端共用同一 schema:

  • 前端 ajv validate
  • 後端 jsonschema validate

新增欄位只改 schema、不改 code。

10. 結論 + 延伸閱讀

正則表達式是文字處理的瑞士刀。本文 50 個範例涵蓋 90% 日常需求,背後 5 個核心觀念(字元類別、量詞、錨點、群組、lookaround)熟練後幾乎所有 pattern 都能拆解。下次寫 validation 或 log 解析時,先到 SuperTools 線上測試器把 pattern 練到綠燈再貼進程式碼,省下無數 debug 時間。三條黃金法則:生產 regex 必先壓測防回溯、熱迴圈用 re.compile 預編、多欄位 validation 改 JSON Schema。

延伸閱讀: