從零開始學正則(三)

 壹 ❀ 引html

我在從零開始學正則(二)這篇文章中介紹了正則位置的概念,以及匹配位置經常使用的六個錨,位置相關的知識雖然很少,不過理解起來還挺費勁。在文章結尾留下了兩個問題,一問寫一個正則將"12345678"變成千位分隔符表示法 "12,345,678";二問驗證密碼長度在6-12位之間,且至少包含數字,小寫字母與大寫字母其中兩種。git

咱們先來分析第一個問題,所謂千位分隔符其實就是從右往左每隔三位數加進一個逗號。有個問題,咱們理解的正則匹配都是從左往右,怎麼反過來?這裏就能夠使用 $ 匹配尾部,表示從尾部開始匹配,改變匹配方向。github

從右往左,逗號都在3n(n>=1,使用量詞+表示)個數字前面,很明顯這是要匹配位置,這裏可使用 (?=p) 正向先行斷言解決,因此正則是這樣:正則表達式

var str = '12345678';
var regex = /(?=(\d{3})+$)/g;
var result = str.replace(regex, ','); //12,345,678

怎麼理解這個正則呢,(\d{3})+$ 是一個匹配條件,也就是找 (\d{3})+$ 前面的位置,其中 (\d{3}) 是一個組,這個組會出現1次或更屢次,因此後面跟了+,又由於要從尾部開始匹配,因此還有個$,不難吧?數組

看着貌似沒問題,但若是咱們要將123456789改成千位分隔符就出現問題了:學習

var str = '123456789';
var regex = /(?=(\d{3})+$)/g;
var result = str.replace(regex, ','); //,123,456,789

很遺憾,若是被處理的字符串恰好是三的倍數,就會出現頭部多一個逗號的狀況,這不是咱們想要的,怎麼辦呢?其實可使用 (?!p) 負向先行斷言表示除了開頭的位置,開頭的位置是誰?固然是脫字符^啦。因而咱們加個條件:測試

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

那麼如今正則的意思就是,匹配不是開頭的且是三倍數前面的位置,這裏(?!^)和(?=(\d{3})+$)是兩個組,表示並列關係,就像JavaScript中的&&,注意不要與管道符 | 弄混淆了,管道符表示分支,即知足其一便可,就像JavaScript中的 || spa

咱們來接着分析第二個問題,驗證密碼長度在6-12,且必須包含大小寫字母數字其中兩種。若是隻是6-12位大小寫字母與數字都還好,只需這樣:3d

var str = "abcdef";
var regex = /^[0-9a-zA-Z]{6,12}$/g;
var result = regex.test(str); //true

那麼咱們如何驗證字符串是否包含一個數字呢,這裏直接上結論,使用 (?=.*[0-9]) 能夠作到,我來詳細解釋下意思:code

首先 (?=.*[0-9]) 的本意是看能不能找到.*[0-9]前面的位置,若是能找到那說明至少有一個.*[0-9],因此咱們只須要明白.*[0-9]是什麼意思就行了。

[0-9] 好理解,0-9之間的任意一個數字,那爲何不直接寫成 (?=[0-9]) 呢,若是說單純判斷有沒有數字,準確來講 (?=[0-9]) 是沒問題的,咱們來測試一下:

var regex = /(?=[0-9])/g;
regex.test('1') //true
regex.test('a1') //true
regex.test('❀1❀') //true
regex.test('a') //false

但如今要求是至少包含兩種數字和大/小寫字母其中兩種,咱們假設是包含數字和小寫字母,按常理來講正則應該是這樣,咱們測試下:

var regex = /(?=[0-9])(?=[a-z])/g;
regex.test('1a') //false
regex.test('aa1') //false
regex.test('11a') //false

結果發現所有爲false,由於此時正則表達式是但願找一個既在數字前又在小寫字母前的位置。或者反過來理解,當同時存在數字和小寫字母時,必定有一個位置同時在數字和字母前。理解這個關鍵點,問題就迎刃而解了。

咱們先單純以1a爲例,哪一個位置既在1前面,又在字母a前面?毫無疑問就是 ^,因此咱們改寫正則:

var regex = /(?=[0-9])(?=1[a-z])/g;
regex.test('1a') //true

你看,這不就爲true了。再看例子aa1,這個位置有兩個,能夠是 a 與 a1 中間,也能夠是 ^,好比咱們以查a與a1中間的位置爲例:

var regex = /(?=a[0-9])(?=[a-z])/g;
regex.test('aa1') //true

或者以查 ^ 爲例:

var regex = /(?=aa[0-9])(?=[a-z])/g;
regex.test('aa1') //true

你看,只要咱們能找到共同位置,就表示同時存在兩種字符。

但有個問題,這幾個例子都是咱們寫死的,字符結構固定。實際開發中咱們也不知道數字前面有沒有字符,字母前有沒有數字,有幾個數字,怎麼辦呢?只要加上 .* 就行了,. 表示通配符,*表示量詞{0,},即任意字符出現任意次數。

咱們再看 /(?=.*[0-9])(?=.*[a-z])/g,這不就是找一個既在數字前又在小寫字母前的正則嗎。那麼咱們再結合6-12位長度,結合起來就是這樣:

var regex = /(?=.*[0-9])(?=.*[a-z])^[0-9a-zA-Z]{6,12}$/g;
regex.test('1aaaaa') //true
regex.test('a12345') //true
regex.test('aaaaaa') //true
regex.test('111111') //true

爲何 (?=.*[0-9])(?=.*[a-z]) 是寫在 ^ 前面?在正則第二章介紹中咱們已經知道位置實際上是很抽象的東西,若是用空字符""表示位置,它能夠是無數個。因此咱們能夠理解爲在 ^ 前面還有無數個看不見的位置,那麼只要你的字符同時擁有小寫字母和數字,就必定能在開頭位置 ^ 前找到這個位置,咱們將上面的正則抽象成js語句,它更像這樣:

if(位置===(?=.*[0-9])&& 位置===(?=.*[a-z])){
  ^[0-9a-zA-Z]{6,12}$;
};

這只是數字和小寫字母的狀況,咱們還得結合數字和大寫字母,小寫字母和大字母,因此最終正則就是這樣:

var regex =/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[AZ]))^[0-9A-Za-z]{6,12}$/

除了使用正向先行斷言,咱們還可使用負向先行斷言,即輸入字段不能同時爲數字,同時爲小寫字母,同時爲大寫字母,正則爲:

var regex = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;

關於這條正則我只能貼出來給你們看看,確實有點無力解釋,請教了公司幾個資歷老的員工,都沒法解答。我忽然明白原書推薦第一遍不求甚解的讀是啥意思了,這兩道題我光分析,查資料整理用了半天....仍是由於我太菜的緣故吧。如有有緣人看到,能幫我解答那是最好不過了。

那麼關於題目先分析到這裏,不知道你們有沒有發現,上述題目解答中對於分組括號使用特別頻繁,我在解答題目時也發現像量詞+*寫在括號內和括號外傳達的意思徹底不一樣,那麼本篇主要對於正則表達式的括號使用展開分析。

說在前面,正則學習系列文章均爲我閱讀 老姚《JavaScript正則迷你書》的讀書筆記,文中全部正則圖解均使用regulex製做。那麼本文開始!

 貳 ❀ 分組和分支結構

1.分組基礎

在正則中,圓括號 () 表示一個分組,即括號內的正則是一個總體,表示一個子表達式。

咱們知道 /ab+/ 表示匹配a加上一個或多個b的組合,那若是咱們想匹配ab的屢次組合呢?這裏就可使用()包裹ab:

var str = 'abab  ababab aabbaa';
var regex = /(ab)+/g;
var result = str.match(regex); //["abab", "ababab", "ab"]

在分支中使用括號也是很是常見的,好比這個例子:

var str1 = 'helloEcho';
var str2 = 'helloKetty';
var regex = /^hello(Echo|Ketty)$/;
var result1 = regex.test(str1); //true
var result2 = regex.test(str2); //true

若咱們不給分組加括號,此時的分支就變成了helloEcho和Ketty,很明顯這就是否是咱們想要的。(注意正則尾部未加全局匹配g,若是加了第二個驗證爲false,緣由參考)。

2.分組引用

不知道你們在以往看正則表達式時有沒有留意到$1,$2相似的字符,這類字符表示正則分組引用,對於正則使用是很是重要的概念。咱們來看一個簡單的例子:

寫一個匹配 yyyy-mm-dd 的正則(這裏先不考慮月不超過12之類的狀況)

var regex = /(\d{4})-(\d{2})-(\d{2})/;

經過圖解咱們能發現每一個分組上面多了相似Group #1的分組編號,是否是已經聯想到$1相關的字符了呢?沒錯,這裏$1,$2正是對應的分組編號。

這裏咱們科普兩個方法,一個是字符串的match方法,一個是正則的exec方法,它們都用於匹配正則相符字段,看個例子:

var result1 = '2019-12-19'.match(regex);
var result2 = regex.exec('2019-12-19');
console.log(result1);
console.log(result2);

能夠看到雖然方法寫法不一樣,但結果如出一轍,咱們來解釋下匹配的結果。

"2019-12-19"爲正則最終匹配到的結果,"2019", "12", "19"這三個分別爲group1,group2,group3三個分組匹配的結果,index: 0爲匹配結果的開始位置,input: "2019-12-19"爲被匹配的輸入字段,groups: undefined表示一個捕獲組數組或undefined(若是沒有定義命名捕獲組)。

咱們能夠經過$1,$2直接訪問上面例子中各分組匹配到的結果。這裏咱們展現一個完整的例子,在使用過一次正則後輸出RegExp對象,能夠看到此對象上有衆多屬性,再經過 RegExp.$1 咱們能直接拿到分組1的匹配結果:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2019-12-19";
//注意,這裏你得先使用一次正則,match test,replace等方法都行
regex.exec(string);
console.dir(RegExp);
console.log(RegExp.$1); // "2019" 
console.log(RegExp.$2); // "02" 
console.log(RegExp.$3); // "119"

如今咱們要明白一個概念,$1表示的就是Group #1的匹配結果,它就像一個變量,保存了匹配到的實際值。那麼知道了這一點咱們能作什麼呢?好比咱們將 yyyy-mm-dd 修改成 dd/mm/yyy 格式。

var result = string.replace(regex, '$3/$2/$1'); // 19/12/2019
console.log(result);

這段代碼等價於:

var result = string.replace(regex, function () {
  return RegExp.$3 + "/" + RegExp.$2 + "/" + RegExp.$1; // 19/12/2019
});

同時也等價於:

var result = string.replace(regex, function (match, year, month, day) {
  console.log(match, year, month, day);//2019-12-19 2019 12 19
  return day + "/" + month + "/" + year;//19/12/2019
});

因此看到這,你們也不要糾結第一個修改中'$3/$2/$1'字段如何關聯上的分組匹配結果,知道是正則底層實現這麼去用就對了。

 叄 ❀ 反向引用

除了像在上文API中那樣使用分組同樣,還有一個比較常見的就是在正則自身中使用分組,即代指以前已經出現過的分組,又稱爲反向引用。咱們經過一個例子來了解反向引用。

如今咱們須要一個正則能同時匹配    2019-12-19      2016/12/19       2016.12.19   這三種字段,正則咱們能夠這麼寫:

var regex = /\d{4}[-\/\.]\d{2}[-\/\.]\d{2}/;
regex.test('2019-12-19'); //true
regex.test('2019/12/19'); //true
regex.test('2019.12.19'); //true

經過圖解咱們也知道這個正則其實有個問題,它甚至能匹配 2019-12.19 格式的字段

regex.test('2019-12.19'); //true

那如今咱們要求先後兩個分隔符必定相同時才能匹配成功怎麼作呢,這裏就須要使用反向引用,像這樣:

var regex = /\d{4}([-\/\.])\d{2}\1\d{2}/;
regex.test('2019-12-19'); //true
regex.test('2019/12/19'); //true
regex.test('2019.12.19'); //true
regex.test('2019-12.19'); //false
regex.test('2019/12-19'); //false

這裏的 \1 就是反向引用,除了代指前面出現過的分組([-\/\.])之外,在匹配時它的分支選擇也會與前者分組同步,說直白點,當前面分組選擇的是 - 時,後者也會選擇 - 而後纔去匹配字段。

有個問題,括號也會存在嵌套的狀況,若是多層嵌套反向引用會有什麼規則呢?咱們來看個例子:

var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
'1231231233'.match(regex); // true 
console.log( RegExp.$1 ); // 123 
console.log( RegExp.$2 ); // 1 
console.log( RegExp.$3 ); // 23 
console.log( RegExp.$4 ); // 3

經過例子與圖解應該不難理解,當存在多個括號嵌套時,從$1-$9的順序對應括號嵌套就是從外到內,從左到右的順序。

$1 對應的是 ((\d)(\d(\d))),$2 對應的是第一個 (\d),$3 對應的是 (\d(\d)),$4 對應的是 $3 中的 (\d)。

雖然咱們在前面說的是$1-$9,準確來講,只要你的分組夠多,咱們甚至能使用$1000都行,好比:

var regex = /(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)\12+/;
var string = "abcdefghijkllll";
regex.test(string);//true
console.log(RegExp.$12);//undefined

能夠看到 \12 確實指向了前面的 (l) 分組,但因爲RegExp對象只提供了 $1-$9 的屬性,因此這裏咱們輸出RegExp.$12是undefined

還有一個問題,若是咱們反向引用了不存在的分組會怎麼樣呢?很好理解,直接看個例子:

var regex = /\1\2\3/;
var string = "\1\2\3";
regex.test(string);//true
console.log(RegExp.$1);//爲空

因爲在\1前面不存在任何分組,因此這裏的\1\2\3就單純變成轉義符\和三個數字123了,不會代指任何分組。

最後一點,分組後面若是有量詞,分組會記錄匹配的最後一次的數據,看個例子:

var regex = /(\w)+/;
var string = "abcde";
console.log(regex.exec(string));// ["abcde", "e", index: 0, input: "abcde", groups: undefined]

能夠看到分組匹配的結果爲e,也就是最後捕獲的數據,但index仍是爲0,表示捕獲結果的開始位置。

因此在分組有量詞的狀況下使用反向引用,它也會指向捕獲最大次數最後一次的結果。

var regex = /(\w)+\1/;
regex.test('abcdea');//false
regex.test('abcdee');//true

var regex1 = /(\w)+\1/;
regex.test('abcdee');
console.log(RegExp.$1);//2

 肆 ❀ 非捕獲括號

在前面講述分組匹配以及反向引用時,咱們都知道正則其實將分組匹配的結果都儲存起來了,否則也不會有反向引用這個功能,那麼若是咱們不須要使用反向引用,說直白點就是不但願分組去記錄那些數據,怎麼辦呢?這裏就可使用非捕獲括號了。

寫法很簡單,就是在正則條件加上 ?: 便可,例如 (?:p) 和 (?:p1|p2|p3),咱們來作個試驗,看看最終match輸出結果:

var regex = /(ab)+/;
var string = "ababa aab ababab";
string.match(regex);
console.log(RegExp.$1);//ab

var regex = /(?:ab)+/;
var string = "ababa aab ababab";
string.match(regex);
console.log(RegExp.$1);//

咱們分別在正則分組 ab前面加或不加 ?:,再分別輸出 RegExp.$1 ,能夠看到普通分組記錄了最後一次的匹配結果,而非捕獲括號單純起到了匹配做用,並無去記錄匹配結果。

 伍 ❀ 總結

那麼到這裏,第三章知識所有解釋完畢,咱們來作一個技術總結,你們能夠參照下方思惟導圖回顧知識點,看看是否還熟記於心頭。

最後留兩個思考題,請模擬實現 trim方法,即便用正則去除字符串開頭與結尾的空白符。第二個,請將my name is echo每一個單詞首字母轉爲大寫。

那麼本文就寫到這裏了。我要開始學習第四章了。

相關文章
相關標籤/搜索