掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
現(xiàn)如今,正則表達(dá)式幾乎是程序員的必備技能了,它入手確實(shí)很容易,但如果你不仔細(xì)琢磨學(xué)習(xí),會(huì)長(zhǎng)期停留在正則最基本的用法層面上。
因此,本篇文章,我會(huì)介紹一些能用正則解決的場(chǎng)景,但這些場(chǎng)景如果全自己琢磨實(shí)現(xiàn)的話,需要花一些時(shí)間才能完成,或者就完全想不出來(lái),另外也會(huì)介紹一些正則表達(dá)式的性能問(wèn)題。

比如我想匹配zhangsan、lisi、wangwu這三個(gè)人名,這是一個(gè)很常見(jiàn)的場(chǎng)景,其實(shí)在正則里面也算基本功,但鑒于本人初入門時(shí)還是在網(wǎng)上搜索得到的答案,還是值得提一下的!
實(shí)現(xiàn)如下:
zhangsan|lisi|wangwu
其中|表示或的含義,就是匹配zhangsan或lisi或wangwu了。
匹配如1111、2222、3333這樣的4位長(zhǎng)度的重復(fù)數(shù)字,突一想,這不用\d{4}就解決了嚒,其實(shí)不然,因?yàn)?code>\d{4}可以匹配1111,但也可以匹配1234啊。
寫法如下:
(\d)\1{3}
\d匹配第一個(gè)數(shù)字,后面的\1匹配前面\d匹配的內(nèi)容,重復(fù)3次,這樣就可以匹配1111或2222這樣的4位數(shù)字串了。
在使用正則時(shí),常用\s來(lái)匹配空白,但遺憾的是,還是有一些Unicode的空白字符,\s無(wú)法匹配,這時(shí)可以嘗試POSIX字符類\p{Space},我在Java中驗(yàn)證通過(guò),可以匹配ascii空白字符與Unicode空白字符,如果是其它語(yǔ)言的話,可能正則語(yǔ)法會(huì)稍有區(qū)別。
正則表達(dá)式中\(zhòng)G與環(huán)視是比較難理解的,因?yàn)檫@兩個(gè)東西很多書上只是介紹了匹配的規(guī)則,沒(méi)有說(shuō)出實(shí)質(zhì),導(dǎo)致死記的規(guī)則過(guò)一段時(shí)間就忘,也不明白這兩東西有啥用。
我們轉(zhuǎn)換一下思維,其實(shí)在正則表達(dá)式中,匹配目標(biāo)只有兩個(gè),一是匹配字符串中的字符,二是匹配字符串中的位置,如下圖:
上邊的hello,有5個(gè)字符可以匹配,另外還有6個(gè)位置可以匹配,而^hello中^就是代表匹配開頭的位置,所以如果是_hello就無(wú)法被^hello匹配,因?yàn)?code>_與h之間的位置并不是開頭,不能與^匹配!
常見(jiàn)位置匹配規(guī)則
| 規(guī)則 | 匹配的位置 |
|---|---|
| ^ \A | 匹配開始位置 |
| $ \z \Z | 匹配結(jié)束位置 |
| \b \B | 匹配單詞與非單詞邊界位置 |
| \G | 匹配當(dāng)前匹配的開始位置 |
| (?=a) (?!a) | 正向環(huán)視,看看當(dāng)前位置后面是否是a,或不是a |
| (?<=a) (? | 逆向環(huán)視,看看當(dāng)前位置前面是否是a,或不是a |
^與\A
^ 匹配文本開始位置,但在多行匹配模式下,^匹配每一行的開始位置。
\A 僅僅只能匹配開始位置,不管什么匹配模式下
$與\Z
$?匹配文本末尾位置,但在多行匹配模式下,$匹配每一行的末尾位置。
\Z 僅僅只能匹配末尾位置,不管什么匹配模式下
\b與\B
\b匹配單詞邊界,在Java中,單詞邊界即是字母與非字母之間的位置,中文不認(rèn)為是單詞,另外文本開頭與文本結(jié)尾也是單詞邊界
\B匹配非單詞邊界
\G
匹配上次匹配的結(jié)束位置或當(dāng)前匹配的開始位置,第一次匹配時(shí),匹配文本開始位置,如下:
從1234a5678中找單個(gè)數(shù)字,如果用\d去找,可以找到8個(gè),但使用\G\d去找,卻只能找到4個(gè)
查找過(guò)程:
第1次查找,\G匹配文本開始位置,1與\d匹配,找到第1個(gè)匹配,即1
第2次查找,\G匹配1后面2前面之間的位置,2與\d匹配,找到第2個(gè)匹配,即2
第3次查找,\G匹配2后面3前面之間的位置,3與\d匹配,找到第3個(gè)匹配,即3
第4次查找,\G匹配3后面4前面之間的位置,4與\d匹配,找到第4個(gè)匹配,即4
第5次查詢,\G匹配4后面5前面之間的位置,但a與\d不匹配,匹配結(jié)束,總共找到4個(gè)匹配。
環(huán)視
(?=a) 與 (?!a)
正向肯定(否定)環(huán)視,用來(lái)檢測(cè)當(dāng)前位置后面字符是否是a,或不是a
(?<=a) 與 (?
逆向肯定(否定)環(huán)視,用來(lái)檢查當(dāng)前位置前面字符是否是a,或不是a
如下,查找被()包裹的單詞,使用環(huán)視限定單詞左邊是(,右邊是)
位置可被多次匹配
文本中的一個(gè)位置,可以同時(shí)匹配多個(gè)規(guī)則,且與規(guī)則在正則表達(dá)式中的先后順序無(wú)關(guān),例如下面3個(gè)正則表達(dá)式是等價(jià)的:
^abc
^^^^^^abc
^(?=a)\b^^^abc
下面舉兩個(gè)實(shí)際例子體會(huì)一下位置匹配!
例1:密碼強(qiáng)度校驗(yàn)
前端校驗(yàn)密碼強(qiáng)度時(shí),經(jīng)常有這樣的要求,長(zhǎng)度8到10位,且必須包含數(shù)字、字母、標(biāo)點(diǎn)符號(hào),可通過(guò)一個(gè)正則表達(dá)式校驗(yàn)出來(lái),如下:
^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\p{P}).{8,10}$
其中,(?=.*[0-9])表示開頭位置的后面一定要有數(shù)字,(?=.*[a-zA-Z])表示開頭位置后面一定要有字母,(?=.*\p{P})表示開頭位置的后面一定要有標(biāo)點(diǎn)符號(hào),.{8,10}表示匹配8到10位字符,這幾個(gè)正則合在一起,就實(shí)現(xiàn)了校驗(yàn)密碼強(qiáng)度的要求。
例2:千分位數(shù)字
有時(shí)我們需要將123456789變成123,456,789這樣的千分位數(shù)字,這個(gè)使用正則就可以實(shí)現(xiàn),如下,將此正則匹配到的位置,替換為,:
(?!^)(?=(\d{3})+$)
其中,(?=(\d{3})+$)表示匹配位置,這個(gè)位置后面必須要有一組或多組3個(gè)數(shù)字,滿足這樣條件的位置有3個(gè),開頭與1之間的位置,3和4之間的位置,6和7之間的位置,然后(?!^)又限制了同樣的這些位置,不能是開頭,就只能3和4,6和7之間的位置滿足要求了,所以替換之后,就變成了123,456,789。
匹配諸如"hello,world"這樣的帶引號(hào)的字符串,很容易想到,用"[^"]+"即可,但是如果引號(hào)字符串里面允許用\來(lái)轉(zhuǎn)義"呢,如"hello \"bob\"!",如果用"[^"]+"來(lái)匹配的話,就只會(huì)匹配到"hello \"了,顯然不對(duì),可以先自行想想如何用正則實(shí)現(xiàn)。
...
...
...
想不出來(lái)?我們可以換一個(gè)視角,包含帶\開頭轉(zhuǎn)義字符的字符串,其實(shí)可以拆解為",hello,\"bob,\"!,",然后再泛化為正則形式,",[^\\"]*,\\.[^\\"]*,\\.[^\\"]*,",組合在一起如下:
"[^\\"]*(?:\\.[^\\"]*)*"
表達(dá)式中多了個(gè)(?:),這表示非捕獲分組,可以用來(lái)提高正則匹配性能,而由于字符串中有可能沒(méi)有\開頭的轉(zhuǎn)義字符,故(?:\\.[^\\"]*)后面是*,直接由[^\\"]*匹配完引號(hào)內(nèi)所有內(nèi)容。
正則表達(dá)式如果寫得很復(fù)雜,就需要謹(jǐn)慎評(píng)估了,因?yàn)橛锌赡芷綍r(shí)運(yùn)行得好好的,但遇到一些特殊情況,會(huì)導(dǎo)致CPU直接100%,比如還是上面那個(gè)匹配帶引號(hào)字符串的場(chǎng)景,有同學(xué)可能會(huì)給出這樣的正則:
"([^\\"]+|\\.)*"
乍一看,這個(gè)正則很完美,[^\\"]+匹配非轉(zhuǎn)義字符的部分,\\.匹配\",\n之類的。這個(gè)正則在遇到滿足條件的字符串時(shí)完全沒(méi)有問(wèn)題(如"hello \"bob\"!"),而遇到不滿足條件的字符串時(shí),正則匹配復(fù)雜度會(huì)隨著字符串長(zhǎng)度呈指數(shù)式上升,導(dǎo)致CPU 100%,如"hello \"bob\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!,其中"沒(méi)有閉合。
public static void main(String[] args) {
long begin = System.currentTimeMillis();
boolean isMatch = "\"hello \\\"bob\\\"!!!!!!!!!!!!!!!!!!".matches("\"([^\\\\\"]+|\\\\.)*\"");
System.out.println(String.format("%s ms, isMatch: %s", System.currentTimeMillis() - begin, isMatch));
}
這段java代碼,在我機(jī)器上跑完要2s的樣子,但如果字符串中再加4個(gè)!,運(yùn)行時(shí)間立馬上升到17s,性能下降非常恐怖!
原因
如果知道一些正則匹配原理,應(yīng)該知道正則在匹配時(shí),如果匹配不上,會(huì)將已經(jīng)匹配的字符吐出來(lái),再看看是否能夠匹配,這叫回溯,比如".*"匹配"hello",先正則中的"匹配上了字符串中的",然后.*依次匹配了h,e,l,l,o,",最后正則中的"匹配字符串結(jié)尾位置,匹配不上,這時(shí)正則引擎會(huì)讓前面的.*吐出它匹配的",然后吐出來(lái)的這個(gè)",剛好可以和正則中的"匹配,這樣就匹配成功了。
那如果是"hello這樣沒(méi)有閉合的字符串,.*會(huì)一直吐字符,一直到它沒(méi)有字符可吐,發(fā)現(xiàn)還是匹配不上,這樣整個(gè)匹配才認(rèn)定為匹配失敗。
是的,正則中包含匹配量詞?,*,+時(shí),你就可以想像為它們一直在吃字符,當(dāng)后面的規(guī)則匹配不上時(shí),會(huì)強(qiáng)迫它又吐出來(lái),而如果是懶惰匹配量詞??,*?,+?,你就可以想像它先不吃,當(dāng)后面的規(guī)則匹配不上時(shí),會(huì)強(qiáng)迫它去吃。
我們?cè)賮?lái)分析下"([^\\"]+|\\.)*"匹配"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!為啥會(huì)如此低效!
注:為了分析方便,我簡(jiǎn)化了待匹配字符串,但效果是一樣的
[^\\"]+吃掉了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!。"與字符串結(jié)尾位置不匹配,開始回溯。[^\\"]+吐出一個(gè)!,注意這里,由于外層還有一個(gè)*貪婪量詞,吐出來(lái)的!又被[^\\"]+|\\.中的[^\\"]+吃掉了,它吃掉后,到了字符串結(jié)尾,發(fā)現(xiàn)結(jié)尾又與正則中的"不匹配,又要求[^\\"]+|\\.中的[^\\"]+吐出剛吃掉的!,結(jié)果吐出后又不匹配。[^\\"]+吐出倒數(shù)第二個(gè)!,注意,再次吐出!后,當(dāng)前匹配位置后面有兩個(gè)!,可惡的是,這兩個(gè)!又被后面[^\\"]+|\\.中的[^\\"]+吃掉了,然后悲劇重演,它又要吐出來(lái),如此循環(huán)往復(fù),計(jì)算量指數(shù)級(jí)上升。解決辦法
其實(shí)可以看出來(lái),造成這個(gè)問(wèn)題是因?yàn)檎齽t表達(dá)式中有兩個(gè)量詞,內(nèi)層有一個(gè)+,外層有一個(gè)*,不信的話,你可以嘗試用^(a+)*$去匹配aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0,同樣的會(huì)非常慢。
而要解決這個(gè)問(wèn)題,有兩個(gè)辦法。
[^\\"]+吐出來(lái)的字符,無(wú)法被外層正則中另一個(gè)貪婪的自己吃掉,比如前面介紹的"[^\\"]*(?:\\.[^\\"]*)*",[^\\"]*吐出來(lái)的字符,是無(wú)法被\\.[^\\"]*吃掉的,因?yàn)橥鲁鰜?lái)的一定不是\,而\\.[^\\"]*要先吃一個(gè)\。"([^\\"]++|\\.)*"這樣,+變成了++,像這種量詞后面再加+號(hào)的,比如?+,*+,++,這表示占有量詞,吃完字符后就不會(huì)吐了。注:占有量詞不要亂用,有時(shí)吐出來(lái)字符可以讓整個(gè)正則匹配,而你強(qiáng)制讓它不吐出來(lái),反而讓它匹配不了了,如^.+b$可以匹配ab,但如果你用^.++b$就無(wú)法匹配ab了,因?yàn)?code>.吃掉了ab,吐出一個(gè)b剛好可以使后面的b匹配。而^[^b]++b$這種用法就是對(duì)的,因?yàn)?code>^b吐出來(lái)的字符肯定不能和后面的b匹配,就沒(méi)必要再吐了。
正則表達(dá)式很強(qiáng)大,用好它事半功倍,但也需要了解它的執(zhí)行過(guò)程,避免指數(shù)級(jí)回溯陷阱。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流