寫程式的時候,多少都會遇到需要檢查文字格式、從一大段文字裡撈出特定資訊的場景。像是檢查 Email 格式對不對、從 Log 裡面抓 IP 位址、或是把日期格式批次轉換。這些事情如果用一般的字串操作來處理,程式碼會又臭又長,但如果用 Regular Expression(正規表達式,簡稱 Regex),往往一行 pattern 就能搞定。不過很多開發者一看到像 ^(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$ 這樣的 pattern 就頭痛,覺得像在看外星文。其實 Regex 是由一組簡單的規則積木組合而成,只要一塊一塊學,很快就能看懂,甚至自己寫出來。

Regular Expression 是什麼

簡單來說,Regular Expression 是一種描述文字模式的迷你語言。平常我們在文件裡按 Ctrl+F(macOS 是 Cmd+F)搜尋,只能找「固定的文字」,例如搜尋 “error” 就只會找到 error 這個字。但如果想找的是「所有 Email 地址」或「任何 YYYY-MM-DD 格式的日期」,固定文字就做不到了——這時候就需要 Regex 來描述這個模式(pattern),讓程式幫我們比對、擷取、甚至取代符合模式的文字。

Regex 本身不是一種程式語言,而是一種語法規範,被各種程式語言和工具支援。不管是 JavaScript、Python、Java、Go,還是終端機上的 grepsed,甚至 VS Code 的搜尋功能,都能使用 Regex。

在哪些地方會用到 Regex

使用場景範例沒有 Regex有 Regex
表單驗證檢查 Email 格式手動拆字串逐一判斷一行 pattern 搞定
Log 分析從 log 提取 IP 和時間戳寫一堆 split / indexOf一個 pattern 精準擷取
文字取代日期格式 MM/DD/YYYY → YYYY-MM-DD複雜的字串操作邏輯sed 或 IDE 一次取代
程式碼搜尋找出所有 TODO 和 FIXME 註解grep 純文字搜尋有限grep -E 支援 pattern

在哪裡練習

學 Regex 最好的方式就是邊看邊試。推薦使用 regex101.com ,它可以即時測試 pattern,還會用視覺化的方式解釋每一段的意思,支援 JavaScript、Python、Java、Go 等多種語言的 Regex 引擎。另外 regexr.com 也是不錯的選擇。建議把這些工具打開,跟著文章的範例一起操作。

基本語法入門

Regex 的核心是由一組「積木」組成的,每塊積木代表一個規則。接下來會從最簡單的開始,一個觀念一個觀念往上堆疊。

字面比對(Literal Match)

最簡單的 Regex 就是直接寫出想找的文字。例如 pattern hello 就會比對到字串中的 “hello”。大部分的字元都是直接對應自己,只有少數「特殊字元」有其他意義。

Pattern: hello
文字:    say hello world
比對:        ^^^^^ (比對到 hello)

特殊字元與跳脫(Metacharacters)

Regex 中有一些字元有特殊含義,稱為特殊字元(metacharacters)。如果想比對這些字元本身,就需要在前面加上反斜線 \ 來跳脫(escape)。

特殊字元意義比對字面意思
.任意一個字元(換行除外)\.
^行首\^
$行尾\$
*前一個項目出現 0 次以上\*
+前一個項目出現 1 次以上\+
?前一個項目出現 0 或 1 次\?
{}指定出現次數\{\}
[]字元類別\[\]
()群組\(\)
|或(交替)\|
\跳脫字元\\

例如想比對 IP 位址中的 .,就要寫 \.,不然 . 會比對到任何字元。

Character Class 字元類別 […]

用中括號 [] 包起來的是字元類別,表示「比對其中任何一個字元」。

# 比對 a、b 或 c 其中一個
[abc]

# 用範圍表示:比對 a 到 z 的任何小寫英文字母
[a-z]

# 組合多種範圍:小寫、大寫、數字
[a-zA-Z0-9]

# 取反:比對「不是」a、b、c 的任何字元
[^abc]

# 在 [] 裡面,大部分特殊字元會失去特殊意義
# 例如 [.] 就是比對 . 這個字元本身
[.]

預定義字元類別(Shorthand Character Classes)

常用的字元類別有簡寫,省去每次都要寫 [a-zA-Z0-9] 的麻煩。

簡寫等同於說明
\d[0-9]數字
\D[^0-9]非數字
\w[a-zA-Z0-9_]文字字元(字母、數字、底線)
\W[^a-zA-Z0-9_]非文字字元
\s[ \t\n\r\f\v]空白字元(空格、Tab、換行等)
\S[^ \t\n\r\f\v]非空白字元

大寫版本就是小寫的相反。例如 \d 是數字,\D 就是「不是數字的任何字元」。

Quantifier 量詞

量詞用來指定前面的項目要出現幾次。

量詞意義範例
*0 次以上ab*c → ac, abc, abbc, abbbc…
+1 次以上ab+c → abc, abbc, abbbc…(不含 ac)
?0 或 1 次colou?r → color, colour
{n}剛好 n 次\d{4} → 剛好 4 個數字
{n,}至少 n 次\d{2,} → 2 個以上數字
{n,m}n 到 m 次\d{2,4} → 2 到 4 個數字

預設情況下,量詞是貪婪的(greedy),會盡可能多吃字元。在量詞後面加上 ? 就變成惰性的(lazy),盡可能少吃。這個差異在後面的進階語法會更詳細說明。

# 貪婪模式(預設)
Pattern: <.*>
文字:    <b>hello</b>
比對:    <b>hello</b>  (一次吃到底)

# 惰性模式
Pattern: <.*?>
文字:    <b>hello</b>
比對:    <b>  和  </b>  (盡量少吃)

Anchor 錨點

錨點不比對任何字元,而是比對位置

錨點意義範例
^行首(或字串開頭)^Hello → 只比對行首的 Hello
$行尾(或字串結尾)end$ → 只比對行尾的 end
\b單字邊界\bcat\b → 比對 cat 但不比對 catch
\B非單字邊界\Bcat\B → 比對 concatenate 中的 cat

錨點為什麼重要?如果驗證 Email 只寫 \w+@\w+\.\w+,像 [email protected]!!! 也會比對成功,因為 pattern 只要在字串中任何位置比對到就算過。加上 ^$ 之後就能確保整個字串都要符合。

分組與擷取(Groups and Capturing)

學會了基本的比對規則之後,接下來看怎麼把比對到的內容「拆開來用」。

捕獲群組 (…)

用小括號 () 把 pattern 的一部分包起來,就形成一個捕獲群組(capturing group)。比對成功後,可以分別取出每個群組的內容。

# 比對日期並分別擷取年、月、日
Pattern: (\d{4})-(\d{2})-(\d{2})
文字:    2026-04-09
結果:    群組 1 = 2026, 群組 2 = 04, 群組 3 = 09

在各語言中取出群組的方式:

// JavaScript
const match = '2026-04-09'.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // "2026"
console.log(match[2]); // "04"
console.log(match[3]); // "09"
# Python
import re
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2026-04-09')
print(match.group(1))  # "2026"
print(match.group(2))  # "04"
print(match.group(3))  # "09"
// Java
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("2026-04-09");
if (matcher.find()) {
    System.out.println(matcher.group(1)); // "2026"
    System.out.println(matcher.group(2)); // "04"
    System.out.println(matcher.group(3)); // "09"
}

非捕獲群組 (?:…)

有時候需要用小括號來做分組(例如搭配量詞 (?:ab)+),但不需要擷取群組內容。這時候加上 ?: 就變成非捕獲群組,不佔群組編號,效能也稍微好一點。

# 捕獲群組:(?:https?://)會佔一個群組編號
Pattern: (https?://)(\w+\.\w+)
文字:    https://klab.tw
結果:    群組 1 = https://, 群組 2 = klab.tw

# 非捕獲群組:(?:https?://) 不佔編號
Pattern: (?:https?://)(\w+\.\w+)
文字:    https://klab.tw
結果:    群組 1 = klab.tw

Named Group 命名群組

當 pattern 裡有很多群組,用數字 1, 2, 3... 來記哪個是哪個很容易搞混。命名群組讓我們可以幫每個群組取名字,程式碼更容易閱讀和維護。

語言命名群組語法取值方式
JavaScript(?<name>...)match.groups.name
Python(?P<name>...)match.group('name')
Java(?<name>...)matcher.group("name")
.NET / C#(?<name>...)match.Groups["name"]
Go(?P<name>...)match[index](需配合 SubexpNames)
// JavaScript 命名群組範例
const pattern = /(?\d{4})-(?\d{2})-(?\d{2})/;
const match = '2026-04-09'.match(pattern);
console.log(match.groups.year);  // "2026"
console.log(match.groups.month); // "04"
console.log(match.groups.day);   // "09"

Backreference 反向參照

反向參照可以在 pattern 裡引用前面已經捕獲的群組內容。最經典的用法就是找出重複的單字。

# 找出連續重複的單字
Pattern: \b(\w+)\s+\1\b
文字:    the the quick brown fox fox
比對:    "the the" 和 "fox fox"

# \1 參照第一個群組捕獲的內容
# 如果群組 1 捕獲了 "the",\1 就只會比對 "the"

命名群組也可以做反向參照:用 \k<name>(JavaScript / .NET)或 (?P=name)(Python)。

進階語法

掌握了基礎之後,接下來這些進階功能會讓 Regex 的能力更上一層。

Alternation 交替 |

| 表示「或」,可以比對左邊或右邊的 pattern。搭配群組使用可以限定「或」的範圍。

# 比對 cat 或 dog
cat|dog

# 搭配群組:比對 cat food 或 dog food
(cat|dog) food

# 不加群組的話,| 的範圍是整個 pattern
# cat food|dog 會比對 "cat food" 或 "dog",不是 "cat food" 或 "dog food"

Lookahead 與 Lookbehind 環視

環視(lookaround)是 Regex 中比較進階的功能。可以把它想成「往前看路或往後看路,但不移動」。環視會檢查某個位置的前面或後面是否符合條件,但不會消耗任何字元

語法名稱說明
(?=...)Positive Lookahead後面必須符合
(?!...)Negative Lookahead後面必須不符合
(?<=...)Positive Lookbehind前面必須符合
(?<!...)Negative Lookbehind前面必須不符合
# Positive Lookahead:找數字,但後面要有 "元"
Pattern: \d+(?=元)
文字:    售價 100元,庫存 50個
比對:    100(50 不會比對到,因為後面不是 "元")

# Negative Lookahead:找數字,但後面不能是 "元"
Pattern: \d+(?!元)
文字:    售價 100元,庫存 50個
比對:    10 和 50

# Positive Lookbehind:找數字,但前面要有 "$"
Pattern: (?<=\$)\d+
文字:    Price: $199, Code: A100
比對:    199(100 不會比對到,因為前面不是 $)

# Negative Lookbehind:找數字,但前面不能有 "$"
Pattern: (?

環視最常見的實戰用法就是密碼強度驗證。開頭提到的 ^(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$,就是用多個 lookahead 來同時檢查「包含大寫」和「包含數字」這兩個條件。

要注意的是,不同語言對 lookbehind 的支援程度不一樣:

功能JavaScriptPythonJavaGogrep -P
Lookahead
Lookbehind(固定長度)
Lookbehind(可變長度)✓(ES2024+)

Go 的 regexp 套件不支援環視,這是因為 Go 使用 RE2 引擎,保證線性時間複雜度,但犧牲了環視功能。

Greedy vs Lazy vs Possessive 貪婪、惰性與占有量詞

在基本語法提過量詞預設是貪婪的,這裡更深入解釋背後的回溯(backtracking)機制。

類型語法行為
Greedy(貪婪)* + ?先盡量多吃,比對失敗就一個一個吐回來重試
Lazy(惰性)*? +? ??先盡量少吃,比對失敗就多吃一個重試
Possessive(占有)*+ ++ ?+盡量多吃,但不回溯(Java / PCRE 支援)
# 想比對引號內的字串
文字: He said "hello" and "world"

# Greedy:吃到最後一個引號
Pattern: ".*"
比對:    "hello" and "world"

# Lazy:吃到第一個引號就停
Pattern: ".*?"
比對:    "hello" 和 "world"

# 更好的做法:用排除字元類別,不需要 lazy
Pattern: "[^"]*"
比對:    "hello" 和 "world"

最後那個用 [^"]* 的寫法值得記住,很多時候比 lazy 更直覺也更有效率。

Unicode 與多語言支援

對繁體中文使用者來說,比對中文字元是很常見的需求。傳統做法是用 Unicode 範圍 [\u4e00-\u9fff],但更現代的方式是使用 Unicode Property

// JavaScript(需要 u flag)
// 比對任何中日韓統一表意文字(CJK)
const pattern = /\p{Script=Han}+/u;
'Hello 世界'.match(pattern); // ["世界"]

// 比對任何 Unicode 字母(含中文、日文假名等)
const letterPattern = /\p{L}+/gu;
'Hello 世界 こんにちは'.match(letterPattern);
// ["Hello", "世界", "こんにちは"]
# Python(使用 regex 套件,非內建 re)
import regex
pattern = regex.compile(r'\p{Han}+')
pattern.findall('Hello 世界')  # ['世界']

# 用內建 re 的話,可以用 Unicode 範圍
import re
re.findall(r'[\u4e00-\u9fff]+', 'Hello 世界')  # ['世界']

實戰應用

學了這麼多語法,來看看實際開發中怎麼使用。

表單驗證

Email 驗證(簡化版)

# Pattern 拆解:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

^                     # 字串開頭
[a-zA-Z0-9._%+-]+    # 使用者名稱:英數字和一些特殊符號,至少 1 個
@                     # @ 符號
[a-zA-Z0-9.-]+       # 域名:英數字、點、連字號
\.                    # 一個 . (跳脫)
[a-zA-Z]{2,}         # 頂級域名:至少 2 個英文字母(com, tw, org...)
$                     # 字串結尾

這是簡化版的 Email 驗證,涵蓋絕大多數正常的 Email 地址。真正的 Email 規範(RFC 5322)極其複雜,實務上這個 pattern 已經夠用。

台灣手機號碼

# 台灣手機號碼:09 開頭,後面 8 個數字
^09\d{8}$

# 允許中間有連字號的格式(0912-345-678)
^09\d{2}-?\d{3}-?\d{3}$

台灣身分證字號

# 基本格式驗證:1 個大寫英文字母 + 1 或 2(性別碼)+ 8 個數字
^[A-Z][12]\d{8}$

# 2020 年後新增的居留證號碼格式
^[A-Z][89A-D]\d{8}$

注意:這些 pattern 只驗證「格式」正不正確,不驗證「內容」是否合法(例如身分證字號有檢查碼邏輯,需要額外計算)。

文字擷取與資料清洗

解析 Nginx Access Log

一筆典型的 Nginx access log 長這樣:

203.0.113.50 - - [09/Apr/2026:10:15:32 +0800] "GET /api/users HTTP/1.1" 200 1234

用命名群組一次擷取所有欄位:

import re

log_pattern = re.compile(
    r'(?P\d+\.\d+\.\d+\.\d+)'     # IP 位址
    r' - - '                              # 固定分隔
    r'\[(?P

從文字中提取所有 URL

import re

text = "參考 https://klab.tw/2026/04/regex-tutorial/ 和 http://example.com/path?q=1"
urls = re.findall(r'https?://\S+', text)
print(urls)
# ['https://klab.tw/2026/04/regex-tutorial/', 'http://example.com/path?q=1']

清除 HTML 標籤

import re

html = '<p>Hello <b>World</b></p>'
clean = re.sub(r'<[^>]+>', '', html)
print(clean)  # Hello World

清除 HTML 標籤用 Regex 處理簡單的情境沒問題,但如果是複雜的巢狀 HTML,建議使用專門的 HTML parser(例如 Python 的 BeautifulSoup)。

開發工具整合

Regex 不只用在程式碼裡,日常使用的開發工具也處處用得到。

grep — 在檔案中搜尋

# 從 log 中找出所有 IP 位址
grep -oE '\b[0-9]{1,3}(\.[0-9]{1,3}){3}\b' access.log

# 找出所有 TODO 和 FIXME 註解
grep -rn -E 'TODO|FIXME' src/

# 找出定義函式的行(JavaScript)
grep -rn -E 'function\s+\w+|const\s+\w+\s*=' src/*.js

sed — 批次取代

# 把日期格式從 MM/DD/YYYY 轉成 YYYY-MM-DD
sed -E 's|([0-9]{2})/([0-9]{2})/([0-9]{4})|\3-\1-\2|g' data.csv

# 移除行尾空白
sed -E 's/[[:space:]]+$//' file.txt

# 把 console.log 全部註解掉
sed -E 's/(console\.log\()/\/\/ \1/' app.js

VS Code 搜尋取代

在 VS Code 的搜尋框開啟 Regex 模式(點擊 .* 圖示或按 Alt+R),就可以使用 Regex 搜尋和取代。取代時用 $1$2 參照群組。

# 把 console.log("...") 改成 logger.info("...")
搜尋:  console\.log\(([^)]+)\)
取代:  logger.info($1)

Git grep — 搜尋整個 Git 倉庫

# 在整個 Git 倉庫搜尋符合 pattern 的內容
git grep -E 'TODO|FIXME|HACK'

# 搜尋特定語言的檔案
git grep -E 'import\s+.*axios' -- '*.ts' '*.js'

各語言 Regex 差異對照

Regex 語法在各語言大致相同,但建立方式、flag 名稱和一些細節有所不同。以下整理主要差異:

功能JavaScriptPythonJavaGogrep -E
建立方式/pattern/flagsre.compile(r'...')Pattern.compile("...")regexp.Compile("...")CLI 參數
全域搜尋g flagre.findall()find() 迴圈FindAllString()預設行為
忽略大小寫i flagre.IGNORECASECASE_INSENSITIVE(?i) 前綴-i flag
多行模式m flagre.MULTILINEMULTILINE(?m) 前綴預設逐行
Named Group(?<n>...)(?P<n>...)(?<n>...)(?P<n>...)(?P<n>...)
跳脫字串不需要雙跳脫使用 r'...'需要 \\ 雙跳脫使用反引號 `...`不需要

特別注意 Java 的字串需要對反斜線做雙跳脫,所以 \d 在 Java 裡要寫成 "\\d",Python 用 raw string r'\d' 就不用,Go 用反引號也不用。

常見陷阱與效能考量

Catastrophic Backtracking 災難性回溯

某些 pattern 在特定輸入下會導致引擎瘋狂回溯,執行時間呈指數成長。這不只是效能問題,還可能被惡意利用——這種攻擊叫做 ReDoS(Regular Expression Denial of Service)

# 危險 pattern 範例
Pattern: (a+)+$
文字:    aaaaaaaaaaaaaaaaaaaaX

# 引擎會嘗試所有可能的 a 分組方式,時間複雜度 O(2^n)
# 20 個 a 加一個 X,可能要跑好幾秒甚至更久

避免災難性回溯的原則:

  • 避免巢狀量詞(如 (a+)+(a*)*
  • 用具體的字元類別取代 .*(例如 [^"]* 取代 .*?
  • 使用 Possessive 量詞或 Atomic Group 防止回溯(如果語言支援)
  • 在 regex101.com 上測試,留意步驟數(steps)是否異常高

不要用 Regex 的時候

Regex 很強大,但不是萬能的。以下情境建議用其他工具:

  • 解析 HTML/XML:Regex 無法正確處理巢狀標籤,請用 HTML parser(Python BeautifulSoup、JavaScript DOMParser)
  • 複雜的巢狀結構:像是 JSON、程式語言語法,Regex 無法處理遞迴巢狀,需要用 parser
  • 簡單的字串操作:只是檢查開頭結尾或是否包含某段文字,用 startsWith()includes()endsWith() 更清楚也更快

常用語法速查表

最後整理一份速查表,方便日後回來查閱。

字元比對

語法說明
.任意一個字元(換行除外)
\d / \D數字 / 非數字
\w / \W文字字元 / 非文字字元
\s / \S空白 / 非空白
[abc]a、b 或 c
[a-z]a 到 z 的範圍
[^abc]不是 a、b、c

量詞

語法說明
*0 次以上(貪婪)
+1 次以上(貪婪)
?0 或 1 次
{n}剛好 n 次
{n,m}n 到 m 次
*? / +?惰性版本

錨點與斷言

語法說明
^行首 / 字串開頭
$行尾 / 字串結尾
\b單字邊界
(?=...)Positive Lookahead
(?!...)Negative Lookahead
(?<=...)Positive Lookbehind
(?<!...)Negative Lookbehind

群組與參照

語法說明
(...)捕獲群組
(?:...)非捕獲群組
(?<name>...)命名群組
\1反向參照第 1 個群組
a|ba 或 b

發佈留言