跳至內容
Toova
所有工具

每位開發者都該知道的 10 個正規表示式技巧

Toova

正規表示式(regex)屬於那種一開始讓人覺得難以理解、突然之間又豁然開朗的工具之一。一旦理解之後,你會發現它無所不在:輸入驗證、日誌解析、搜尋取代流程、URL 路由。但大多數開發者只用到少數幾項功能 — 字元類別、量詞、錨點 — 而規格中其他部分則完全沒碰過。

本指南介紹十項超越基礎的正規表示式功能。每一項都能解決簡單模式無法乾淨處理的真實問題。所有範例皆使用 JavaScript 語法,也適用於任何相容於 ECMAScript 的環境。

你可以使用 Toova 正規表示式測試工具來測試本文中的每個模式,完全不需要撰寫任何設定程式碼。

1. 預查:匹配但不消耗字元

預查會斷言某個模式必須(或必須不)跟在目前位置之後,但不會讓匹配引擎越過這些字元。匹配的文字不會包含預查所檢查的內容。

正向預查語法:(?=...)

// 正向預查:僅當後方跟著 "bar" 時才匹配 "foo"
const re1 = /foo(?=bar)/;
re1.test('foobar'); // true
re1.test('foobaz'); // false

負向預查語法:(?!...)

// 負向預查:匹配後方「沒有」跟著 "bar" 的 "foo"
const re2 = /foo(?!bar)/;
re2.test('foobaz'); // true
re2.test('foobar'); // false

一個實際用法:只在價格數字後面跟著貨幣符號時匹配它,但不將符號納入捕獲的值中。或使用 (?=.*\d) 驗證密碼至少包含一個數字,而不必指定數字必須出現的位置。

預查是零寬度的 — 不消耗任何字元。你可以在同一位置堆疊多個預查,同時強制多個獨立條件成立。

2. 後查:檢查之前的內容

後查是預查的鏡像版本:它檢查位於目前位置之前的文字,但不將其納入匹配結果。

正向後查語法:(?<=...)

// 正向後查:僅當前方為 "foo" 時才匹配 "bar"
const re3 = /(?<=foo)bar/;
re3.test('foobar'); // true
re3.test('bazbar'); // false

負向後查語法:(?<!...)

// 負向後查:匹配前方「沒有」 "foo" 的 "bar"
const re4 = /(?<!foo)bar/;
re4.test('bazbar'); // true
re4.test('foobar'); // false

後查在 ECMAScript 2018 中正式加入,所有現代瀏覽器與 Node.js 10+ 皆支援。一個常見用例:從 name=Alice 這類鍵值對中擷取值的部分,匹配 name= 之後的所有內容,但不將鍵納入匹配。

注意:與預查不同,JavaScript 中的後查表達式不能包含可變長度的模式 — 後查表達式必須具有固定或有上限的最大長度。

3. 具名捕獲群組:自我說明的模式

標準捕獲群組透過數字編號引用:$1$2等。當你新增或移除群組時,下游所有引用都會失效。具名群組透過為每個群組附加一個標籤來解決此問題。

const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-05-10'.match(dateRe);
console.log(m.groups.year);  // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day);   // "10"

具名群組也可在取代字串中透過 $<name> 使用:

// 在模式中使用具名群組進行反向引用
const quoteRe = /(?<q>['"]).*?\k<q>/;
quoteRe.test('"hello"'); // true
quoteRe.test('"hello''); // false

群組名稱必須是合法的 JavaScript 識別字。使用能反映群組所捕獲內容的描述性名稱 — yearportprotocol — 你的模式就幾乎能自我說明。如上所示,你也可以在模式內使用 \k<name> 來反向引用具名群組。

4. 非貪婪量詞:匹配最少字元

預設情況下,量詞(*+?)是貪婪的 — 它們會盡可能多地匹配字元。在量詞後加上 ? 可使其變為非貪婪(也稱為惰性或謙讓),盡可能少地匹配字元。

const html = '<a>click</a>';

// 貪婪(預設) — 匹配最長的字串
/<.+>/.exec(html)?.[0]; // '<a>click</a>'

// 非貪婪 — 匹配最短的字串
/<.+?>/.exec(html)?.[0]; // '<a>'

這在解析 HTML、XML 或任何同一分隔符可能多次出現的格式時很重要。貪婪版本會從第一個開頭吞噬到整個字串中最後一個結尾標籤。非貪婪版本則會在第一個合法的結尾匹配處停止。

相同的規則適用於 +?(一或多個,惰性)與 ??(零或一個,惰性)。非貪婪量詞並不會改變能匹配的內容 — 它改變的是在多個有效匹配存在時選擇哪一個。

5. 避免災難性回溯

回溯是正規表示式引擎從失敗的匹配嘗試中恢復的方式 — 它們會嘗試模式中不同的路徑。多數情況下這是隱形且快速的。但某些模式可能會讓引擎探索呈指數成長的路徑數量,即使是中等大小的輸入字串,也能讓 Node.js 行程陷入停滯。

經典的危險模式是巢狀量詞,例如 (a+)+ 套用於 aaaaab 這樣的字串。引擎會嘗試將 a 字元在內外群組之間分配的所有可能方式,最後才得出無法匹配的結論。

原子群組((?>...))透過告知引擎在群組匹配後不要回溯來避免此問題。JavaScript 原生不支援原子群組,但你可以使用預查模擬佔有式行為:

// 沒有原子群組 — 引擎會回溯進入 (\d+)
// 有原子群組 — 一旦 (\d+) 匹配成功,就不允許回溯
// JavaScript 原生不支援原子群組,
// 但可以使用預查技巧來模擬:
const re5 = /(?=(\d+))\1(?!\d)/; // 模擬佔有式 \d++

較安全的經驗法則:除非有特定理由,否則避免將量詞直接巢狀於其他量詞中。重寫模式以更精確地表達要匹配的內容。你也可以使用 文字差異比對工具,在重構時並排比較兩個等價模式的輸出。

6. 字元類別集合運算(Unicode v 旗標)

ECMAScript 2024 引入了 v 旗標,可在字元類別中啟用集合運算。這讓你能以乾淨的類別定義表達「所有非母音字母」或「同時為大寫字母與 ASCII 字元」,而不必使用笨重的選擇結構。

// POSIX 字元類別減法在 JS 中不可用,
// 但 Unicode 集合模式(`v` 旗標)新增了集合運算:
const lettersNoVowels = /[a-z--[aeiou]]/v;
lettersNoVowels.test('b'); // true
lettersNoVowels.test('e'); // false

v 旗標在字元類別內支援三種運算:

  • 差集[A--B] — 在 A 中但不在 B 中的字元
  • 交集[A&&B] — 同時在 A 與 B 中的字元
  • 聯集[AB] — 在 A 或 B 中的字元(與標準字元類別相同)

Node.js 20+ 與所有常綠瀏覽器都支援 v 旗標。它是 u 旗標的超集 — 不要同時使用兩者;需要其功能時請單獨使用 v

7. 單字邊界:完整單字匹配

錨點 \b 匹配單字字元(\w)與非單字字元之間的位置。它不消耗字元 — 僅斷言該位置。其反向版本 \B 則匹配任何「不是」單字邊界的位置。

const sentence = 'cat concatenate';

// 沒有 \b — "cat" 也會在 "concatenate" 內部被找到
/cat/g.exec(sentence); // 同時匹配 "cat" 中的 "cat" 與 "concatenate" 中的 "cat"

// 有 \b — 只匹配完整的 "cat" 單字
/\bcat\b/g.exec(sentence); // 只匹配獨立存在的 "cat"
// \B 為其反向:匹配單字內部,而非邊界
/\Bcat\B/.test('concatenate'); // true — "cat" 位於單字內部

在程式碼或文字中搜尋識別字時,單字邊界至關重要。若沒有它們,搜尋名為 id 的變數時也會匹配到 indexOfinvalidgrid。使用 \bterm\b 將匹配限制為獨立出現的詞。

一個重要的注意事項:\b 使用 JavaScript 對單字字元的定義([a-zA-Z0-9_])。重音字元與非拉丁字母會被視為非單字字元。對於支援 Unicode 的單字邊界,請將 v 旗標與 Unicode 屬性類別結合使用。

8. 多行模式:每行的錨點

預設情況下,^ 只匹配整個字串的開頭,而 $ 只匹配整個字串的結尾。m(多行)旗標會改變此行為:^ 匹配每一行的開頭,$ 匹配每一行的結尾。

const text = 'line one\nline two\nline three';

// 沒有 m 旗標 — ^ 只匹配整個字串的開頭
/^line/.test(text); // true(僅第一行)

// 有 m 旗標 — ^ 匹配「每一行」的開頭
const matches = text.match(/^line/gm);
console.log(matches); // ['line', 'line', 'line']

當處理多行文字(例如日誌檔、設定檔或程式碼)時,這項功能不可或缺。常見用途包括擷取以關鍵字開頭的行、取代行尾標記,或驗證區塊中每一行皆符合某個模式。

不要將 m 旗標與 s 旗標(dotAll)混淆。s 旗標讓 . 也能匹配換行字元。m 旗標完全不影響 . — 它只改變 ^$ 的行為。

9. Unicode 屬性轉義:跨語言字元匹配

u 旗標啟用 Unicode 屬性轉義,讓你能基於 Unicode 類別、書寫系統或其他屬性匹配字元。這是跨所有人類書寫系統匹配字母、數字或標點符號的正確方式 — 而非只限於 ASCII。

// u 旗標啟用 Unicode 屬性轉義
const letters = /\p{L}+/u;
letters.test('Héllo');   // true
letters.test('你好');    // true
letters.test('12345');   // false

// 跨所有書寫系統匹配大寫字母
const upper = /\p{Lu}+/u;
upper.test('ABC');  // true
upper.test('abc');  // false
// 匹配 emoji(Unicode 一般類別:Symbol, Other)
const emoji = /\p{So}/u;
emoji.test('🚀'); // true

最常用的 Unicode 屬性包括:

  • \p{L} — 任何字母(所有書寫系統)
  • \p{Lu} — 大寫字母
  • \p{Ll} — 小寫字母
  • \p{N} — 任何數字
  • \p{Nd} — 十進位數字
  • \p{P} — 標點符號
  • \p{Script=Latin} — 拉丁書寫系統字元
  • \p{Emoji} — emoji 字元

使用 \P{...}(大寫 P)來反向匹配 — 匹配「不」具備指定屬性的所有字元。所有支援的 Unicode 屬性及其值的完整清單,維護於 MDN 文件中。

10. 透過字串組裝實現詳盡模式

許多正規表示式方言(Python、Ruby、.NET、PCRE)支援詳盡或擴充模式(x 旗標),允許在模式內使用空白與註解。JavaScript 沒有這個旗標 — x 旗標在 ECMAScript 中不合法。

標準的替代方案是由具名字串常數組合模式,並透過 new RegExp() 結合:

// JavaScript 沒有原生的 x 旗標,
// 但可以將可讀的模式建構為字串常數:
const YEAR  = '(?<year>\\d{4})';
const SEP   = '-';
const MONTH = '(?<month>\\d{2})';
const DAY   = '(?<day>\\d{2})';
const datePattern = new RegExp(YEAR + SEP + MONTH + SEP + DAY);

每個常數描述其匹配的內容,最終模式讀起來就像一句句子。此方法也讓你能在程式碼基礎中跨多個正規表示式定義組合共享子模式,並對每個元件獨立進行單元測試。

對於較不複雜的模式,將整個正規表示式寫在一行,並在附近的區塊註解中加入行內說明,通常就已足夠。目標是確保下一位開發者(包括未來的自己)能夠不必透過解碼器就能理解該模式。

整合運用

這十項技巧涵蓋了「為什麼我的正規表示式在邊界情況下失敗?」這一表面區域的大部分。預查與後查讓你能斷言上下文而不消耗它。具名群組讓模式在重構時仍保有可讀性。非貪婪量詞防止意外過度匹配。Unicode 屬性轉義處理超越 ASCII 的輸入。

建立熟練度的最佳方式是用真實資料實驗真實的模式。使用 正規表示式測試工具快速迭代。當你的模式產生需要比對或清理的輸出時,文字差異比對工具能準確顯示兩次執行之間有何變化。如果你的正規表示式正在解析 JSON,JSON 格式化工具讓你能在瀏覽器內檢查結構化結果。

若需 JavaScript 正規表示式所有語法與旗標的完整參考,MDN 正規表示式速查表是值得收藏的單頁資源。若需互動式模式除錯與完整匹配視覺化,regex101.com支援 JavaScript 模式,並內建對模式中每個元件的解說。