正則表達(dá)式(Regular Expression,簡稱regex)作為一種工具,它擅長的領(lǐng)域是文本處理。最基本的用途是查找(search/match)和替換(replace)特定的文本內(nèi)容,在此之上可以延伸到:
驗(yàn)證(validation)內(nèi)容是否符合指定的規(guī)則
提取(Extract)指定的內(nèi)容
1 JavaScript中正則表達(dá)式
JavaScript中為正則表達(dá)式提供了專門的RegExp類型,可以使用new RegExp()實(shí)例化一個(gè)正則表達(dá)式,不過通常我們使用如下的語法創(chuàng)建一個(gè)正則表達(dá)式。
let expression = /pattern/flags;
/pattern/表示正則表達(dá)式的匹配模式,flags表示該表達(dá)式支持的標(biāo)志。
1.1 正則表達(dá)式的標(biāo)志
一個(gè)正則表達(dá)式可以帶一個(gè)或多個(gè)flag,flag有如下取值:
- g 表示全局模式(global),默認(rèn)情況下,js的正則表達(dá)式在找到第一個(gè)匹配項(xiàng)時(shí),就會停止匹配,加上g標(biāo)志后,表達(dá)式會查找目標(biāo)字符串中所有的匹配項(xiàng)。
- i 忽略大小寫
- m 多行模式
flag可以組合使用,比如/exp/gi表示exp表達(dá)式用于全局匹配,且忽略大小寫。
1.2 正則表達(dá)式的用法
我們先不看正則表達(dá)式是怎么來的,先看它是怎么用的。使用RegExp主要是其實(shí)例方法以及外部支持RegExp的方法。后續(xù)的章節(jié)中,我們以patt表示表達(dá)式,text表示目標(biāo)字符串。
- RegExp.exec()方法,exec是RegExp的實(shí)例方法,用法是
patt.exec(text),在text中查找匹配patt的內(nèi)容,并以數(shù)組的方式返回第一個(gè)匹配的且未返回的結(jié)果,當(dāng)無匹配時(shí)返回null。注意前面這句話中的"第一個(gè)"和"未返回的"我打了重點(diǎn)符號,
let text="112233";
let patt=/(\d)\1/g;
let matchs = patt.exec(text);
//["11", "1", index: 0, input: "112233", groups: undefined]
matchs = patt.exec(text);
//["22", "2", index: 2, input: "112233", groups: undefined]
matchs = patt.exec(text);
//["33", "3", index: 4, input: "112233", groups: undefined]
matchs = patt.exec(text);
//null
本列中/(\d)\1/g表達(dá)式用來查找連續(xù)相同的數(shù)字,因?yàn)檫€沒有講表達(dá)式,所以這里不需要知道表達(dá)式的細(xì)節(jié)。patt.exec(text)查找text中匹配patt表達(dá)式的內(nèi)容,第一次返回了一個(gè)數(shù)組并賦值給matchs,這個(gè)數(shù)組有index和input屬性,index表示當(dāng)前匹配的內(nèi)容在text中的起點(diǎn)位置,input表示目標(biāo)字符串(及text文本內(nèi)容)。matchs[0]表示當(dāng)前匹配的內(nèi)容,這里匹配的內(nèi)容是11,如果表達(dá)式產(chǎn)生了分組(還沒講分組),會在matchs中添加分組內(nèi)容,這里是1就是分組內(nèi)容。
再次執(zhí)行exec方法,返回第二個(gè)匹配的內(nèi)容數(shù)組,直到返回為null,表示所有的匹配內(nèi)容都已提取完。
如果表達(dá)式?jīng)]有設(shè)置全局模式g,那么每次執(zhí)行patt.exec(text)時(shí),都會從頭開始匹配,及每一次執(zhí)行exec()方法就會返回第一次的內(nèi)容。
-
RegExp.test()方法用于測試patt與text是否匹配,它只會返回目標(biāo)字符串是否與表達(dá)式匹配,不會返回文本內(nèi)容,這個(gè)方法一般用于驗(yàn)證用戶的輸入。
let patt = /^d{3}-d{8}$/; let phoneNumber = "010-88888888"; let flag = patt.test(phoneNumber); //true上例中,patt表達(dá)式用于驗(yàn)證電話號碼的格式,而使用test方法,只返回目標(biāo)字符串是否為匹配,而不返回具體的內(nèi)容。
-
string.match()方法是string的實(shí)例方法,與RegExp的方法類似,返回匹配的字符集合,注意,這里不包含組信息。
let text="11112223455"; let patt=/(\d)\1/g; text.match(patt); // ["11", "11", "22", "55"] -
string.search()方法,返回第一個(gè)匹配的字符串的位置,如果沒有匹配的內(nèi)容,返回-1。
let text="12345"; let patt=/(\d)\1/g; text.search(patt); //-1 沒有任何匹配的內(nèi)容 text = "1233445"; text.search(patt); //2 返回字符串的第一個(gè)匹配內(nèi)容的位置,這里是33,而后面44的位置不會返回 -
string.replace()方法是string的實(shí)例方法,用于替換指定的字符串,返回一個(gè)替換完成的字符串,相對之前的方法要復(fù)雜些,它接收兩個(gè)參數(shù):
- 第一個(gè)參數(shù)時(shí)要匹配的內(nèi)容,是一個(gè)RegExp對象或字符串。
- 第二個(gè)參數(shù)要替換的內(nèi)容,可以是字符串或函數(shù)。
當(dāng)?shù)谝粋€(gè)參數(shù)是字符串時(shí),只能替換第一個(gè)匹配的內(nèi)容,如果要全局替換,需要使用全局模式的正則表達(dá)式。
let text = "cat,bat,sat,fat"; let tmp = text.replace("at","ond"); //text "cat,bat,sat,fat" //tmp "cond,bat,sat,fat"第一個(gè)參數(shù)使用字符串時(shí),只能替換第一個(gè)匹配的內(nèi)容,后面的bat并沒有被替換。
let text = "cat,bat,sat,fat"; let tmp = text.replace(/at/g,"ond"); // tmp "cond,bond,sond,fond"第一個(gè)參數(shù)使用帶全局標(biāo)志(g)的正則表達(dá)式,會替換全部的匹配的內(nèi)容。替換內(nèi)容還可以使用特殊的字符(如:$n,這個(gè)叫回溯引用),引用前面匹配的結(jié)果。
let text = "01088888888,01077777777,01066666666"; let patt = /(\d{3})(\d{8})/g let tmp = text.replace(patt,"$1-$2"); //tmp "010-88888888,010-77777777,010-66666666"上面使用特殊字符引
$n引用了匹配結(jié)果的分組信息,并將匹配的結(jié)果格式化為自定的樣式。$1引用了第一個(gè)分組信息(\d{3}),$2引用了第二個(gè)分組信息(\d{8})。replace方法的第二個(gè)參數(shù)是Function時(shí),replace會向該函數(shù)傳遞三個(gè)參數(shù),分別是,匹配的內(nèi)容,匹配項(xiàng)的位置,原生字符串。使用function可以做更精細(xì)的替換,這里不深入講解。
string.split()方法,該方法用指定的分隔符將指定的字符串分隔為多個(gè)子字符串。這個(gè)分隔符可以是字符串也可以是RegExp對象。使用RegExp作為分隔符時(shí),該方法在不同瀏覽器存在差異。
2 編寫正則表達(dá)式
正則表達(dá)式的符號較多,可分為普通字符,轉(zhuǎn)義字符,特殊符號,定位符,限定符與組共6大類。
2.1 普通字符
普通字符代表字符本身,如:
let text = "cat,bat";
let patt = /at/g;
let tmp = text.match(patt);
//tmp ["at", "at"]
目標(biāo)字符串text,會找到兩處與patt相關(guān)的內(nèi)容。
2.2 轉(zhuǎn)義字符
有些普通字符轉(zhuǎn)義后代表特殊的含義,比如:w在轉(zhuǎn)義后\w表示任意的0-9,a-z,A-Z以及_下劃線。
| 轉(zhuǎn)義字符 | 說明 |
|---|---|
| \w | 匹配任一0-9,a-z,A-Z,_字符 |
| \W | 匹配任一非0-9,a-z,A-Z,_的字符 |
| \d | 匹配任一0-9的數(shù)字 |
| \D | 匹配任一非0-9的數(shù)字 |
| \s | 匹配任一空格,tab和換行符\n |
| \S | 匹配任一非空格,tab和換行符\n的字符 |
轉(zhuǎn)義字符有很多,這里我只列出了常用的3個(gè)w、d、s,他們的大寫是對應(yīng)的含義取反。
let text = "要出差3天,還是1周";
let patt = /\d天/g;
let tmp = patt.exec(text);
// ["3天", index: 3, input: "要出差3天,還是1周", groups: undefined]
上面的例子使用\d表示匹配數(shù)字,后面跟普通字符"天",這樣就能從上面的一段文字中提取出幾天,注意:雖然1也是數(shù)字,但是它不符合表達(dá)式,因?yàn)楸磉_(dá)式是數(shù)據(jù)后面跟著"天"。
2.3 特殊符號
普通字符代表字符本身,有些普通字符通過轉(zhuǎn)義代表了另一類含義。而有些字符不需要轉(zhuǎn)義就代表了與本身不同的含義,如果要表示這些字符本身的含義,還得通過轉(zhuǎn)義符來轉(zhuǎn)義。這些字符就是我們說的特殊字符:
| 特殊字符 | 說明 |
|---|---|
| \ | 轉(zhuǎn)義符,如果要表示\本身,還得通過轉(zhuǎn)義符,寫成\\,后面的特殊字符同理,不在累述 |
| . | 表示任意字符,但不包括換行符 |
| | | 表示或 |
| [ ] | 表示匹配中括號中的任意一個(gè)字符,如[ab],表示匹配a或b,與|的含義相同 |
| [^ ] | 表示不能匹配中括號中的任意一個(gè)字符 |
.表示任意字符,但不包括換行符,如果我們要代表任意字符可以使用前面的\s和\S的組合[\s\S]。
2.4 定位符
定位符比較特別,前面我們講的不管是普通字符、還是特殊字符它匹配的要么是單個(gè)字符,要么就是字符集中的某個(gè)字符,但終歸說來就是匹配的字符,而定位符卻是匹配的位置,注意是位置而不是字符。如果要表示字符本身,也需要使用轉(zhuǎn)義符。
| 定位符 | 說明 |
|---|---|
| ^ | 匹配字符串開始位置,注意^與[^ ]中的^不同,[^ ]中^與[ 是一個(gè)整體的,表示非的意思 |
| $ | 匹配字符串的結(jié)束位置 |
| \b | 表示單詞的邊界 |
| \B | 表示非單詞邊界 |
^和$表示字符串的開始和結(jié)束位置,這個(gè)很好理解。而\b表示單詞的邊界,我們知道單詞是以空格隔開的,那么單詞的邊界就是一個(gè)單詞的開始或結(jié)尾處,\B表示非單詞的邊界,即非單詞的開始或結(jié)尾處。例如:
let text = "element release bele";
let patt = /ele/g;
let tmp = patt.exec(text);
//["ele", index: 0, input: "element release", groups: undefined]
tmp = patt.exec(text);
//["ele", index: 9, input: "element release", groups: undefined]
tmp = patt.exec(text);
//["ele", index: 17, input: "element release bele", groups: undefined]
tmp = patt.exec(text);
//null
通過普通字符能夠找到3個(gè)匹配的字符,位置分別在0,9和17。下面我們看看通過\b和\B查找。
let text = "element release bele";
//使用\b定位在單詞的開始位置
let patt = /\bele/g;
patt.exec(text);
//["ele", index: 0, input: "element release", groups: undefined]
//使用\B定位ele元素不在開始或結(jié)束的任意位置
patt = /\Bele/g;
patt.exec(text);
//["ele", index: 9, input: "element release", groups: undefined]
//使用\b定位在單詞的結(jié)束位置
patt = /ele\b/g;
tmp = patt.exec(text);
//["ele", index: 17, input: "element release bele", groups: undefined]
使用\B能找到不是以ele開頭或結(jié)尾的內(nèi)容,\b表示單詞的邊界,邊界分為開始處和結(jié)尾處,\b如果加在前面代表匹配單詞的開始處\bele,加在后面表示匹配單詞的結(jié)尾處ele\b
2.5 限定符
限定符表示重復(fù)的次數(shù),與其它的類搭配使用。如果要表示字符本身的含義,也需要使用轉(zhuǎn)義符
| 限定符 | 說明 |
|---|---|
| + | 表示+前面的內(nèi)容,重復(fù)1次到N次 |
| * | 表示*前面的內(nèi)容,重復(fù)0次到N次 |
| ? | 表示?前面的內(nèi)容,重復(fù)0次或1次 |
| {n} | 表示{}前面的內(nèi)容,重復(fù)n次 |
| {n,m} | 表示{}前面的內(nèi)容,重復(fù)n次到m次 |
| {n,} | 表示{}前面的內(nèi)容,至少重復(fù)n次 |
| ? | 限定符之后,表示懶惰模式 |
上面出現(xiàn)了兩個(gè)?號,表示?有2種用法,一種是在字符或分組之后,表示重復(fù)0次或1次。第2中是在限定符之后,表示懶惰模式。
通常正則表達(dá)式在加了限定符后,默認(rèn)都會匹配盡可能多的字符,這種叫做貪婪模式,比如:
let text = "gooooogle;
// 貪婪模式
let patt = /o+/g;
text.match(patt);
//["ooooo"]
// 懶惰模式
let patt = /o+?/g;
text.match(patt);
//["o", "o", "o", "o", "o"]
這里的限定符是+,表示前面的o可以重復(fù)1-n次,在貪婪模式下,表達(dá)式會盡可能多的匹配,對應(yīng)到本例就是5個(gè)連續(xù)的o,所以,返回了一個(gè)"ooooo"字符串。而在懶惰模式下,表達(dá)式會匹配盡可能少的字符,對應(yīng)到本例就是當(dāng)匹配了一個(gè)o時(shí),表達(dá)式就返回了,因?yàn)楸磉_(dá)式是全局匹配,所以返回了5次o。
2.6 分組
分組使用()來表示,它將括號內(nèi)的表達(dá)式看做一個(gè)整體??梢詫⒎纸M內(nèi)的表達(dá)式看做整個(gè)表達(dá)式的一個(gè)子表達(dá)式。子表達(dá)式還可以嵌套子表達(dá)式。
2.6.1 分組的限定
我們前面做限定(如+,*,?,{n,m})時(shí),都是針對限定符前面的單個(gè)字符,比如https?表示的是對s這個(gè)字符重復(fù)0或1次,代表的是http和https,分組的一個(gè)用途是將字符組成一個(gè)整體,這樣就可以對這個(gè)整體(分組)做限定。
let text = "haha"
let patt = /haha/g
let tmp = text.match(patt);
// ["haha"]
patt = /(ha)+/g
tmp = text.match(patt);
// ["haha"]
/(ha)+/g是對ha分組做+限定。
2.6.2 回溯引用
回溯引用的含義是,在同一個(gè)表達(dá)式中,后面的部分可以用\n的方式引用前面分組匹配的結(jié)果。正則表達(dá)式會為每個(gè)分組從1開始順序的分配一個(gè)編號。結(jié)合前面的\n方式,當(dāng)n=1,表示第一個(gè)分組的內(nèi)容,以此類推。比如我們要查找html文檔中所有的h標(biāo)簽,可以使用如下表達(dá)式:
let patt = /<(h[1-6])[\S\s]*?<\/\1>/ig
/<(h[1-6])[\S\s]*?<\/\1>/ig表達(dá)式使用全局標(biāo)記g和忽略大小寫標(biāo)記i,<是普通字符,(h[1-6])作為一個(gè)分組,能匹配h1-h6的內(nèi)容,這個(gè)分組的編號為1,表達(dá)式的后面部分使用\1引用這個(gè)分組的內(nèi)容,[\S\s]*表示任一空格和非空格字符重復(fù)0到n次,及表示的是任意行數(shù)的全部內(nèi)容,注意.*表示的是單行內(nèi)容,因?yàn)?code>.不包含換行符,所以重復(fù)n次,也只能是一行的內(nèi)容,這里的?加載限定符之后,表示懶惰模式。
回溯引用還可以用于高級的替換場景,這個(gè)需要結(jié)合具體的腳本語言來實(shí)現(xiàn),下面以js為例
let text = "<p>如有需要,請發(fā)郵件到 xxx@gmail.com </p>";
let patt = /(\b\w+[\w\.]*@[\w+\.]+\.\w+\b)/g;
let tmp = text.replace(patt,"<a href='mailto:$1'>$1</a>");
//<p>如有需要,請發(fā)郵件到 <a href='mailto:xxx@gmail.com'>xxx@gmail.com</a> </p>
上面的場景是將郵箱地址替換為可點(diǎn)擊的鏈接,在js中使用$符號來獲取回溯引用。
2.7 前后查找
前后查找(Lookaround)有時(shí)又叫零寬度匹配,它和定位符(^,$,\b,\B)有點(diǎn)像,都是匹配的是位置,與定位符不同的是,定位符的位置是預(yù)先定義好的,要么是字符串的開始或結(jié)尾,要么就是單詞的邊界與非邊界。而前后查找可以通過子表達(dá)式(定位條件)來定位位置。
與單詞邊界\b相似,在前后查找中,定位的位置(表達(dá)式所匹配的內(nèi)容)也分為前與后,及向前查找與向后查找
| 類型 | 語法格式 | 說明 |
|---|---|---|
| 向前查找 | (?=exp) | 返回表達(dá)式exp所匹配內(nèi)容之前的位置 |
| 向后查找 | (?<=exp) | 返回表達(dá)式exp所匹配內(nèi)容之后的位置 |
前后查找也可以組合起來使用,比如要取標(biāo)簽p中的內(nèi)容:
let text = "<p>如有需要,請發(fā)郵件到 <a href='mailto:xxx@gmail.com'>xxx@gmail.com</a> </p>";
let patt =/(?<=<p>)[\s\S]*?(?=<\/p>)/g
let tmp = text.match(patt);
//["如有需要,請發(fā)郵件到 <a href='mailto:xxx@gmail.com'>xxx@gmail.com</a> "]
上面組合使用了向前查找和向后查找,(?<=<p>)是向后查找,返回條件<p>后面的位置,(?=<\/p>)是向前查找,返回</p>結(jié)束標(biāo)簽前面的位置。[\s\S]*?中間采用懶惰模式匹配兩個(gè)位置(及p標(biāo)簽)之間的內(nèi)容。
let text = "1234567890";
let patt = /\B(?=(\d{3})+$)/g
let tmp = text.replace(patt,",");
//1,234,567,890
text = "1234567890.54";
patt = /\B(?=(\d{3})+\.)/g
tmp = text.replace(patt,",");
//"1,234,567,890.54"
上例是給數(shù)字按3位加逗號,使用了向前查找表達(dá)式,整個(gè)表達(dá)式開始使用了\B,表示位置只會出現(xiàn)在字符串的非邊界上,防止逗號加在整個(gè)字符串的外面。(?=(\d{3})+$)是向前查找,返回滿足(\d{3})+$內(nèi)容前面的位置,注意表達(dá)式最后的定位符是$,可以理解為結(jié)束位置是字符串的最后。當(dāng)有小數(shù)點(diǎn)的時(shí)候,結(jié)束位置應(yīng)該換為\.。
前后查找是查找滿足條件的內(nèi)容的前或后的位置。還有一種是查找不滿足條件的內(nèi)容的前后位置,稱為負(fù)向前后查找,這種用的比較少。
| 類型 | 語法格式 | 說明 |
|---|---|---|
| 負(fù)向前查找 | (?!exp) | 返回不能匹配表達(dá)式exp內(nèi)容之前的位置 |
| 負(fù)向后查找 | (?<!exp) | 返回不能匹配表達(dá)式exp內(nèi)容之后的位置 |
2.8 正則表達(dá)式中的條件
在處理復(fù)雜的業(yè)務(wù)邏輯時(shí),可能需要在正則表達(dá)式中嵌入條件,及只有滿足特定條件時(shí),才執(zhí)行相應(yīng)的表達(dá)式。嵌入條件語法使用?,它有兩種情況:
- 根據(jù)回溯引用來進(jìn)行條件處理
- 根據(jù)前后查找來進(jìn)行條件處理
| 類型 | 語法格式 | 說明 |
|---|---|---|
| 回溯引用 | (?(ref)true_exp|false_exp) | ref是前面的表達(dá)式的引用編號,這里填寫數(shù)字即可,從1開始(與回溯引用的\n相同),true_exp表示ref匹配到內(nèi)容時(shí)執(zhí)行,false_exp表示ref匹配不到內(nèi)容時(shí)執(zhí)行,false_exp是可選內(nèi)容 |
| 前后查找 | (?(?=exp)true_exp|false_exp) | (?=exp)是前后查找的表達(dá)式,也可以換為(?>=exp),true_exp表示前后查找能匹配時(shí)執(zhí)行,false_exp表示前后查找不能匹配時(shí)執(zhí)行,false_exp是可選內(nèi)容 |
3 參考
[1] 正則表達(dá)式30分鐘入門教程
[2] JavaScript高級程序設(shè)計(jì)(第3版)
[3] 正則表達(dá)式必知必會(修訂版)