在寫程式時常常會需要比對字串,或是從字串中取得目標的結果,在還不知道有正規表達式前,我都是使用像是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旗標可以不分大小寫查找。
結語
正規表達式是個很強大的功能,用來查找字串非常的方便,但可讀性低,需要記憶的特殊字元多,常常讓開發者忽略了這個好用的功能,在此整理了表達式的寫法,使用分類來加強記憶,希望可以增加開發上的使用頻率。
投影片
在讀書會中使用的投影片: