雜記 › 技術筆記 › 正規表達式看不懂也難除錯?線上即時高亮匹配幫你上手

正規表達式看不懂也難除錯?線上即時高亮匹配幫你上手

我用正規表示式踩過最貴的一次坑,是一段看起來完全正確的手機號碼驗證:^1[3-9]\d{9}$。單條測試沒問題,拿去清洗一份 2000 多行的使用者名單時,只擷取出 1 條。當時懷疑量詞寫錯、懷疑行尾有看不見的字元,甚至懷疑資料本身有問題,最後才反應過來:多行文字裡 $ 預設只匹配整個字串的末尾,也就是最後一行的結尾,前面 1999 行全被「錨點」卡掉了。一個旗標的事,讓我對整個下午的清洗結果都失去了信任。正規表示式的難不在於符號多,而在於你以為它匹配的和它實際匹配的之間,隔著幾個看不見的路徑規則。

一、先把最常用的幾個符號釘死

寫法含義易錯點
. \d \w \s除換行外任意一個字元 / 數字 / 字母數字底線 / 空白. 不轉義時會匹配句號之外的任何東西,IP 裡的點必須寫 \.
* + ?0 次以上 / 1 次以上 / 0 或 1 次? 跟在量詞後是「非貪婪」,跟在括號後是「命名/斷言」,含義完全不同
{n} {n,m}恰好 n 次 / n 到 m 次{0,} 等價 *,但可讀性更好
^ $字串開頭 / 結尾(預設不是「行」首尾)多行文字忘了 m 旗標就會全滅,這是我那次故障的根因
[abc] [^abc]字元集 / 排除集方括號裡 . * + 失去特殊含義,- 位置不對會變成範圍
(a|b) (?:a|b)捕獲分組 / 非捕獲分組括號只是「不想被 | 拆開」時該用 (?:),白佔一個分組序號

二、兩個例子,看旗標怎麼改變結果

手機號碼(驗證整段輸入):^1[3-9]\d{9}$
手機號碼(從多行日誌裡擷取):(?m)^1[3-9]\d{9}$  或  1[3-9]\d{9}(?!\d)
IPv4:\b(?:\d{1,3}\.){3}\d{1,3}\b

第一行只適合表單驗證:輸入框裡就是這一個值,^ 和 $ 恰好夾住它。第二行才是我清洗名單該寫的:m 讓 ^ $ 變成行首行尾;或者乾脆不錨定,用 (?!\d) 保證 11 位之後不再緊跟數字,避免從一串 19 位的訂單號裡「截」出一個假手機號碼。第三行如果把 \. 寫成 .,那麼 123456789 也能被它匹配走——這是從日誌裡擷取 IP 時最常見的誤報來源。

旗標裡 g(全域)決定「找到第一個就停」還是「遍歷全部」,i 忽略大小寫,m 多行,s 讓 . 連換行也能匹配。這四個是我在實際工作裡唯一會反覆切換的。

三、貪婪與非貪婪:為什麼它把整段 HTML 吞了

量詞預設是貪婪的,會盡量往後吃。<a href=".*"> 對 <a href="/home">text</a> <a href="/about"> 這種兩個標籤的行,會從第一個 " 一路吃到最後一個 ",中間的 </a> 全被包含進去。改成 <a href=".*?"> 才是「就近閉合」。同理,抓 HTML 時更穩的做法是排除掉結束符本身:<a href="[^"]*">,用字元集表達「直到遇到引號為止」,比依賴非貪婪更不容易出錯。

我在調這類表達式時的流程是:寫完立刻把正規表示式和一段真實樣本一起貼進正規表示式測試,靠即時高亮看邊界到底落在哪。眼睛在腦子裡推演 .*? 的走位是不可靠的,高亮框一出來就有結論;分組面板還能列出每個捕獲組的序號和內容位置,寫 $1、$2 替換時不用再數括號。

四、三類讓正規表示式變成事故的時刻

  • 災難性回溯:形如 (a+)+b、(\d+[-\s]*)*@ 的嵌套量詞,遇到一段「幾乎符合但最終不匹配」的文字會指數級嘗試。我們線上有一種情況,介面 CPU 被單條輸入打滿,排查到一條清洗規則裡的 ([\w\.-]+)+@,換成 [\w.-]+@ 之後同樣的輸入耗時從 8 秒多降到幾十毫秒。
  • 沒限定結尾的驗證:^[\w.-]+@[\w-]+\.\w+$ 會放過 a@b.c 這種不存在的網域。驗證電子郵件時我傾向於把「合法格式」和「真實可投遞」當成兩件事,正規表示式只負責前者。
  • 批次場景別硬寫:如果只是要從一段文字裡把手機號碼、電子郵件、URL、IP 這類固定類型的東西撈出來,用文字資訊擷取比現寫正規表示式省事;要做批次替換時用批次尋找取代,能同時掛多條規則並看到每條的替換次數。

五、常見疑問

Q:為什麼我的正規表示式在 JS 裡對、在後端 Java 或 Python 裡不對?

多半是方言差異。JS 和 Java 預設的 \d 只認 ASCII 數字,Python 3 預設把它擴展到了全形數字和其他文字的數字,清洗混排資料時結果就會不一樣;(?<=) 這類後行斷言在較舊的 Safari 上會直接拋語法錯誤;Python 的 re.match 只從字串開頭試,re.search 才全串掃描,而 JS 的 test/exec 不加 ^ 時不做錨定。跨語言遷移時至少跑三條樣本:正常值、邊界值、一段更長的髒資料。

Q:捕獲分組和非捕獲分組 (?:) 有什麼區別?

括號會佔一個序號,替換時 $1 按順序引用;只需要分組作用、不需要引用內容時寫成 (?:...),能少踩「序號錯位」的坑,效能也略好。

Q:怎麼知道自己在匹配到的內容裡第幾個分組?

數左括號,從左到右 1 起。帶嵌套時極易數錯,所以我一般用測試工具的分組詳情直接看每個組的實際內容,而不是靠心算。

Q:正規表示式能驗證密碼強度、金額格式嗎?

能,但別指望一條表達式寫完。金額類最容易漏的是小數位數與千分位組合,寫兩條互相獨立的斷言比一條超長表達式更好維護,也更容易定位失敗原因。

Q:為什麼看起來一樣的文字匹配不上?

常見是看不見的字元:全形空格、零寬空格、Windows 換行的 \r、以及從網頁複製帶來的軟連字號。先看清字元再懷疑語法。

那次名單清洗最後拿回來的資料是對的,但我把「錨點預設不作用於每一行」這條寫進了自己的筆記首頁。正規表示式這種東西,讀懂它匹配了什麼只是及格,能說出它在哪些位置停下來、為什麼停下來,才是能被信任的表達式。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22