什麼是正則表達式?
正則表達式(Regular Expression,簡稱 Regex 或 RegExp)是一種用來描述字串模式的語法。你可以把它想像成一種「搜尋語言」——用一組特殊符號來定義你要找的文字模式,然後讓程式去比對、搜尋或取代符合該模式的文字。
正則表達式的起源可以追溯到 1950 年代數學家 Stephen Kleene 的正規語言理論。1968 年,Ken Thompson 將正則表達式引入 Unix 的文字編輯器 ed,從此成為程式設計中不可或缺的工具。如今,幾乎所有主流程式語言(JavaScript、Python、Java、Go、Rust)、文字編輯器(VS Code、Vim、Sublime Text)、命令列工具(grep、sed、awk)都內建了正則表達式引擎。
正則表達式的應用場景
正則表達式的應用場景非常廣泛,涵蓋前端、後端、DevOps 各領域:
- 表單驗證:驗證 Email 地址、電話號碼、身分證字號、信用卡號、統一編號等格式
- 搜尋與取代:在程式碼編輯器中批量修改變數名稱、修正格式問題
- 資料擷取:從網頁 HTML、日誌檔案、CSV 文件中提取特定格式的資料(如 URL、IP 位址、日期)
- 日誌分析:從伺服器日誌中篩選特定錯誤訊息、統計 HTTP 狀態碼
- URL 路由:Web 框架(如 Express.js、Django)中的 URL 模式匹配
- 資料清洗:移除多餘空白、統一日期格式、標準化電話號碼格式
- 語法高亮:程式碼編輯器使用正則表達式來辨識關鍵字、字串、註解等語法元素
基本語法
字面匹配
最簡單的正則表達式就是字面文字本身。引擎會在目標字串中搜尋完全匹配的子字串:
模式:hello
輸入:"hello world" → 匹配 "hello"
輸入:"say hello to me" → 匹配 "hello"
輸入:"Hello World" → 不匹配(預設區分大小寫)
元字元(Meta Characters)
以下字元在正則表達式中有特殊意義,它們是正則表達式的「語法關鍵字」:
| 元字元 | 意義 | 範例 | 匹配結果 |
|---|---|---|---|
. |
任意單一字元(換行除外) | h.t |
hat, hot, hit, h9t |
^ |
字串開頭 | ^Hello |
只匹配開頭的 Hello |
$ |
字串結尾 | world$ |
只匹配結尾的 world |
\ |
跳脫字元 | \. |
匹配字面上的句點 |
\| |
或(OR) | cat\|dog |
匹配 cat 或 dog |
() |
分組 | (ab)+ |
匹配 ab, abab, ababab |
[] |
字元類別 | [aeiou] |
匹配任何母音字母 |
如果你需要匹配這些元字元的字面值,必須用反斜線 \ 跳脫。例如匹配字面上的句點用 \.,匹配字面上的美元符號用 \$。
字元類別(Character Classes)
用方括號 [] 定義一組可匹配的字元。字元類別只匹配一個字元的位置:
[abc] 匹配 a、b 或 c 中的任一個
[a-z] 匹配任何小寫字母(a 到 z 的範圍)
[A-Z] 匹配任何大寫字母
[0-9] 匹配任何數字
[a-zA-Z] 匹配任何英文字母(大寫或小寫)
[a-zA-Z0-9] 匹配任何英數字元
[^abc] 匹配除了 a、b、c 以外的任何字元(^ 在 [] 內表示否定)
[.+*] 在 [] 內,特殊字元失去特殊意義,這裡匹配 . + * 的字面值
預定義字元類別
為了方便使用,正則表達式提供了常用字元類別的簡寫:
| 簡寫 | 等價於 | 說明 | 助記 |
|---|---|---|---|
\d |
[0-9] |
數字(digit) | d = digit |
\D |
[^0-9] |
非數字 | 大寫 = 否定 |
\w |
[a-zA-Z0-9_] |
文字字元(word) | w = word |
\W |
[^a-zA-Z0-9_] |
非文字字元 | 大寫 = 否定 |
\s |
[\t\n\r\f\v ] |
空白字元(space) | s = space |
\S |
[^\t\n\r\f\v ] |
非空白字元 | 大寫 = 否定 |
\b |
- | 單字邊界 | b = boundary |
單字邊界 \b 非常實用,它匹配的是「位置」而非字元。例如 \bcat\b 會匹配獨立的 "cat",但不會匹配 "category" 中的 "cat"。
量詞(Quantifiers)
量詞用來指定前面元素出現的次數,是正則表達式中最常用的語法之一:
| 量詞 | 意義 | 範例 | 匹配 | 不匹配 |
|---|---|---|---|---|
* |
0 次以上 | ab*c |
ac, abc, abbc |
- |
+ |
1 次以上 | ab+c |
abc, abbc |
ac |
? |
0 或 1 次 | colou?r |
color, colour |
colouur |
{n} |
恰好 n 次 | \d{4} |
2026 |
123, 12345 |
{n,} |
至少 n 次 | \d{2,} |
12, 123, 1234 |
1 |
{n,m} |
n 到 m 次 | \d{2,4} |
12, 123, 1234 |
1, 12345 |
貪婪匹配與懶惰匹配
預設情況下,量詞是貪婪的——會盡可能多匹配字元。在量詞後加上 ? 可變成懶惰模式——盡可能少匹配字元。理解這兩者的差異對於正確撰寫正則表達式至關重要:
字串:"<b>hello</b> <b>world</b>"
貪婪:<b>.*</b> → 匹配整段 "<b>hello</b> <b>world</b>"
(.* 盡可能多匹配,直到最後一個 </b>)
懶惰:<b>.*?</b> → 匹配第一段 "<b>hello</b>"
(.*? 盡可能少匹配,遇到第一個 </b> 就停止)
在處理 HTML 或巢狀結構時,懶惰匹配通常是你需要的。一個更好的做法是使用否定字元類別:<b>[^<]*</b> 比 <b>.*?</b> 效能更好且更安全。
群組與擷取
擷取群組(Capturing Groups)
用圓括號 () 將部分模式分組,可同時進行分組和擷取匹配的子字串。每個擷取群組會被自動編號(從 1 開始):
模式:(\d{4})-(\d{2})-(\d{2})
輸入:"會議日期:2026-03-28"
完整匹配:2026-03-28
群組 1:2026(年)
群組 2:03(月)
群組 3:28(日)
在程式語言中,你可以透過群組編號來存取擷取的內容:
const match = "2026-03-28".match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // "2026"
console.log(match[2]); // "03"
console.log(match[3]); // "28"
命名擷取群組
為了提升可讀性,現代正則表達式支援為群組命名:
// JavaScript(ES2018+)
const match = "2026-03-28".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(match.groups.year); // "2026"
console.log(match.groups.month); // "03"
console.log(match.groups.day); // "28"
# Python
import re
match = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-03-28')
print(match.group('year')) # '2026'
非擷取群組
如果只需要分組(用於量詞或 OR 邏輯)但不需要擷取結果,使用 (?:...) 可以避免不必要的擷取,略微提升效能:
# 擷取群組 — 會記錄匹配內容
(https?|ftp)://
# 非擷取群組 — 只分組,不記錄
(?:https?|ftp)://
反向參考(Backreference)
用 \1、\2 參考前面擷取的群組內容,這在尋找重複模式時特別有用:
模式:(\w+)\s+\1
匹配:重複的單字,如 "the the"、"is is"
不匹配:"the is"(因為 \1 必須與群組 1 的內容完全相同)
反向參考的實用場景:
- 找出文章中重複的單字(校對)
- 匹配成對的 HTML 標籤(如 <(\w+)>.*?</\1>)
- 找出重複的字串片段
斷言(Lookaround)
斷言是正則表達式中的進階技巧,它們檢查某個位置前後是否符合條件,但不消耗字元(不把匹配的字元包含在結果中):
| 語法 | 名稱 | 說明 | 範例 |
|---|---|---|---|
(?=...) |
正向前瞻 | 後面必須接著... | \d+(?=元) 匹配 "500元" 中的 "500" |
(?!...) |
負向前瞻 | 後面不能接著... | \d+(?!元) 匹配 "500個" 中的 "500" |
(?<=...) |
正向後顧 | 前面必須是... | (?<=\$)\d+ 匹配 "$100" 中的 "100" |
(?<!...) |
負向後顧 | 前面不能是... | (?<!\$)\d+ 匹配 "200" 但不匹配 "$100" 中的 "100" |
斷言的實用範例
# 找出後面有 "元" 的數字(價格擷取)
\d+(?=元)
"價格 500元 和 200元" → 匹配 "500" 和 "200"
# 找出前面沒有 $ 的數字
(?<!\$)\d+
"$100 和 200" → 匹配 "200"
# 密碼驗證:必須同時包含大寫、小寫和數字
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
利用多個正向前瞻確保密碼包含所有要求的字元類型
# 不以 test 開頭的 Email
^(?!test)[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
實際應用案例
Email 驗證
^[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 個字母),到字串結尾
注意:這個簡化版本適用於大多數場景,但無法覆蓋所有合法 Email 地址。完整的 Email 驗證規範(RFC 5322)極為複雜,實務上建議搭配後端驗證信函確認。
台灣手機號碼
^09\d{8}$
匹配 09 開頭的 10 位數字,如 0912345678。
帶格式(允許中間有連字號或空格)的版本:
^09\d{2}[-\s]?\d{3}[-\s]?\d{3}$
匹配 0912345678、0912-345-678、0912 345 678。
台灣身分證字號
^[A-Z][12]\d{8}$
- 第一位:大寫英文字母(縣市代碼,A-Z 共 26 個)
- 第二位:1(男性)或 2(女性)
- 後八位:數字(最後一位是檢查碼)
新版居留證號碼:
^[A-Z][89A-D]\d{8}$
URL 匹配
https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[^\s]*)?
匹配 http:// 或 https:// 開頭的 URL,包含可選的路徑部分。
統一編號驗證(8 位數字)
^\d{8}$
基本格式檢查(完整驗證需要搭配程式邏輯計算檢查碼)。
IP 位址(IPv4)
^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$
這個版本會驗證每組數字是否在 0-255 之間,比簡化版 ^(\d{1,3}\.){3}\d{1,3}$ 更嚴謹。
HTML 標籤擷取
<(\w+)[^>]*>(.*?)</\1>
擷取成對的 HTML 標籤及其內容。使用反向參考 \1 確保開始和結束標籤匹配。
注意:用正則表達式解析 HTML 只適合簡單場景。複雜的 HTML 解析應使用專門的 DOM 解析器(如 Python 的 BeautifulSoup、JavaScript 的 DOMParser)。
各語言的 Regex 用法
JavaScript
// 字面量建立(推薦用於已知的靜態模式)
const regex = /^09\d{8}$/;
// 建構函式建立(適合動態模式)
const pattern = new RegExp(`^${prefix}\\d{8}$`);
// 測試是否匹配
regex.test('0912345678'); // true
regex.test('0812345678'); // false
// 搜尋與擷取
const str = '日期:2026-03-28';
const match = str.match(/(\d{4})-(\d{2})-(\d{2})/);
// match[0] = "2026-03-28"(完整匹配)
// match[1] = "2026", match[2] = "03", match[3] = "28"
// 全域搜尋所有匹配
const text = '電話 0912345678 和 0998765432';
const phones = text.match(/09\d{8}/g);
// ["0912345678", "0998765432"]
// 全域取代
const result = str.replace(/\d+/g, '**');
// "日期:**-**-**"
// matchAll(ES2020,適合需要擷取群組的全域搜尋)
const dates = '2026-03-28 和 2026-04-15';
for (const m of dates.matchAll(/(\d{4})-(\d{2})-(\d{2})/g)) {
console.log(m[1], m[2], m[3]);
}
Python
import re
# 匹配測試
pattern = r'^09\d{8}$'
if re.match(pattern, '0912345678'):
print('有效的手機號碼')
# 搜尋所有匹配
text = 'Email: alice@example.com 和 bob@test.com'
emails = re.findall(r'[\w.+-]+@[\w.-]+\.\w{2,}', text)
# ['alice@example.com', 'bob@test.com']
# 擷取群組
match = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-03-28')
print(match.group('year')) # '2026'
print(match.group('month')) # '03'
# 取代
result = re.sub(r'\d+', '***', '電話 0912345678')
# '電話 ***'
# 編譯正則表達式(重複使用時提升效能)
phone_pattern = re.compile(r'^09\d{8}$')
phone_pattern.match('0912345678')
效能優化與常見陷阱
陷阱 1:災難性回溯(Catastrophic Backtracking)
某些正則表達式模式會導致引擎進入指數級回溯,嚴重時可以讓程式當機或 DoS 攻擊(ReDoS):
# 危險模式(巢狀量詞)
(a+)+b
# 輸入 "aaaaaaaaaaaaaaaaac" 時,引擎會嘗試 2^n 種分組方式
# n = 17 時大約 131,072 次嘗試
避免方式:不要使用巢狀量詞(如 (a+)+、(a*)*、(a|b)++回溯)。使用原子群組(Atomic Groups)或佔有量詞(Possessive Quantifiers)來防止回溯。
陷阱 2:忘記跳脫特殊字元
. 在 regex 中匹配任意字元,要匹配字面句點需用 \.:
# 錯誤:匹配 "a.b" 也匹配 "axb"
a.b
# 正確:只匹配 "a.b"
a\.b
陷阱 3:忘記錨定
沒有 ^ 和 $ 時,模式可能只匹配字串的一部分,導致驗證失效:
# 錯誤:/\d{4}/ 會匹配 "abc12345" 中的 "1234"
# 正確:/^\d{4}$/ 只匹配恰好 4 位數字的字串
陷阱 4:貪婪匹配導致過度匹配
.* 會盡可能多匹配,常常超出你的預期。優先使用具體的字元類別:
# 不好:可能匹配過多內容
".*"
# 較好:匹配非引號字元
"[^"]*"
效能最佳實踐
- 具體優於模糊:
[a-z]+比.+效能好,因為引擎不需要嘗試過多可能性 - 錨定開頭和結尾:
^和$能讓引擎快速判斷不匹配的字串 - 編譯重複使用的模式:Python 的
re.compile()、JavaScript 的字面量語法 - 避免不必要的擷取群組:使用
(?:...)替代不需要擷取的(...) - 使用字元類別否定:
[^<]*比.*?效能更好
常見問題(FAQ)
Q1:正則表達式在不同語言間有什麼差異?
大部分基本語法是通用的(字元類別、量詞、群組等)。主要差異在於進階功能:JavaScript 直到 ES2018 才支援命名擷取群組和後顧斷言;Python 使用 (?P<name>...) 而 JavaScript 使用 (?<name>...);某些語言支援原子群組 (?>...) 和佔有量詞 ++ 而 JavaScript 不支援。建議使用 Super Tools 的正則表達式測試工具在目標語言的引擎上測試。
Q2:如何學習正則表達式?
建議的學習路線:(1)先掌握基本語法:字面匹配、字元類別、量詞;(2)練習簡單的驗證模式:數字、Email、電話號碼;(3)學習群組和擷取;(4)最後學習斷言和進階技巧。最重要的是動手練習,使用線上測試工具(如 Super Tools 的 Regex 測試工具)即時看到匹配結果。
Q3:正則表達式能解析 HTML 嗎?
簡單的 HTML 擷取(如提取所有 <a> 標籤的 href 屬性)可以用正則表達式。但複雜的 HTML 解析(巢狀標籤、不規則格式、自閉合標籤)應該使用專門的 HTML 解析器。HTML 不是正規語言(Regular Language),正則表達式在理論上就無法完美解析它。
Q4:如何處理 Unicode 和中文字元?
在 JavaScript 中使用 u 旗標啟用 Unicode 模式:/[\u4e00-\u9fff]+/u 匹配中文字元。Python 3 預設支援 Unicode。更現代的做法是使用 Unicode 屬性:/\p{Script=Han}+/u(JavaScript ES2018+)可以匹配所有 CJK 漢字,比硬編碼範圍更準確。
Q5:正則表達式和通配符(Wildcard)有什麼不同?
通配符是簡化版的模式匹配,通常用在檔案系統中:* 匹配任意字串、? 匹配單一字元。正則表達式的 .* 相當於通配符的 *,正則表達式的 . 相當於通配符的 ?。正則表達式功能遠比通配符強大,支援字元類別、量詞、群組、斷言等進階功能。
動手練習
理論學了再多,不如親手試試。Super Tools 提供了免費的正則表達式測試工具,支援即時比對、群組高亮和語法說明,是學習和測試正則表達式的最佳工具。你可以在上面嘗試本文中的所有範例,觀察匹配結果的即時變化。
如果你正在開發需要資料驗證的表單,也推薦搭配 JSON 格式化工具 處理 API 回應的資料格式,以及使用密碼強度檢測工具來測試你的密碼驗證正則表達式效果。