前言
時至今日,我們的生活中會需要填許許多多的表單 (form),例如調查大型聚會的時間。從茫茫資料海當中,我們必須擷取出有用的數據來作分析,以此改善產品的品質或將資料作一層轉換。
對於這些步驟,光用 if-else 來做肯定會累死人且不實際,所以「正規表達法」就隨著這些需求而誕生。
總之,今天要來講的是「正規表達法 (Regular Expression)」。
話不多說,讓我們趕緊開始吧!
Character Classes (字元集合)
原則上我們會需要明確指定出要比對的字元,例如英文字母 A 或 數字 1。但更多時候我們要比對的字元會介於一個範圍內,所以正規表達法也有規定一些通用的keyword來代表一些字元。
- \s: 空白字元 (包含 tabs)
- \d: 數字 (即 0–9)
- \w: 數字 + 英文字母 + 底線符號 ( _ )
- . : 任意字元
Anchors (錨點、定位符號)
錨點的作用是「定位」。也就是這些符號並不會被用於「比對」,而是用來規定一些特定的字串必須待在指定的位置,例如開頭與結尾。
- ^ (start with): 開頭有XXX的字串
- $ (end with): 結尾有XXX的字串
- \b (boundary): 表示特定位置不可有 word character (\w)
- \A (absolutely start with): 類似
^
但差別在於不受 multi-line mode 影響,一律把整個輸入當作一個字串。 - \Z (multi-line end with): 類似
%
但差別是不受multi-line mode 影響,一律將輸入視作一個字串,而且允許結尾有多餘的換行 (trailing newline)。 - \z (absolutely multi-line end with): 同上,但不允許有多餘的換行
\b 這個定位符號,可以用在比對字詞 (word),其中一邊會是 \w 中的字元,而另外一邊則必須不是 \w 中的字元才會符合此表達式。舉個例子來說:+abc+
、abc
、abc def
都符合\babc\b
。
其中 +abc+
會符合的原因是+
並不是 \w 中定義的 word character,所以可以被接受。
後面的三個定位符號很少在使用,我也是在查找資料才知道有這些符號可以使用,因為沒特別使用到 multi-line mode 也就不太會注意到這個細節。
如果對 \Z 與 \z 還是分不清楚,可以用下面的這些例子來釐清:
(O) foo ~= foo\Z
(O) foo ~= foo\z
(O) foo\n ~= foo\Z
(X) foo\n ~= foo\z
如果硬要背的話,感覺用「z 比較小心眼」也許不錯,畢竟 z 是小寫😄
Quantifiers (數詞符號)
數詞用於規範特定字串會重複的次數。
- ?: 字串在比對時可有可無 (但至多一次)
- +: 字串必須出現至少一次 (可無限多次)
- *: 字串可出現任意次數
- {a,b}: 字串出現的次數必須介於 a 和 b 之間 (b 前面不能有空格)
OR Operators
正規表達式沒有特別使用 or operator 的時候都是「sequential matching」,也就是表達式中的字串都有可能同時存在。但當使用 or operator 時就變成唯有其一可以存在。
- |: 用於二個字元以上的字串,例如
hello|hi|hey
- []: 用於單一字元的字串,例如
[abc]
代表 a、b 或 c 都可
事實上 [] 還有其他用法,例如說當字元是「數字」或「字母」,而且剛好是連續的,那可以用 -
來簡化表達式。像是 [abcdefgh] 其實就可以簡化成 [a-h]。
值得注意的是,一般使用「|」會搭配 group capturing,以確保不會搞錯語意。例如:abc(def|ghi)
與 abcdef|ghi
所表達的是不同的意思。
abc(def|ghi)
: abc 後面接著 def 或 ghiabcdef|ghi
: abcdef 或 ghi
因此我們很高機率會使用group的形式來確保語意的正確性,這會間接導致後續擷取 group 的值時需要注意 group 的順序。
對於這項問題,晚點介紹 group 時會一併提及該如何解決。
Negation 否定詞
有時候我們想比對的目標字串 (pattern) 範圍很廣,比如說:挑出不含有 a, t, i, w 的字串,或是挑出不含有數字的字串。這個時候以前所學到的來撰寫,那你的正規表達式就會變得很長且不易閱讀。
因此,語法中也提供一些「否定語法 (negation)」讓開發者可以解決上述的問題。
定位符號與字元集合
有些定位符號 (anchors) 與字元集合 (character classes) 有其否定語法,都只需要把英文改成大寫就可以。清單如下:
- \W: 不是 word character 的字元 (例如: +, @, #)
- \D: 不是數字的字元
- \S: 非空白字元
- \B: 另外一側必須不是
\w
的字元
OR Operator
針對 [] (bracket) 也有其否定語法存在,作法就是在左邊括弧的右側加上一個 ^
即可。舉個例子來說:
[^atiw]
: 非 a, t, i, w 的字元均符合[^x-z]
: 非 x, y, z 的字元均符合[^a-d0–5]
: 非 a~d 且 非 0~5 的字元均符合
Grouping and Capturing
我們不是單純要知道輸入的字串是否符合正規表達式,還要抓出符合的部分。
舉個例子來說,我們透過 09\d{8}
簡易地抓出台灣的手機號碼。但如果今天我所輸入的字串是 my phone number is 0912345678,這時候就需要加上 ()讓程式能夠幫我把括弧框起來的子字串存起來,如此一來就不用擔心使用者輸入不必要的文字。
以下介紹 Grouping 的用法:
基本用法
- Enable grouping:
(your pattern)
ex:(09\d{8})
- Disable grouping:
(?:your pattern)
ex:(?:https?|ftp)
- Named grouping:
(?<group name>your pattern)
ex:(?<foo>bar)
前面有說到當你使用 | 來做 or operation 會需要注意語意的問題,所以時常會借住 group 來減少語意錯誤的發生,但使用 group 也衍生額外的問題 — 產生不必要的 group。因此 grouping 才會有跳過群組的功能,就是為了避免冗員並讓我們能寫出較為簡潔的程式碼。
順帶一提,group 是採用 DFS 做 capturing。比如說 ((abc)(def))(ghi)
會變成 abcdef
、abc
、def
、ghi
。
進階用法
- Back reference
當我們在分析一個 HTML 檔案,必定會做 tag matching。而為避免有人寫了不合法的 tag,像是<div>……</span>
,我們需要在表達式中就可以直接取用前面 group 抓取到的結果。也就是如此,衍生出 back reference 這樣的語法。
- order-based: \1、\2、\3 …
1.<(.+?)>.*<\/\1>
Try it!
2.([a-zA-Z])([0-9])\2\1
Try it!
- name-based:\k<group name>
1.(?<foo>bar)\k<foo>
Try it! - Positive Look-ahead/Look-behind
在正規表達式中還定義了兩個有趣的語法,分別是 look-ahead(?=)
跟 look-behind(?<=)
,而他們有一個共通點是「group是條件,不屬於比對結果」。我們直接來看幾個範例:
1.\d+(?=[a-z]{2})
→ 給 123ab456cd789 則會挑出 123 和 456,但不包含後面的英文字
2.(?<=\$)\d+
e
→ 給 1 iphone costs $699 則會將 699 挑出而不包含 $
簡單來說,look-ahead 跟 look-behind 的 group 雖然會被拿來做比對,但不會列入比對結果中,更不會被 capture。好處是我們可以做額外的filter,找出特定位置的目標字串。 - Negative Look-ahead/Look-behind
特性一樣,差別只是其描述的是否定語句。
- Negative look-ahead(?!)
- Negative look-behind(?<!)
順帶一提,因為正規語言的解析方向是由左往右,所以 ahead 指的是 followed with;而 behind 指的是 preceded by。
總結
從上面的總整理,我們不難看出正規表達法的語法有多麼多樣,但只要你懂得運用,正規表達法就可以在很多領域派上用場:
- 驗證資料 (data validation)
- 網頁爬蟲 (data scraping)
- 資料格式轉換 (data wrangling)
- 分析字串 (string parsing)
- 替換字串 (string replacement)
- 封包分析 (packet sniffing)
- 檔案批次重新命名 (file renaming)
- …
總之,當你學會正確使用正規表達法,必然可達到事半功倍的效益!
希望上述的整理有幫助到正在尋找中文版教學的你~
後記
大家好阿,距離上一次發文已經是去年的事情了。想一想還真的很慚愧,因為上學期的課業有點重,而且為了適應研究所生活也花了一些精力,所以怠惰了寫文章這一塊。
不過!為了能讓自己更加精進自己的 coding 能力,我不會停下學習的腳步!
這次的文章主題跟以前的比較不一樣,偏重的不是 JavaScript 或 前端的一些知識,而是大多程式語言共通的正規表達。近期我大多都在寫 Java 或 Linux 系統相關的程式,反而已經忽略前端很久了……。我自己的想法是在我規劃出未來個人私下的研究方向前,應該就會以近期遇到的一些坑來寫短篇文章。