怎麼寫正規表達式(Regular Expressions)?

在寫程式時常常會需要比對字串,或是從字串中取得目標的結果,在還不知道有正規表達式前,我都是使用像是indexOf之類的字串方法來比對,而我知道了正規表達式後,被這神奇的語法驚呆了,只用了一個表達式就可以實現我寫了幾十行的程式碼,但是對於他那像是天書般的寫法給嚇跑了,現在剛好有時間可以好好研究,這篇用來記錄正規表達式的寫法。

本文使用JavaScript的分隔符: 斜線(/)來表示一段正規表達式,如果使用的語言跟JavaScript的分隔符不同,請自行轉換。

正規表達式由兩種不同的字元組成: 一般字元特殊字元

一般字元

在正規表達式中任何非特殊字元的字元都是一般字元,表達的是完全相符的條件,例如/abc/就是要完全符合abc的字串才符合此表達式的條件。

特殊字元

使用正規表達式定義的特殊字元做模糊搜尋,例如/ab*c/會對應到以a為開頭,中間有零到多個b(因為*這個特殊字元的作用),最後以c結尾的字串,像是ac或是abbbbbc都會符合。

這些特殊字元可以依照作用不同被分為下面幾類:

  • 字元組(Character Sets)
  • 字元族(Character Classes)
  • 反斜線(backslash)
  • 邊界(Boundaries)
  • 多重選項(或)
  • 數量(Quantifiers)
  • 判斷(Assertions)
  • 群組及回溯參照(Grouping and back references)

字元組

如果在同個位置上想要找的不只有一個字串,而是在多個字串中的其中一個就符合條件,這時使用字元組可以達到此需求。

例如[xyz]就是只要xyz就會符合目標。

[xyz][x-z]

取得符合中括號([])中其中一個字元的字元。

如果想要查找的是連續的字母或數字,可以用hypen(-)來設定,例如設定[a-d]的效果跟[abcd]是一樣的。

如果真的要搜尋hypen(-)可以將其放在最前(後)面來查找,例如[abcd-][-abcd] 就會符合non-profit中的hyphen(-)。

在中括號內跳脫字元及特殊字元可以不用用反斜線(\)來變為一般字元,例如.在正規表達式中原本代表除了空白字元外的所有字元,但如果寫成[.]就只會符合.這個字元。

字元組的中括號內可以使用字元族(例如\w)來做表示,例如/[a-z.]+/會跟/[\w.]+/一樣符合test.i.ng全部的字串。

[^xyz]

取得不符合中括號([])中的任何字元,其他的特性跟[xyz]相同。

例如[^abc][^a-c]相同,在brisket中因第一個b不符合條件所以會抓到r,而chop也是會抓到h

字元族

字元族就是預定義的字元組,他會將常用的字元組簡化,讓表達式更加簡潔好記。

.

符合任何除了換行字元外的字元,跟[^\n\r\u2028\u2029]有相同效果。

例如: /.n/可以在nay, an apple is on the tree中符合anon,但是不能符合nay,因為nay前面沒有任何字元。

如果放在[]中的話會變成一般字元。

由於.不包含換行字元,如果要包含換行字元可以用[^]來比對。

\d

符合任何數字字元的條件,跟[0-9]效果相同。

例如在B2 is the suite number.中用/\d//[0-9]/會找到2

\D

符合任何非數字字元的條件,跟[^0-9]效果相同。

例如在B2 is the suite number.中用/\D//[^0-9]/會找到B

\w

符合所有字母數字的字元條件,效果同於[A-Za-z0-9_]

例如/\w/apple中符合a,在$5.28,中符合5,在3D中符合3

\W

符合所有非字母數字的字元條件,效果同於[^A-Za-z0-9_]

例如/\w/50%.中符合%

\s

符合所有空白字元的條件,空白字元包括space, tab, form feed, line feed,它的效果會同於[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]

例如對foo bar.使用/\s\w*/\s會對應至空白字元,\w*會對應零到多個字母字元,所以取得的結果會是" bar"

\S

符合所有非空白字元的條件,空白字元包括space, tab, form feed, line feed,它的效果會同於[^ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]

例如對foo bar.使用/\S*/\S會對應至非空白字元,所以取得的結果會是foo

\f, \n, \r, \t, \v

這個系列為符合各個控制字元\f符合form feed(U+000C)條件,\n符合line feed(U+000A)條件, \r符合carriage return(U+000D)條件,\t符合tab(U+0009)條件,\v符合vertical tab(U+000B)條件。

\f\n\v的差異請參考這篇

\v可以參考這篇

\cX

X為A-Z中其中一個字元,用\cX取得控制字元,分別代表U+0001U+001A中的26個控制字元。

例如\cM就是代表U+000Dcontrol+M,也就是\r,而要找\n(Newline)字元,可以用\cJ(U+000A)來取得。

[\b]

符合退格字元(Backspace)條件,效果同於\cH(U+0008)。

\0

符合NULL字元的條件,注意後面不能接數字,如果接的話會被當作跳脫字元,可以用\x00取代。

\XXX

XXX是個8進位的ISO 8859-1編碼,數字會在0到377之間。

例如\251是copyright(©)的符號。

\xXX

XX是個16進位的ISO 8859-1編碼,數字會在0到FF之間。

例如\xA9是copyright(©)的符號。

\uXXXX

XXXX是個16進位的Unicode編碼

例如\u00A9會是copyright(©)的符號。

\u{XXXX}\u{XXXXX}

在旗標u設置時需要用{}(大括號)括住16進位的Unicode編碼,這個狀態下最多可以表示5碼的Unicode。

反斜線

接於反斜線(/)後的一般字元變為特殊字元,特殊字元變為一般字元。

一般字元變為特殊字元

字元b原本是一般字元,用來找尋符合小寫b的內容, 但如果是\b的話就代表字母邊界,就是用來找尋所有用來切開字母的字元(例如空白, 符號…等)。

特殊字元變為一般字元

原本/a*/會去找零到多個連續的a,但如果使用/a\*/的話就會找尋符合a*的字串。

反斜線本身也是特殊字元,如果要找尋反斜線請使用\\

邊界

^

^後的字元為內容的開頭字元,例如使用/^A/找尋an A時會沒有符合的結果,但An E就會符合A

使用m旗標可以使其作用在換行字元後,例如/^A/m找尋an\nA時可以找到A

^出現在字元組前會有不同的定義,詳細請看字元組的說明。

$

$前面的字元為輸入或是斷行字元前的最後一個字元,例如使用/t$/eater中不會找到t,但是在eat中會找到t

使用m旗標可以使其作用在換行字元後,例如/A$/m找尋A\nan時可以找到A

\b

字母字元前(後)沒有字母字元時符合此條件,截斷字母字元的字元不會被列入結果中,也可以將截斷的字元想成長度是0的字元。

下面有三個例子:

  • /\bm/: 會符合moon中的m,因為m的前面不是字母字元。
  • /oo\b/: 對於moon不會符合任何字串,因為oo後面接的是字母字元n
  • /oon\b/: 會符合moon中的oon
  • /\w\b\w/: \b自己不計入結果,但是並不會有一個字母後面同時跟著非字母及字母的情況發生,所以這個表達式不會符合任何條件。

字母字元在ECMAScript上有定義。

\B

字母字元前(後)若是字母字元的話則符合此條件,本身不會是結果的一部分,在它的左右兩邊同時要是字母字元或是非字母字元。

例如對noonday使用/\B../會對應到oo,而對possibly yesterday使用/y\B./會對應到ye

多重選項(或)

符合其中一個條件即為比對成功。

x|y

**符合xy**,例如/green|red/green apple中符合green,在red apple中符合red

在寫的時候需要注意順序,越前面的條件被符合後就不會再去符合之後的條件,例如對字串ba*|b會因為符合了empty而傳回空字串,如果使用b|a*就會先符合b而傳回b字母。

數量

符合某個字元(字串)多次的條件。

x{n}

符合重複x恰好n次的字串

例如用a{2}不會符合candy中的a,但會符合caandy中的aa

x{n,}

符合重複x至少n次的字串

例如a{2,}會符合aaaaaaaaaaa,但不會符合a

x{n,m}

符合重複x至少n次至多m次的字串

例如a{1,3}會符合aaaaaa,但不會符合aaaa

x*

符合x任意數量的字串,是x{0,}的簡寫。

例如使用/bo*/,在A ghost booooed中會找到boooo,在A bird warbled中會找到b,但A goat grunted找不到符合字串。

x+

符合x至少1次的字串,是x{1,}的簡寫。

例如使用/a+/,在candy中找的到a,在caaaaaaandy中找的到所有a所組成的字串,但是在cndy中找不到符合條件。

x?

符合x零到一次的字串,是x{0,1}的簡寫。

例如使用/e?le?/,在angel中找的到el,在angle中找到le的字串,在oslo中找到l字串。

xQ?

Q為數量特殊字元({n}{n,}{n,m}*+?)。

符合Q定義的數量的x的字串,但是符合最少字串(non-greedy)。

在沒有加?時,會是取符合最多數的結果,加了?後會是取符合最短字串的結果

例如<.*>會符合<foo> <bar>中的<foo> <bar>,但是<.*?>會取到<foo>

判斷

利用判斷式來決定要不要匹配條件。

x(?=y)

只有**在x後面緊跟著y時才會取得x**。

例如/Jack(?=Sprat)/一定要Jack後面緊接著Sprat才會取得Jack,而Sprat不會在結果裡面。

x(?!y)

只有在x後面沒有緊跟著y時才會取得x

例如/\d+(?!\.)/會對應到3.141中的141,但不會對到3

群組及回溯參照

群組有下面這些功能:

  • 使數量特殊字元作用於字串上。
  • 群組中匹配的字串會放入結果中。
  • 群組中匹配的字串可以回溯參照

(x)

取得符合x的字串並且放入結果中

例如: /(foo) (bar)/foo bar中會完整符合foo bar(放於結果陣列[0]中),及群組1的foo(放於結果陣列[1]中)和群組2的bar(放於結果陣列[2]中)。

如果在其後使用數量特殊字元會作用於整個群組中的字串上。

例如/foo{1,2}/後面的{1,2}的效果就只有在最後一個o才有效果,而/(foo){1,2}/則可以抓出重複foo一到兩次的結果。

(?:x)

取得符合x的字串但不放入結果中

如果在其後使用數量特殊字元會作用於整個群組中的字串上。

例如/(foo){1,2}/可以抓出重複foo一到兩次的結果,但我們可能只是要做匹配,並不想要取得此群組結果,所以可以用/(?:foo){1,2}/使數量特殊字元作用於群組上,但並不會將其放入結果陣列中。

\n

用來表示回溯參照: 符合結果陣列中第n個結果

例如用/apple(,)\sorange\1/查找apple, orange, cherry, peach.,因為\1會符合結果陣列中的[1],而[1]是存放,字元([0]是存放完整結果),之後的\s會符合空白字元,所以會符合apple, orange,

再比對html文檔時可以看出回溯參照的優點,例如使用<(\w+)>(.+)<\/\w+>比對<b>Hello</div>會成功,但html的起始及結束tag要是相同的,這樣的結果不符合需求,而使用<(\w+)>(.+)<\/\1>可以排除這種起始及結束tag不同的情況。

旗標

正規表達式中的旗標可以設定搜尋的方式,它提供了下面幾個旗標:

  • g: 搜尋完整的內容找出全部的結果。
  • m: 用^或$的效果在換行時也有用。
  • i: 不分大小寫。

範例

下面這個範例演繹各個旗標的功能。

See the Pen regular expression flags by Peter Chen (@peterhpchen) on CodePen.

  • /[a-z]+/: 因為沒有g旗標,所以取得結果後馬上結束查找。
  • /[a-z]+/g: 加上g旗標會將查找所有內容。
  • /^[a-z]+/g: 因為加了^開頭的條件,所以只會符合整個內容的開頭字串。
  • /^[a-z]+/gm: 加了m旗標後^$也會作用於換行字元上。
  • /^[a-z]+/gim: 加上i旗標可以不分大小寫查找。

結語

正規表達式是個很強大的功能,用來查找字串非常的方便,但可讀性低,需要記憶的特殊字元多,常常讓開發者忽略了這個好用的功能,在此整理了表達式的寫法,使用分類來加強記憶,希望可以增加開發上的使用頻率。

投影片

在讀書會中使用的投影片:

參考