正規表達式總複習 | Regular Expression Summary

CHC1024
狗奴工程師
Published in
8 min readFeb 26, 2021

前言

時至今日,我們的生活中會需要填許許多多的表單 (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+abcabc 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 或 ghi
  • abcdef|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) 會變成 abcdefabcdefghi

進階用法

  • 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 系統相關的程式,反而已經忽略前端很久了……。我自己的想法是在我規劃出未來個人私下的研究方向前,應該就會以近期遇到的一些坑來寫短篇文章。

--

--