對(duì)于一門(mén)語(yǔ)言的掌握程度怎么樣,可以有兩個(gè)角度來(lái)衡量:讀和寫(xiě)。
不僅要求自己能解決問(wèn)題,還要看懂別人的解決方案。代碼是這樣,正則表達(dá)式也是這樣。
正則這門(mén)語(yǔ)言跟其他語(yǔ)言有一點(diǎn)不同,它通常就是一大堆字符,而沒(méi)有所謂“語(yǔ)句”的概念。
如何能正確地把一大串正則拆分成一塊一塊的,成為了破解“天書(shū)”的關(guān)鍵。
本章就解決這一問(wèn)題,內(nèi)容包括:
- 結(jié)構(gòu)和操作符
- 注意要點(diǎn)
- 案例分析
#1. 結(jié)構(gòu)和操作符
編程語(yǔ)言一般都有操作符。只要有操作符,就會(huì)出現(xiàn)一個(gè)問(wèn)題。當(dāng)一大堆操作在一起時(shí),先操作誰(shuí),又后操作誰(shuí)呢?為了不產(chǎn)生歧義,就需要語(yǔ)言本身定義好操作順序,即所謂的優(yōu)先級(jí)。
而在正則表達(dá)式中,操作符都體現(xiàn)在結(jié)構(gòu)中,即由特殊字符和普通字符所代表的一個(gè)個(gè)特殊整體。
JS正則表達(dá)式中,都有哪些結(jié)構(gòu)呢?
字符字面量、字符組、量詞、錨字符、分組、選擇分支、反向引用。
具體含義簡(jiǎn)要回顧如下(如懂,可以略去不看):
字面量,匹配一個(gè)具體字符,包括不用轉(zhuǎn)義的和需要轉(zhuǎn)義的。比如a匹配字符"a",又比如\n匹配換行符,又比如\.匹配小數(shù)點(diǎn)。字符組,匹配一個(gè)字符,可以是多種可能之一,比如[0-9],表示匹配一個(gè)數(shù)字。也有\(zhòng)d的簡(jiǎn)寫(xiě)形式。另外還有反義字符組,表示可以是除了特定字符之外任何一個(gè)字符,比如[^0-9],表示一個(gè)非數(shù)字字符,也有\(zhòng)D的簡(jiǎn)寫(xiě)形式。量詞,表示一個(gè)字符連續(xù)出現(xiàn),比如a{1,3}表示“a”字符連續(xù)出現(xiàn)3次。另外還有常見(jiàn)的簡(jiǎn)寫(xiě)形式,比如a+表示“a”字符連續(xù)出現(xiàn)至少一次。錨點(diǎn),匹配一個(gè)位置,而不是字符。比如^匹配字符串的開(kāi)頭,又比如\b匹配單詞邊界,又比如(?=\d)表示數(shù)字前面的位置。分組,用括號(hào)表示一個(gè)整體,比如(ab)+,表示"ab"兩個(gè)字符連續(xù)出現(xiàn)多次,也可以使用非捕獲分組(?:ab)+。分支,多個(gè)子表達(dá)式多選一,比如abc|bcd,表達(dá)式匹配"abc"或者"bcd"字符子串。反向引用,比如\2,表示引用第2個(gè)分組。
其中涉及到的操作符有:
轉(zhuǎn)義符 \括號(hào)和方括號(hào) (...)、(?:...)、(?=...)、(?!...)、[...]量詞限定符 {m}、{m,n}、{m,}、?、*、+位置和序列 ^ 、$、 \元字符、 一般字符管道符(豎杠)|
上面操作符的優(yōu)先級(jí)從上至下,由高到低。
這里,我們來(lái)分析一個(gè)正則:
/ab?(c|de*)+|fg/
- 由于括號(hào)的存在,所以,
(c|de*)
是一個(gè)整體結(jié)構(gòu)。 - 在
(c|de*)
中,注意其中的量詞*
,因此e*
是一個(gè)整體結(jié)構(gòu)。 - 又因?yàn)榉种ЫY(jié)構(gòu)“|”優(yōu)先級(jí)最低,因此
c
是一個(gè)整體、而de*
是另一個(gè)整體。 - 同理,整個(gè)正則分成了
a
、b?
、(...)+
、f
、g
。而由于分支的原因,又可以分成ab?(c|de*)+
和fg
這兩部分。
希望你沒(méi)被我繞暈,上面的分析可用其可視化形式描述如下:
#2. 注意要點(diǎn)
關(guān)于結(jié)構(gòu)和操作符,還是有幾點(diǎn)需要強(qiáng)調(diào):
2.1 匹配字符串整體問(wèn)題
因?yàn)槭且ヅ湔麄€(gè)字符串,我們經(jīng)常會(huì)在正則前后中加上錨字符^
和$
。
比如要匹配目標(biāo)字符串"abc"或者"bcd"時(shí),如果一不小心,就會(huì)寫(xiě)成/^abc|bcd$/
。
而位置字符和字符序列優(yōu)先級(jí)要比豎杠高,故其匹配的結(jié)構(gòu)是:
應(yīng)該修改成:
2.2 量詞連綴問(wèn)題
假設(shè),要匹配這樣的字符串:
每個(gè)字符為a、b、c任選其一字符串的長(zhǎng)度是3的倍數(shù)
此時(shí)正則不能想當(dāng)然地寫(xiě)成/^[abc]{3}+$/
,這樣會(huì)報(bào)錯(cuò),說(shuō)+
前面沒(méi)什么可重復(fù)的:
此時(shí)要修改成:
2.3 元字符轉(zhuǎn)義問(wèn)題
所謂元字符,就是正則中有特殊含義的字符。
所有結(jié)構(gòu)里,用到的元字符總結(jié)如下:
^ $ . * + ? | \ / ( ) [ ] { } = ! : - ,
當(dāng)匹配上面的字符本身時(shí),可以一律轉(zhuǎn)義:
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;
console.log( regex.test(string) );
// => true
其中string
中的\
字符也要轉(zhuǎn)義的。
另外,在string
中,也可以把每個(gè)字符轉(zhuǎn)義,當(dāng)然,轉(zhuǎn)義后的結(jié)果仍是本身:
var string = "^$.*+?|\\/[]{}=!:-,";
var string2 = "\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,";
console.log( string == string2 );
// => true
現(xiàn)在的問(wèn)題是,是不是每個(gè)字符都需要轉(zhuǎn)義呢?否,看情況。
2.3.1 字符組中的元字符
跟字符組相關(guān)的元字符有[]
、^
、-
。因此在會(huì)引起歧義的地方進(jìn)行轉(zhuǎn)義。例如開(kāi)頭的^
必須轉(zhuǎn)義,不然會(huì)把整個(gè)字符組,看成反義字符組。
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /[\^$.*+?|\\/\[\]{}=!:\-,]/g;
console.log( string.match(regex) );
// => ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":", "-", ","]
2.3.2 匹配“[abc]”和“{3,5}”
我們知道[abc]
,是個(gè)字符組。如果要匹配字符串"[abc]"時(shí),該怎么辦?
可以寫(xiě)成/\[abc\]/
,也可以寫(xiě)成/\[abc]/
,測(cè)試如下:
var string = "[abc]";
var regex = /\[abc]/g;
console.log( string.match(regex)[0] );
// => "[abc]"
只需要在第一個(gè)方括號(hào)轉(zhuǎn)義即可,因?yàn)楹竺娴姆嚼ㄌ?hào)構(gòu)不成字符組,正則不會(huì)引發(fā)歧義,自然不需要轉(zhuǎn)義。
同理,要匹配字符串"{3,5}",只需要把正則寫(xiě)成/\{3,5}/
即可。
另外,我們知道量詞有簡(jiǎn)寫(xiě)形式{m,}
,卻沒(méi)有{,n}
的情況。雖然后者不構(gòu)成量詞的形式,但此時(shí)并不會(huì)報(bào)錯(cuò)。當(dāng)然,匹配的字符串也是"{,n}",測(cè)試如下:
var string = "{,3}";
var regex = /{,3}/g;
console.log( string.match(regex)[0] );
// => "{,3}"
2.3.3 其余情況
比如=
!
:
-
,
等符號(hào),只要不在特殊結(jié)構(gòu)中,也不需要轉(zhuǎn)義。
但是,括號(hào)需要前后都轉(zhuǎn)義的,如/\(123\)/
。
至于剩下的^
$
.
*
+
?
|
\
/
等字符,只要不在字符組內(nèi),都需要轉(zhuǎn)義的。
#3. 案例分析
接下來(lái)分析兩個(gè)例子,一個(gè)簡(jiǎn)單的,一個(gè)復(fù)雜的。
3.1 身份證
正則表達(dá)式是:
/^(\d{15}|\d{17}[\dxX])$/
因?yàn)樨Q杠“|”,的優(yōu)先級(jí)最低,所以正則分成了兩部分\d{15}
和\d{17}[\dxX]
。
\d{15}
表示15位連續(xù)數(shù)字。\d{17}[\dxX]
表示17位連續(xù)數(shù)字,最后一位可以是數(shù)字可以大小寫(xiě)字母"x"。
可視化如下:
3.2 IPV4地址
正則表達(dá)式是:
/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/
這個(gè)正則,看起來(lái)非常嚇人。但是熟悉優(yōu)先級(jí)后,會(huì)立馬得出如下的結(jié)構(gòu):
((...)\.){3}(...)
上面的兩個(gè)(...)
是一樣的結(jié)構(gòu)。表示匹配的是3位數(shù)字。因此整個(gè)結(jié)構(gòu)是
3位數(shù).3位數(shù).3位數(shù).3位數(shù)
然后再來(lái)分析(...)
:
(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])
它是一個(gè)多選結(jié)構(gòu),分成5個(gè)部分:
0{0,2}\d
,匹配一位數(shù),包括0補(bǔ)齊的。比如,9、09、009;0?\d{2}
,匹配兩位數(shù),包括0補(bǔ)齊的,也包括一位數(shù);1\d{2}
,匹配100到199;2[0-4]\d
,匹配200-249;25[0-5]
,匹配250-255。
最后來(lái)看一下其可視化形式: