在寫程式時常常會需要比對字串,或是從字串中取得目標的結果,在還不知道有正規表達式前,我都是使用像是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]
就是只要x
、y
、z
就會符合目標。
[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
中符合an
,on
,但是不能符合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+0001
到U+001A
中的26個控制字元。
例如\cM
就是代表U+000D
的control+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
**符合x
或y
**,例如/green|red/
在green apple
中符合green
,在red apple
中符合red
。
在寫的時候需要注意順序,越前面的條件被符合後就不會再去符合之後的條件,例如對字串b
做a*|b
會因為符合了empty而傳回空字串,如果使用b|a*
就會先符合b
而傳回b
字母。
數量
符合某個字元(字串)多次的條件。
x{n}
符合重複x
恰好n
次的字串。
例如用a{2}
不會符合candy
中的a
,但會符合caandy
中的aa
。
x{n,}
符合重複x
至少n
次的字串。
例如a{2,}
會符合aa
、aaaa
、aaaaa
,但不會符合a
。
x{n,m}
符合重複x
至少n
次至多m
次的字串。
例如a{1,3}
會符合a
、aa
、aaa
,但不會符合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
旗標可以不分大小寫查找。
結語
正規表達式是個很強大的功能,用來查找字串非常的方便,但可讀性低,需要記憶的特殊字元多,常常讓開發者忽略了這個好用的功能,在此整理了表達式的寫法,使用分類來加強記憶,希望可以增加開發上的使用頻率。
投影片
在讀書會中使用的投影片: