正則簡(jiǎn)介
參考資料
正則表達(dá)式(regular expression),字面意思是描述規(guī)則的表達(dá)式,這里的規(guī)則指搜索字符串(即在字符串中搜索子串)的規(guī)則,你可以用正則描述你搜索字符串子串時(shí)候的規(guī)則,通過(guò)正則搜索時(shí)候會(huì)按照描述的規(guī)則進(jìn)行匹配。
為什么我們需要搜索字符串子串(即正則匹配)呢?
- 判斷一個(gè)字符串中是否有數(shù)字【驗(yàn)證】
- 將字符串以數(shù)字為分隔符分成多段【切分】
- 把“2021-12-16”中的年月日提取出來(lái)【提取】
- 把字符串中的數(shù)字都替換成16進(jìn)制【替換】
我們?cè)谌粘I(yè)務(wù)開(kāi)發(fā)中經(jīng)常會(huì)處理字符串,而在處理字符串時(shí)候可能需要按照某種規(guī)則搜索字符串子串。\
正則語(yǔ)法
通常我們使用自然語(yǔ)言描述我們查找字符串子串的需求時(shí)候會(huì)如何表達(dá)呢?
- abab【精確匹配】
- 3個(gè)數(shù)字、4個(gè)字母【元字符】
- 連續(xù)2個(gè)ab【量詞】
- 以a開(kāi)頭、以b結(jié)尾【位置】
- 1個(gè)1~5的數(shù)字【字符組】
- 大小寫(xiě)無(wú)關(guān)【修飾符】
- 盡量多匹配/盡量少匹配【貪婪/惰性】
- 3個(gè)數(shù)字重復(fù)2次【分組反向引用】
- 3個(gè)數(shù)字或2個(gè)字母【分支】
可以看到我們?cè)诿枋鏊阉鞯淖哟囊?guī)則時(shí)候,會(huì)有不同的需求(精確匹配、模糊匹配、重復(fù)次數(shù)、盡量多/盡量少匹配)等等,這些需求正則語(yǔ)法都支持通過(guò)特定的語(yǔ)法表達(dá)。
匹配位置和匹配字符
正則表達(dá)式不是匹配字符就是匹配位置
因此當(dāng)我們根據(jù)需求寫(xiě)正則時(shí)候,先將需求轉(zhuǎn)化為匹配字符/匹配正則的描述,然后根據(jù)描述寫(xiě)正則。
精確匹配
正則表達(dá)式中可以使用具體字符來(lái)構(gòu)造要匹配的子串
/abc/
修飾符
i(ignore)
不區(qū)分大小寫(xiě)
g(global)全局匹配
默認(rèn)正則從左到右匹配,匹配到第一個(gè)返回
增加g修飾后,會(huì)將所有匹配的都記錄下來(lái),直到字符串結(jié)束
m(multiline)多行匹配
^和$匹配行首和行位,如果不加m修飾,整個(gè)作為一行,如果加了m修飾,換行符\n分割為多行。通常使用m修飾都會(huì)配合g一起使用。
'1\n'.match(/1$/)
'1\n'.match(/1$/m)
'1\n2'.match(/^2/)
'1\n2'.match(/^2/m)
元字符、元字符轉(zhuǎn)義
元字符是擁有特殊含義的字符,每個(gè)元字符代表一類(lèi)字符(數(shù)字、字母)或者位置(開(kāi)頭、結(jié)尾、單詞邊界)
字符組
當(dāng)需要表達(dá)一個(gè)在某個(gè)集合內(nèi)的字符時(shí)候,例如“1個(gè)1~5的數(shù)字”,語(yǔ)法是中括號(hào)[1,2,3,4,5],也可以用范圍[1-5],范圍表示ASCii碼對(duì)應(yīng)的碼點(diǎn)范圍,如果范圍不對(duì)(下限在ASCii中小于上限)則會(huì)報(bào)錯(cuò)。
量詞
元字符指擁有特殊含義的字符,量詞指用來(lái)修飾字符、元字符和分組的標(biāo)志符。
元字符和量詞都是正則的重要組成部分,這是從語(yǔ)法結(jié)構(gòu)來(lái)進(jìn)行分類(lèi)的。從使用場(chǎng)景的角度,更好地劃分方式是分為匹配位置和匹配字符。
匹配位置
位置指字符之間的部分,包括開(kāi)頭和結(jié)尾。字符之間只有一個(gè)位置,位置和位置不會(huì)相鄰。
- 開(kāi)頭^
- 結(jié)尾$
- 單詞邊界 \b
- 非單詞邊界 \B
- (?=p)后面匹配到p的位置
- (?!p)后面沒(méi)有匹配到p的位置
- (?<=p)前面匹配到p的位置
- (?<!p)前面未匹配到p的位置
連續(xù)的位置匹配子表達(dá)式,在正則匹配時(shí)候是“與”的關(guān)系。
'abc'.match(/(?=b)(?<=a)/)
'abc'.match(/(?<=a)(?=b)/)
注意這個(gè)括號(hào)不是分組
'abc'.match(/(?=b)(?<=a)(b)/) // 可以看到位置沒(méi)有作為匹配子表達(dá)式
貪婪、惰性
貪婪匹配指盡可能多地匹配,默認(rèn)是貪婪匹配
'aaaa'.match(/a+/g)
'aaaa'.match(/(a+)(a+)/g) // 第一個(gè)括號(hào)先盡可能多匹配,然后后面的再匹配
'aaaa'.match(/(a+)+/g) // 括號(hào)內(nèi)量詞先貪婪匹配,然后外面量詞再匹配
惰性匹配盡可能少地匹配,在量詞后面加“?”即可實(shí)現(xiàn)惰性匹配
'aaaa'.match(/a+?/g) // ["a", "a", "a", "a"]
'aaaa'.match(/(a+?)(a+?)/g) // ["aa", "aa"]
'aaaa'.match(/(a+?)+?/g) // ["a", "a", "a", "a"]
分支
分支相當(dāng)于編程語(yǔ)言中的或運(yùn)算
/(a|b)c/g.test('ac') // true
/(a|b)c/g.test('bc') // true
分組
分組有很多作用(作為整體進(jìn)行匹配、緩存分組、反向引用),可以將多個(gè)子表達(dá)式打包,這樣可以讓量詞作用于多個(gè)子表達(dá)式組成的整體。
分組的使用
/(ab)+/g
正則表達(dá)式匹配到之后,會(huì)緩存匹配到的每個(gè)分組,用于引用、替換和提取
提取
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
\
引用
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
string.match(regex); // 或者regex.test(string);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
替換
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result);
// => "06/12/2017"
反向引用
可以在正則內(nèi)部引用前面匹配到的分組,因此叫“反向引用”。
var regex = /\d{4}(-|/|.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
非捕獲分組
如果不希望分組被捕獲,可以使用這種語(yǔ)法
(?:p)
正則回溯原理
正則匹配的大致過(guò)程是(假定使用了g修飾符)
- 先取出正則的子表達(dá)式進(jìn)行匹配(默認(rèn)貪婪匹配)
- 如果能夠匹配上,則對(duì)下一個(gè)子表達(dá)式進(jìn)行匹配(可能是下個(gè)元字符,也可能是加上量詞,例如/(a+){2,}/g 中,“(a+)”是一個(gè)子表達(dá)式,先對(duì)其匹配,如果匹配成功則繼續(xù)對(duì)(a+){2,}進(jìn)行匹配;再比如/ab/g,先匹配a,匹配上的話(huà)繼續(xù)匹配b)
- 如果匹配不上,則開(kāi)始回溯,回溯到上一個(gè)狀態(tài)繼續(xù)匹配。如果回溯到第一個(gè)子表達(dá)式也沒(méi)有匹配,則從下一個(gè)字符開(kāi)始匹配。為什么需要回溯呢?因?yàn)橛行┠:ヅ淇梢杂卸喾N匹配方法,只有嘗試過(guò)所有的情況都沒(méi)有匹配成功,才能認(rèn)為匹配失敗。例如/ab{1,3}c/g.test('abbc'),{1,3}可以是1個(gè)~3個(gè)。
- 如果正則所有子表達(dá)式匹配完,則匹配成功,這時(shí)候如果字符串沒(méi)有結(jié)束,則繼續(xù)匹配(如果沒(méi)有g(shù)修飾符,則停止)
- 如果字符串結(jié)束,正則未把所有子表達(dá)式匹配完,則失敗
正則使用回溯算法嘗試所有可能情況進(jìn)行匹配?;厮葸^(guò)程可能比較消耗性能。
正則性能優(yōu)化
盡量精確
正則表達(dá)式在匹配時(shí)候,如果表達(dá)式比較模糊就會(huì)存在多個(gè)狀態(tài),就可能會(huì)出現(xiàn)很多回溯情況。因此應(yīng)該盡量精確,減少可能的狀態(tài),從而減少回溯
`"abc"de`.match(/".*?"/g) // 由于惰性匹配,因此匹配到第二個(gè)引號(hào)之前會(huì)進(jìn)行回溯。
`"abc"de`.match(/"[^"]"/g) // 沒(méi)有回溯
使用非捕獲分組
因?yàn)榉纸M可能會(huì)被緩存,如果不需要對(duì)分組進(jìn)行提取、引用、反向引用等操作,則不需要捕獲分組。
獨(dú)立出確定字符
/a+/ // bad
/aa*/ // good
減少分支匹配損耗
提取分支公共部分,可以減少回溯損耗
/^abc|^def/ // bad
/^(?:abc|def)/ // good
/red|read/ // bad
/rea?d/ // good
閱讀正則
正則語(yǔ)法結(jié)構(gòu)中的組成元素。
字符字面量、字符組、量詞、錨字符、分組、選擇分支、反向引用。
在閱讀正則表達(dá)式時(shí)候,先拆分分支,再對(duì)每個(gè)分支分析,每個(gè)分支從左至右,劃分成一個(gè)個(gè)的子表達(dá)式,根據(jù)元字符+量詞理解子表達(dá)式描述的規(guī)則,子表達(dá)式中還可能有分支,再遞歸地拆分和分析不同分支即可。
正則編程
test、split、search、match、replace\
驗(yàn)證
// test返回是否成功匹配到子串
/\d+/g.test('1234'); // true
檢索
// search返回匹配到的子串的index
'1234abc5678'.search(/\d+/g) // 1
提取
match方法匹配到則返回匹配結(jié)果,否則返回null。
在全局模式下match返回所有匹配到的子串?dāng)?shù)組。
在非全局模式下,match返回一個(gè)數(shù)組,第一個(gè)元素是匹配到的子串,后面元素是分組。返回的結(jié)果中還包括輸入字符串、匹配到的子串的index等信息。
在全局檢索模式下,match() 即不提供與子表達(dá)式匹配的文本的信息,也不聲明每個(gè)匹配子串的位置。如果您需要這些全局檢索的信息,可以使用 RegExp.exec()。
'1a2 3b4'.match(/(\d+)(\w+)(\d+)/g) // ["1a2", "3b4"]
'1a2 3b4'.match(/(\d+)(\w+)(\d+)/) // ["1a2", "1", "a", "2", index: 0, input: "1a2 3b4", groups: undefined]
替換
"2017-06-12".replace(/(\d{4})-(\d{2})-(\d{2})/, function ($0, $1, $2, $3) {
return `${$2}/${$1}/${$3}`;
})
// "06/2017/12"
總結(jié)
正則表達(dá)式中的主要組成元素包括 字符字面量、字符組、量詞、錨字符、分組、選擇分支、反向引用。
這里需要注意一些符合,它們?cè)诓煌瑘?chǎng)景有不同含義
括號(hào)的作用
- 分組
- 非捕獲
- 位置
問(wèn)號(hào)的作用
- 惰性
- 量詞
- 位置