正則表達(dá)式介紹

正則表達(dá)式是匹配模式,匹配字符或者匹配位置。

一、字符匹配

1.兩種模糊匹配

1.1 橫向模糊匹配

一個正則可匹配的字符串的長度不是固定的,可以是多種情況的,實現(xiàn)方式是使用量詞。
例如{m,n},表示連續(xù)出現(xiàn)最少m次,最多n次。

var regex = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log(string.match(regex) );  // ["abbc", "abbbc", "abbbbc", "abbbbbc"]
1.2 縱向模糊匹配

一個正則匹配的字符串,具體到某一位字符時,它可以不是某個確定的字符,可以有多種可能,實現(xiàn)方式是使用字符組。
例如[abc],表示該字符可以是字符“a”、“b”、“c”中的任何一個。

var regex = /a[123]b/g;
var string= "a0b a1b a2b a3b a4b";
console.log(string.match(regex) );  // ["a1b", "a2b", "a3b"]

2. 字符組

雖然叫字符組(字符類),但只是其中一個字符。例如[abc],表示匹配一個字符,它可以是“a”、“b”、“c”之一。

2.1 范圍表示法

比如[123456abcdefGHIJKLM],可以用連字符-來簡寫成[1-6a-fG-M]。
因為連字符有特殊用途,匹配“a”、“-”、“z”這三者中任意一個字符,就可以寫成以下的方式:[-az]或[az-]或[a\-z],即放在開頭、結(jié)尾或者轉(zhuǎn)義。

2.2 排除字符組

例如[^abc],表示是一個除"a"、"b"、"c"之外的任意一個字符。字符組的第一位放^(脫字符),表示求反。

2.3 常見的簡寫形式

\d:[0-9]。表示是一位數(shù)字。
\D:[^0-9]。表示除數(shù)字外的任意字符。
\w:[0-9a-zA-Z_]。表示數(shù)字、大小寫字母和下劃線。
\W:[^0-9a-zA-Z_]。非單詞字符。
\s:[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、換行符、回車符、換頁符。
\S:[^ \t\v\n\r\f]。非空白符。
.是[^\n\r\u2028\u2029]。通配符,表示幾乎任意字符。換行符、回車符、行分隔符和段分隔符除外。

3. 量詞

量詞也稱重復(fù)。掌握{(diào)m,n}的準(zhǔn)確含義后,只需要記住一些簡寫形式。

3.1 簡寫形式

{m,} 表示至少出現(xiàn)m次。
{m} 等價于{m,m},表示出現(xiàn)m次。
? 等價于{0,1},表示出現(xiàn)或者不出現(xiàn)。
+ 等價于{1,},表示出現(xiàn)至少一次。
* 等價于{0,},表示出現(xiàn)任意次,有可能不出現(xiàn)。

3.2 貪婪匹配和惰性匹配

貪婪的,會盡可能多的匹配,只要在能力范圍內(nèi),越多越好。

var regex = /\d{2,5}/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); // ["123", "1234", "12345", "12345"]

惰性的,就是盡可能少的匹配。
通過在量詞后面加個問號就能實現(xiàn)惰性匹配,因此所有惰性匹配情形如下:
{m,n}?
{m,}?
??
+?
*?

var regex = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); // ["12", "12", "34", "12", "34", "12", "34", "56"]

4. 多選分支

多選分支可以支持多個子模式任選其一。
具體形式如下:(p1|p2|p3),其中p1、p2和p3是子模式,用|(管道符)分隔,表示其中任何之一。

var regex = /good|nice/g;
var string = "good idea, nice try.";
console.log( string.match(regex) ); // ["good", "nice"]

分支結(jié)構(gòu)也是惰性的。

var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) ); // ["good"]

二、位置匹配

位置是相鄰字符之間的位置



對于位置的理解,可以理將其解成空字符""。

1.匹配位置

1.1^和$

在多行匹配中^匹配行開頭,$ 匹配行結(jié)尾。
模擬字符串trim方法,即匹配到字符串開頭和結(jié)尾的空白符,然后替換成空字符。

function trim(str) {
  return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim("  foobar   ") ); // "foobar"
1.2 \b和\B

\b是單詞邊界,具體就是\w和\W之間的位置,包括\w和^之間的位置,也包括\w和$之間的位置。

var result= "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result); // "[#JS#] #Lesson_01#.#mp4#"

\B就是\b的反面,非單詞邊界,具體說來就是\w與\w、\W與\W、^與\W,\W與$之間的位置。

var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result); //"#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
1.3 (?=p)和(?!p)

二者的學(xué)名分別是positive lookahead(正向先行斷言)和negative lookahead(負(fù)向先行斷言)。ES6中,還支持(?<=p)positive lookbehind和(?<!p)negative lookbehind。
比如(?=l),表示'l'字符前面的位置

var result = "hello".replace(/(?=l)/g, '#');
console.log(result); // "he#l#lo" 

var result = "hello".replace(/(?!l)/g, '#');
console.log(result); // "#h#ell#o#"

比如把"123456789",變成"123,456,789"。

var result = "123456789".replace(/(?!^)(?=(\d{3})+$)/g, ',')
console.log(result); // "123,456,789"

三、正則表達(dá)式括號的作用

1. 分組和分支結(jié)構(gòu)

1.1 分組

/a+/匹配連續(xù)出現(xiàn)的“a”,而要匹配連續(xù)出現(xiàn)的“ab”時,需要使用/(ab)+/。

var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );  // ["abab", "ab", "ababab"]
1.2 分支結(jié)構(gòu)

在多選分支結(jié)構(gòu)(p1|p2)中,括號提供了子表達(dá)式的所有可能。

var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") ); // true
console.log( regex.test("I love Regular Expression") ); // true

2. 引用分組

2.1 提取數(shù)據(jù)

比如提取時間字符串中某些字符

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2019-04-12";
console.log( string.match(regex) ); // ["2019-04-12", "2019", "04", "12", index: 0, input: "2019-04-12"]

有修飾符g的時候返回結(jié)果不一樣

var regex = /(\d{4})-(\d{2})-(\d{2})/g;
var string = "2019-04-12";
console.log( string.match(regex) ); // [ '2019-04-12' ]
2.2 替換
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2019-04-12";
var result = string.replace(regex, "$2/$3/$1"); 
console.log(result);               // "04/12/2019"

3. 反向引用

比如要寫一個正則支持匹配如下三種格式:
2017-04-12
2019/04/12
2019.04.12

var string = "2019-04/12";
var string2 = "2019-04-12";
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
console.log( regex.test(string) ); // false
console.log( regex.test(string2) ); // true

\1,表示的引用之前的那個分組(-|/|.)。不管它匹配到什么(比如-),\1都匹配那個同樣的具體某個字符。同理\2和\3指代第二個和第三個分組。\10是表示第10個分組

4. 非捕獲分組

之前文中出現(xiàn)的分組,都會捕獲它們匹配到的數(shù)據(jù),以便后續(xù)引用,因此也稱他們是捕獲型分組。
如果只想要括號最原始的功能,但不會引用它,即,既不在API里引用, 也不在正則里反向引用。此時可以使用非捕獲分組(?:p)

var regex = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); // ["abab", "ab", "ababab"]

四、正則表達(dá)式回溯法原理

1. 沒有回溯的匹配

假設(shè)正則是/ab{1,3}c/,其可視化形式是:



而當(dāng)目標(biāo)字符串是"abbbc"時,就沒有所謂的“回溯”。其匹配過程是:


2. 有回溯的匹配

如果目標(biāo)字符串是"abbc",中間就有回溯。


圖中第5步有紅顏色,表示匹配不成功。此時b{1,3}已經(jīng)匹配到了2個字符“b”,準(zhǔn)備嘗試第三個時,結(jié)果發(fā)現(xiàn)接下來的字符是“c”。那么就認(rèn)為b{1,3}就已經(jīng)匹配完畢。
然后狀態(tài)又回到之前的狀態(tài)(即第6步,與第4步一樣),最后再用子表達(dá)式c,去匹配字符“c”。當(dāng)然,此時整個表達(dá)式匹配成功了。圖中的第6步,就是“回溯”。

3.常見的回溯形式

正則表達(dá)式匹配字符串的這種方式,叫回溯法。也稱試探法
基本思想是:從問題的某一種狀態(tài)(初始狀態(tài))出發(fā),搜索從這種狀態(tài)出發(fā)所能達(dá)到的所有“狀態(tài)”,當(dāng)一條路走到“盡頭”的時候(不能再前進(jìn)),再后退一步或若干步,從另一種可能“狀態(tài)”出發(fā),繼續(xù)搜索,直到所有的“路徑”(狀態(tài))都試探過。這種不斷“前進(jìn)”、不斷“回溯”尋找解的方法,就稱作“回溯法”。本質(zhì)上就是深度優(yōu)先搜索算法。其中退到之前的某一步這一過程,我們稱為“回溯”。即,嘗試匹配失敗時,接下來的一步通常就是回溯。
JS中正則表達(dá)式會產(chǎn)生回溯的地方有以下幾種:

3.1 貪婪量詞

貪婪量詞相關(guān)的。比如b{1,3},因為是貪婪的,嘗試可能的順序是從多往少的方向。
首先會嘗試"bbb",然后再看整個正則是否能匹配。不能匹配時,吐出一個"b",即在"bb"的基礎(chǔ)上,再繼續(xù)嘗試。如果不行,再吐出一個,再試。如果還不行就只能說明匹配失敗了。
當(dāng)多個貪婪量詞挨著存在,并相互有沖突時,因為是深度優(yōu)先搜索,會先下手為強!

var string = "12345";
var regex = /(\d{1,3}) (\d{1,3})/;
console.log( string.match(regex) ); // ["12345", "123", "45, index: 0, input: "12345"]

其中,前面的\d{1,3}匹配的是"123",后面的\d{1,3}匹配的是"45"

3.2惰性量詞

惰性量詞就是在貪婪量詞后面加個問號,表示盡可能少的匹配。比如:

var string = "12345";
var regex = /(\d{1,3}?)(\d{1,3})/;
console.log( string.match(regex) ); // ["1234", "1", "234", index: 0, input: "12345"]

其中\(zhòng)d{1,3}?只匹配到一個字符"1",而后面的\d{1,3}匹配了"234"。
雖然惰性量詞不貪,但也會有回溯的現(xiàn)象。比如正則是:



目標(biāo)字符串是"12345",匹配過程是:



為了整體匹配成,最后\d{1,3}?匹配的字符是"12",是兩個數(shù)字,而不是一個。
3.3 分支結(jié)構(gòu)

分支也是惰性的,比如/can|candy/,去匹配字符串"candy",得到的結(jié)果是"can",因為分支會一個一個嘗試,如果前面的滿足了,后面就不會再試驗了。
分支結(jié)構(gòu),可能前面的子模式會形成了局部匹配,如果接下來表達(dá)式整體不匹配時,仍會繼續(xù)嘗試剩下的分支。這種嘗試也可以看成一種回溯。
比如正則:


目標(biāo)字符串是"candy",匹配過程:


回溯法簡單總結(jié)就是,正因為有多種可能,所以要一個一個試。直到某一步時整體匹配成功了或者最后都試完了,發(fā)現(xiàn)整體匹配不成功。
貪婪量詞“試”的策略是:買衣服砍價。價錢太高了,便宜點,不行,再便宜點。
惰性量詞“試”的策略是:賣東西加價。給少了,再多給點行不,還有點少啊,再給點。
分支結(jié)構(gòu)“試”的策略是:貨比三家。這家不行,換一家吧,還不行,再換。

五、 正則表達(dá)式的拆分

1. 結(jié)構(gòu)和操作符

在正則表達(dá)式中,操作符都體現(xiàn)在結(jié)構(gòu)(由特殊字符和普通字符所代表的特殊整體)中。

JS正則表達(dá)式中的結(jié)構(gòu)有字符字面量、字符組、量詞、錨字符、分組、選擇分支、反向引用。
1.字面量,匹配一個具體字符,包括不用轉(zhuǎn)義的和需要轉(zhuǎn)義的。比如a匹配字符"a",又比如\n匹配換行符,又比如.匹配小數(shù)點。
2.字符組,匹配一個字符,可以是多種可能之一,比如[0-9],表示匹配一個數(shù)字。也有\(zhòng)d的簡寫形式。另外還有反義字符組,表示可以是除了特定字符之外任何一個字符,比如[^0-9],表示一個非數(shù)字字符,也有\(zhòng)D的簡寫形式。
3.量詞,表示一個字符連續(xù)出現(xiàn),比如a{1,3}表示“a”字符連續(xù)出現(xiàn)3次。另外還有常見的簡寫形式,比如a+表示“a”字符連續(xù)出現(xiàn)至少一次。
4.錨點,匹配一個位置,而不是字符。比如^匹配字符串的開頭,又比如\b匹配單詞邊界,又比如(?=\d)表示數(shù)字前面的位置。
5.分組,用括號表示一個整體,比如(ab)+,表示"ab"兩個字符連續(xù)出現(xiàn)多次,也可以使用非捕獲分組(?:ab)+。
6.分支,多個子表達(dá)式多選一,比如abc|bcd,表達(dá)式匹配"abc"或者"bcd"字符子串。
7.反向引用,比如\2,表示引用第2個分組。

涉及到的操作符有:
1.轉(zhuǎn)義符 \
2.括號和方括號 (...)、(?:...)、(?=...)、(?!...)、[...]
3.量詞限定符 {m}、{m,n}、{m,}、?、*、+
4.位置和序列 ^ 、$、 \元字符、 一般字符

  1. 管道符(豎杠)|
    這些操作符的優(yōu)先級從上至下,由高到低。

例子:
正則:/ab?(c|de)+|fg/
由于括號的存在,所以,(c|de
)是一個整體結(jié)構(gòu)。
在(c|de)中,注意其中的量詞,因此e是一個整體結(jié)構(gòu)。又因為分支結(jié)構(gòu)“|”優(yōu)先級最低,因此c是一個整體、而de是另一個整體。
同理,整個正則分成了 a、b?、(...)+、f、g。而由于分支的原因,又可以分成ab?(c|de*)+和fg這兩部分。其可視化結(jié)構(gòu)如下:

2. 元字符轉(zhuǎn)義問題

所謂元字符,就是正則中有特殊含義的字符。所有結(jié)構(gòu)里,用到的元字符總結(jié)如下:
^ $ . * + ? | \ / ( ) [ ] { } = ! : - ,
當(dāng)匹配上面的字符本身時,可以一律轉(zhuǎn)義。

例(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])$/
簡化結(jié)構(gòu) ((...).){3}(...),兩個(...)是一樣的結(jié)構(gòu)。表示匹配的是3位數(shù)字。因此整個結(jié)構(gòu)是:3位數(shù).3位數(shù).3位數(shù).3位數(shù)

(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])
它是一個多選結(jié)構(gòu),分成5個部分:
0{0,2}\d,匹配一位數(shù),包括0補齊的。比如9、09、009;
0?\d{2},匹配兩位數(shù),包括0補齊的,也包括一位數(shù);
1\d{2},匹配100到199;
2[0-4]\d,匹配200-249;
25[0-5],匹配250-255。
其可視化形式如下:


六、正則表達(dá)式的構(gòu)建

對正則的運用,最重要的就是如何針對問題,構(gòu)建一個合適的正則表達(dá)式。

1.構(gòu)建正則前提

1.是否能使用正則:比如匹配這樣的字符串:1010010001....雖然很有規(guī)律,但是只靠正則就是無能為力。
2.是否有必要使用正則,能用字符串API解決的簡單問題,就不該使用正則。比如,從日期中提取出年月日,雖然可以使用正則,但也可以使用字符串的split方法來做。
3.是否有必要構(gòu)建一個復(fù)雜的正則。

2.效率

正則表達(dá)式的運行分為如下的階段:
1.編譯
2.設(shè)定起始位置
3.嘗試匹配
4.匹配失敗的話,從下一位開始繼續(xù)第3步
5.最終結(jié)果:匹配成功或失敗
以代碼為例,如下:

var regex = /\d+/g;
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
// => 0 ["123", index: 0, input: "123abc34def"]
// => 3 ["34", index: 6, input: "123abc34def"]
// => 8 null
// => 0 ["123", index: 0, input: "123abc34def"]

1.當(dāng)生成一個正則時,引擎會對其進(jìn)行編譯。報錯與否出現(xiàn)這這個階段
2.當(dāng)嘗試匹配時,需要確定從哪一位置開始匹配。一般情形都是字符串的開頭,即第0位。但當(dāng)使用test和exec方法,且正則有g(shù)時,起始位置是從正則對象的lastIndex屬性開始。因此第一次exec是從第0位開始,而第二次是從3開始的。設(shè)定好起始位置后,就開始嘗試匹配了。
3.比如第一次exec,從0開始,去嘗試匹配,并且成功地匹配到3個數(shù)字。此時結(jié)束時的下標(biāo)是2,因此下一次的起始位置是3。第二次,起始下標(biāo)是3,但第3個字符是“a”,并不是數(shù)字。但此時并不會直接報匹配失敗,而是移動到下一位置,即從第4位開始繼續(xù)嘗試匹配,但該字符是b,也不是數(shù)字。再移動到下一位,c仍不是數(shù)字,再移動一位是數(shù)字3,此時匹配到了兩位數(shù)字34。此時,下一次匹配的位置是d的位置,即第8位。
4.第三次,是從第8位開始匹配,直到試到最后一位,也沒發(fā)現(xiàn)匹配的,因此匹配失敗,返回null。同時設(shè)置lastIndex為0,即,如要再嘗試匹配的話,需從頭開始。
可以看出,匹配會出現(xiàn)效率問題,主要出現(xiàn)在上面的第3階段和第4階段。因此,主要優(yōu)化手法也是針對這兩階段的。

優(yōu)化:
1.使用具體型字符組來代替通配符,來消除回溯。
2.使用非捕獲型分組。
3.獨立出確定字符 。例如/a+/,可以修改成/aa*/。因為后者能比前者多確定了字符a。這樣會在第四步中,加快判斷是否匹配失敗,進(jìn)而加快移位的速度。
4.提取分支公共部分。比如/this|that/,修改成/th(?:is|at)/。這樣做,可以減少匹配過程中可消除的重復(fù)。
5.減少分支的數(shù)量,縮小它們的范圍。

七、 正則表達(dá)式編程

1. 正則表達(dá)式的四種操作

正則表達(dá)式是匹配模式,不管如何使用正則表達(dá)式,萬變不離其宗,都需要先“匹配”。有了匹配這一基本操作后,才有其他的操作:驗證、切分、提取、替換。

1.1 驗證 test

驗證是正則表達(dá)式最直接的應(yīng)用,比如表單驗證。
比如,判斷一個字符串中是否有數(shù)字.

var regex = /\d/;
var string = "abc123";
console.log( regex.test(string) ); //true
1.2 切分 (split直接切更直接)
1.3 提取 match

比如:以日期為例,提取出年月日

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2019-04-12";
console.log( string.match(regex) ); // ["2019-04-12", "2019", "04", "12", index: 0, input: "2017-04-12"]
1.4 替換 replace

比如:把日期格式,從yyyy-mm-dd替換成yyyy/mm/dd:

var string = "2019-04-12";
var day = new Date( string.replace(/-/g, "/") );
console.log( day ); //(中國標(biāo)準(zhǔn)時間)

2.相關(guān)API注意要點

用于正則操作的方法,共有6個: String#search、 String#split、String#match、String#replace、RegExp#test、RegExp#exec

1.match返回結(jié)果的格式問題

match返回結(jié)果的格式,與正則對象是否有修飾符g有關(guān)。
沒有g(shù),返回的是標(biāo)準(zhǔn)匹配格式,即,數(shù)組的第一個元素是整體匹配的內(nèi)容,接下來是分組捕獲的內(nèi)容,然后是整體匹配的第一個下標(biāo),最后是輸入的目標(biāo)字符串。
有g(shù),返回的是所有匹配的內(nèi)容。
當(dāng)沒有匹配時,不管有無g,都返回null。

2.test整體匹配時需要使用^和$

因為test是看目標(biāo)字符串中是否有子串匹配正則,即有部分匹配即可。如果,要整體匹配,正則前后需要添加開頭和結(jié)尾:

console.log( /123/.test("a123b") );
// => true
console.log( /^123$/.test("a123b") );
// => false
console.log( /^123$/.test("123") );
// => true

總結(jié)

正則有回溯的過程,匹配效率肯定低一些。但是好在編譯快而且也有趣,所以比較流行。
一般情況下,針對某問題能寫出一個滿足需求的正則,基本上就可以了。至于準(zhǔn)確性和效率方面的追求,純屬看個人要求了,我覺得夠用就行了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容