什麼是正規表達式?為什麼用讀的看不出問題
正規表達式(Regular Expression,簡稱 Regex 或 RegExp)是「用一串符號描述一組字串模式」的語言。 理論基礎 1956 年由數學家 Stephen Kleene 提出,1968 年起進入 UNIX 工具(grep、sed、awk)成為實用方言,1994 年 Perl 5 的擴充語法(PCRE)影響至今所有主流語言。今天 JavaScript、Python、Java、Go、Ruby 幾乎都支援,語法 90% 互通,剩 10% 差異是各家方言。
本頁把你輸入的內容用 new RegExp(pattern, flags) 編譯,再拿瀏覽器裡原本就有的 JavaScript 引擎去比對你的測試文字,所以這裡收的語法是 ECMAScript,跟你在 Node.js 與瀏覽器主控台得到的一樣。它不是 PCRE、不是 Python 的 re,也不是 POSIX 的 grep。
要「跑」一次而不是用讀的,是因為 regex 失敗得很安靜。一個編得過的 pattern 可以高高興興地比對到錯的範圍、多吃掉一個字元,或是你以為有三十個結果它只找到三個,而程式碼上完全看不出來。光讀 a.*b 不會告訴你貪婪的 .* 一路跑到了那一行最後一個 b。把匹配就地標起來、底下再列出位置與捕獲分組,猜測才會變成看得見的東西。
看結果之前有三件事要先知道。本頁完全照你的旗標跑,所以沒開 g 就只顯示第一個匹配,這在測一條兩端綁死的驗證 pattern 時反而是正確行為。重疊的匹配不可能出現,因為引擎會從上一個匹配的結尾繼續往下掃。還有,有參與匹配但什麼都沒抓到的分組會顯示成空字串而不是被省略,這正是你抓出「那個可有可無的分組其實從來沒中過」的方法。
哪個旗標、哪個分頁、哪種情境
支援的 Regex 旗標:
g(global):找出所有匹配。預設開啟,可以關。i(ignoreCase):大小寫不敏感m(multiline):`^` 與 `$` 對應每行的開頭結尾s(dotAll):`.` 可以匹配換行u(unicode):完整 Unicode 支援(含 Emoji 等 surrogate pair)y(sticky):lastIndex 開始的位置匹配
實務上最常被忘記的是 m:pattern 在一行裡好好的,貼上整份 log 就什麼都抓不到,多半是 ^ 還在對整段文字的開頭。做表單驗證、兩端用 ^ 與 $ 綁死時則要反過來把 g 關掉,只看第一個結果才是對的。
常見使用情境:
- 表單驗證:email、手機、身分證、密碼複雜度檢查(前端即時提示)
- Log / 設定檔解析:從 server log 撈出時間戳、IP、錯誤代碼,用 Regex 切欄位
- 大量取代:VS Code、Sublime 的 Find & Replace 開 Regex 模式,一次改幾百個檔案
- 資料清理:CSV 欄位裡的全形空白、零寬字元、HTML entity,一次清掉
- 網頁爬蟲:HTML / 純文字裡抓 URL、email、電話、價格
- 程式碼搜尋:找
console.log殘留、TODO 註解、未使用的 import
兩個分頁回答的是不同問題。「測試(match)」回答 pattern 有沒有中、中在哪裡;「替換(replace)」回答改完之後文字長什麼樣,而那才是你要對整個專案跑搜尋取代之前真正該問的問題。上方的「常用範本(台灣)」直接把身分證、統編、手機條碼載具、發票號碼、+886 手機、車牌、民國日期這些本地格式連同會中的範例文字一起填好,從那裡改比從零開始快。套用之後範本列下面會寫出這條只驗什麼;身分證與統編要另外算檢查碼,發票號碼要對獎,說明旁邊會連到站內能做的那支工具。
實戰教學:從一段真文字長出 pattern
- 先把真的文字貼進測試欄,再開始寫。照想像中的輸入寫出來的 pattern,通常在真檔案的第一行就破功。
- 從「能動的最鬆版本」開始。要抓錯誤碼就先打 ERROR,看它亮起來,再長成 \bERROR\b.*,這時你已經看得到它順手撈進了什麼。
- 把 g 打開,一次看到全部;文字有很多行、pattern 又用到 ^ 或 $ 的話,m 也要打開。
- 一次只收緊一個地方。把 . 換成明確的字元組,例如 [^\s] 或 \d;標示範圍跑過頭就把 .* 換成 .*?。
- 把真正要的那一段用括號包起來變成捕獲分組,然後展開「詳細:分組與位置」,確認第一個分組剛好是你要的子字串,不是整行。
- 最後試著弄壞它。貼一行「不該中」的文字,確認結果數掉到零。沒被餵過壞輸入的驗證 pattern,等於沒測過。
常用 Regex 範例:
- Email:
\b\w+@\w+\.\w+\b - URL:
https?://[^\s]+ - 台灣手機:
09\d{8} - 台灣手機含國際寫法:
(?:\+886[- ]?|0)9\d{2}[- ]?\d{3}[- ]?\d{3}(+886 後面要拿掉開頭的 0,+8860912… 是常見的錯誤寫法,這條不收) - 身分證字號與新式統一證號:
[A-Z][1289]\d{8}(只驗形狀,檢查碼要另外算,可以用身分證產生器與驗證) - 電子發票手機條碼載具:
\/[0-9A-Z.+-]{7}(斜線開頭共 8 碼,後 7 碼只有數字、大寫英文與 + - . 三個符號;格式對了不代表條碼存在) - 統一發票號碼:
[A-Z]{2}-?\d{8}(2 碼英文字軌加 8 碼數字,證明聯上印成 AB-12345678) - IP v4:
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} - 日期 YYYY-MM-DD:
\d{4}-\d{2}-\d{2}
語法速查(最常用 8 個):
.:任意單一字元(換行除外,加sflag 才含換行)\d\w\s:數字 / 英數底線 / 空白;大寫版\D \W \S是反向[abc][a-z]:字元組(其中一個)。前面加^變排除*+?:重複 0+、1+、0 或 1 次{n}{n,}{n,m}:剛好 n 次 / 至少 n 次 / n 到 m 次^$:行首 / 行尾(搭配mflag 更實用)(...):分組 + 捕獲;(?:...)只分組不捕獲|:或;\b:詞邊界(單字外圍)
實戰教學:用捕獲分組做搜尋與取代
- 先在「測試(match)」分頁把匹配弄對。拿一個沒親眼看過標示的 pattern 去取代,就是大量編輯出事的標準劇本。
- 切到「替換(replace)」寫取代字串。編號分組是 $1、$2 依此類推,命名分組是 $<name>,整個匹配是 $&,要一個真的錢字號就寫 $$。
- 大量編輯時 g 要開著。沒開只會改掉第一個,預覽一長就很容易漏看。
- 預覽要從最後面往回讀,不要只看開頭。錯誤常躲在最後一行:某個可有可無的分組沒中,$2 就悄悄變成空字串。
- 把 pattern 與取代字串一起複製進編輯器,在那邊跑同一次編輯。重排日期是很好的第一題:抓 (\d{4})-(\d{2})-(\d{2}),取代成 $3/$2/$1。
取代字串一長,命名分組就值得用。抓 (?<year>\d{4})-(?<month>\d{2}),取代成 $<month>/$<year>,半年後再讀還說得出自己在做什麼,$2/$1 則不會。想把民國年跟西元年互換的規則寫進程式前,先在民國年換算把兩種年份的實際字串對一次,再回來改 pattern。
把 pattern 帶進編輯器、終端機或程式碼
欄位裡放的是「裸的 pattern」,前後沒有斜線。它接下來要去哪裡,決定了你得多跳脫幾層:
- JavaScript 的正規表達式字面值:前後加斜線、旗標放後面,寫成
/\d+/gi,其他什麼都不用改。 - JavaScript 的字串:每個反斜線都要寫兩次,因為字串字面值會先吃掉一層。這是「在測試器會、放進程式碼就不會」最常見的單一原因。
- VS Code 與多數編輯器:打開搜尋,點
.*圖示開啟 regex 模式,pattern 不要帶斜線直接貼。大小寫與全字拼寫是獨立的按鈕而不是旗標,取代欄位的 $1 語法則跟這裡一樣。 - 終端機:pattern 要用單引號包起來,免得 shell 先去展開它,例如
grep -E 'pattern' file.log。命令列工具用的是另一套方言,下一節會講。 - 傳給同事:pattern、旗標與一行範例輸入要一起給。單獨一條 pattern 沒辦法 review,是那行範例讓 bug 變得明顯。
同一條 pattern 換到別的語言與工具
技術細節:本工具用瀏覽器原生 RegExp(V8、SpiderMonkey、JavaScriptCore),效能與 Node.js 一致。支援 ECMAScript 2024 規格:命名分組((?<name>...))、後行斷言((?<=...) (?<!...))、Unicode property escapes(\p{L} 匹配任何語言的字母)。Safari 16.4 以下不支援後行斷言,需注意。
regex 大部分是可以搬的,搬不動的通常集中在命名分組的寫法、Unicode 底下那些簡寫字元組到底代表什麼,以及引擎會不會回溯。先看本頁跑的 JavaScript,字面值與建構子只差在跳脫:
const re = /(?<user>[\w.+-]+)@(?<host>[\w.-]+)/gi
const same = new RegExp('(?<user>[\\w.+-]+)@(?<host>[\\w.-]+)', 'gi')
text.match(re) // 開了 g,回傳所有匹配的陣列
re.exec(text).groups.user // 下一個匹配的命名分組Python 的命名分組拼法不一樣,而且對 str 預設就是 Unicode 感知的,所以那邊的 \d 連其他文字系統的數字都算,除非你明講要 ASCII。標準函式庫也沒有 \p{...} 這種屬性跳脫:
import re
m = re.search(r'(?P<user>[\w.+-]+)@(?P<host>[\w.-]+)', text, re.IGNORECASE)
m.group('user')
re.findall(r'\d+', text, re.ASCII) # 只抓 ASCII 數字Java 的命名分組跟 JavaScript 一樣寫成 (?<name>...),但它的 \d 與 \w 預設只認 ASCII,要開 UNICODE_CHARACTER_CLASS 才會變寬,而且 Java 字串字面值裡每個反斜線一樣要寫兩次。Go 與 Rust 用的是 RE2 引擎,它為了保證線性時間,乾脆完全不支援反向參照與環顧,帶 (?<=...) 的 pattern 在那邊根本編不過,解法是改用分組把前後文一起抓下來。
命令列的預設比上面任何一個都更舊、更窄。純 grep 與 sed 收的是 POSIX 基本表達式,+ 與 ? 不跳脫就只是普通字元;加上 -E 兩者都會切到延伸表達式,那才是大多數人心裡想的那一套。GNU grep 另外有 -P 走 PCRE 方言,最接近你在這裡測到的行為,但 macOS 內建的是 BSD grep,沒有這個選項,要嘛 brew 裝 GNU 版,要嘛改寫成 -E 的形式:
grep -E '09[0-9]{8}' contacts.txt # 延伸表達式,到哪都能跑
grep -P '09\d{8}' contacts.txt # PCRE,只有 GNU grep 有
sed -E 's/([0-9]{4})-([0-9]{2})/\2\/\1/' dates.txt取代的語法也跟著變:sed 與多數 POSIX 工具用 \1,JavaScript 用 $1。資料庫又是另一套。Postgres 有 ~ 與 regexp_match,走它自己的 POSIX 風味;MySQL 8 的 REGEXP_LIKE 底層是 ICU;SQLite 認得 REGEXP 運算子卻沒有內建實作,應用程式沒有自己註冊一個就會直接報錯。要跑在資料庫裡的 pattern,請當成另一個需要單獨測試的東西,不要當成這裡這條的副本。
注意事項與隱私
測試資料含個資怎麼辦。拿真實 log 或客戶資料來試 pattern 是常態,這裡的比對用的是 瀏覽器自己的 RegExp 引擎,文字留在你的分頁裡, 不需要先把個資碼掉再貼。工具會把最近五組 pattern 與測試文字存在這台瀏覽器的 localStorage,讓歷史紀錄能還原,按歷史紀錄裡的清除就會刪掉;整個工具區也標了錄影遮罩,回顯出來的內容不會被收走。
巢狀量詞會把你的分頁卡死。這裡的引擎會回溯,所以像 (a+)+b 這種 pattern 遇到很長又不匹配的字串,可能嘗試指數級數量的組合。那也就是這種 pattern 跑到伺服器上會變成 ReDoS 阻斷服務攻擊的原因。比對是在本頁的主執行緒上跑的,所以分頁會直接凍住而不是跳錯誤訊息。匹配結果列表最多 10,000 筆當作安全閥,意思是太寬鬆的 pattern 配上很長的文字,只會顯示前一萬個。
regex 不是 parser。HTML、JSON、含引號逗號的 CSV 與程式碼,全都是有巢狀或需要看上下文的結構,沒有任何正規表達式能正確處理巢狀。用 pattern 找候選、或做一次你事後會親自檢查的編輯可以,正確性要緊時請用真正的解析器。email 同理:RFC 5322 比任何實用 pattern 都寬鬆得多,所以驗證要寬鬆,最後靠寄一封信確認。
瀏覽器差異很小但真的存在。後行斷言((?<=...) 與 (?<!...))在 Chrome 與 Firefox 已經好幾年,Safari 是 2023 年的 16.4 才跟上,所以在你筆電上編得過的 pattern,在比較舊的 iPhone 可能回報語法錯誤。看到解釋不了的錯誤,先測這一項。
常見問題
greedy 跟 lazy 差在哪?
預設量詞是 greedy(貪婪),會盡量吃多。例如 a.*b 對 axxxbxxxb 會吃到最後一個 b。在量詞後加 ? 變 lazy(懶惰),會盡量吃少:a.*?b 只吃到第一個 b。寫 HTML / 字串解析時常需要 lazy。
為什麼我的 email regex 抓不到某些 email?
因為 email 規範(RFC 5322)超複雜,所謂「完美的 email regex」長到誇張。實務上用簡化版(如 \S+@\S+\.\S+)擋掉 99% 明顯錯誤就夠,剩下的 0.1% 邊角案例(含中文網域、加號別名)讓伺服器寄驗證信去確認最實在。
什麼是「災難性回溯」(ReDoS)?
當 regex 含「巢狀量詞」(如 (a+)+b)遇到不匹配的長字串時,引擎會嘗試指數級數量的組合,變成卡死。寫 regex 時避免同位置兩個 + 或 * 巢狀,可降低風險。Go / Rust 用的 RE2 引擎不支援回溯,所以絕對不會卡。
PCRE / Python / Java 規則一樣嗎?
90% 一樣,10% 不同。本工具基於 JavaScript 引擎,所以結果跟你在 Node.js / 瀏覽器跑相同。要驗證 Python re 或 Java Pattern 的差異,請用對應語言實測(命名分組語法、unicode 預設行為都有微差)。
為什麼 pattern 在單行有效,貼上整份 log 就抓不到?
幾乎都是少了 m 旗標。沒有 m 的時候,^ 只對整段文字的最開頭、$ 只對最結尾,所以一份多行的 log 裡只有第一行的開頭算數。把 m 打開,^ 與 $ 就會對每個換行生效。如果要讓 . 也吃得到換行(例如抓跨行的註解區塊),那是另一個旗標 s。
沒有開 g 旗標,為什麼只出現一個結果?
那是正確行為。沒有 g 時引擎找到第一個就停,本頁也照你的旗標顯示,所以只會標出一個。做表單驗證、pattern 兩端用 ^ 和 $ 綁死時本來就該關掉 g;要從 log 裡撈出全部的網址或錯誤碼才需要打開。另外本頁的匹配結果最多列 10,000 筆,太寬鬆的 pattern 配上很長的文字只會顯示前一萬個。
在這裡會、放進程式碼就壞掉,通常是什麼原因?
八成是反斜線被吃掉一層。本頁欄位裡放的是「裸的 pattern」,寫成 JavaScript 字面值只要前後加斜線、旗標放後面,例如 /\d+/gi,不用改任何東西;但若是用 new RegExp('...') 這種字串寫法,每個反斜線都要寫兩次,因為字串字面值會先吃掉一個。sed 之類的工具則是連取代語法都不同:JavaScript 用 $1,sed 用 \1。
我貼進去的 log 或個資會被上傳嗎?
不會。pattern 與測試文字都是由你自己的瀏覽器編譯與比對,不會送到伺服器、不會被記錄。工具會把最近五組 pattern 與文字存在這台瀏覽器的 localStorage 供歷史紀錄還原,按歷史紀錄的清除就會刪掉。結果區也標了遮罩,錄影分析不會收走回顯的內容。
資料來源
- MDN,正規表達式指南與RegExp 參考(旗標、命名分組、後行斷言、Unicode property escapes、
lastIndex,以及各瀏覽器的支援表)。 - 手機條碼載具的字元規則:綠界科技 ECPay 的手機條碼驗證 API 文件(「第 1 碼為 / ,其餘 7 碼則由數字 0-9、大寫英文 A-Z 與特殊符號 +、-、. 這 39 個字元組成」)。財政部電子發票整合服務平台的說明頁擋自動讀取,沒能直接引用。
- Python Software Foundation,
re模組文件((?P<name>...)的拼法與re.ASCII旗標)。 - Google,RE2 語法(Go 與 Rust 背後的引擎,以及它刻意不做的那些功能)。
- OWASP,Regular expression Denial of Service(巢狀量詞怎麼變成一場事故)。
- IETF,RFC 5322的 email 位址語法。命令列的行為出自 POSIX 對
grep與sed的規格,以及 GNU 與 BSD 的說明頁;資料庫的支援各家文件自有說明,且會隨大版本改變。
更多工具
要比對的文字是設定檔或 API 回應時,先用「JSON 格式化」把結構攤開來看,常常會發現你要的其實是查一個 key 而不是寫 regex。 pattern 要塞進查詢字串時,「URL 編碼轉換」會告訴你伺服器實際收到什麼。log 開頭通常是一個值得先換算的Unix 時間戳。
相關工具
- JSON 格式化
JSON 美化與壓縮雙向轉換,支援 2 / 4 / Tab 縮排,語法錯誤即時標出行號。
- URL 編碼解碼
URL 編碼解碼:中文與特殊字元的百分比編碼雙向轉換,支援 encodeURI 與 encode…
- Base64 編碼解碼
文字 ↔ Base64 雙向編碼解碼,完整支援 UTF-8 中文與特殊字元。
- Markdown 預覽
即時 Markdown 編輯與預覽,支援 GFM(表格、刪除線、checkbox、code block)。
- 字數計算機
即時統計字數、中文字數、英文單字、段落數、行數、含與不含空白字元。
- 身分證產生器與驗證
台灣身分證產生器與驗證,也支援居留證號(ARC 統一證號):隨機產生測試用合法號碼、驗證並解析縣…
- Lorem Ipsum 產生器
產生 Lorem Ipsum 拉丁假文或中文亂數段落,可自選段落數與每段句數,複製成純文字、Ma…
隱私:pattern 與你的測試文字完全在瀏覽器裡編譯與比對,不會送到伺服器、不會存在伺服器上,也不會被記錄。最近五組 pattern 與文字會留在這台瀏覽器的 localStorage 供歷史紀錄還原,可以從那裡清除。本工具沒有自己的分析事件,結果區也排除在錄影分析之外,pattern 與文字都不會被收集。
