掌握正則表達式,不過這三點

講個小笑話

昨天下完班,好久纔回到家,就因爲公司樓下的停車場設計的和迷宮一樣,每次都要找好久,才能發現,我沒有車o(╥﹏╥)o。

前言

曾經我一度對正則表達式有種恐懼和厭惡感,爲啥?因爲總感覺這玩意很難,很枯燥,看到別人寫出賊牛逼的正則,我想啥時候我能像他們一樣優秀。直到我看到了這三個知識點。。。

只需要花 10 分鐘時間,你可以收穫

  1. 正則表達式中的位置匹配原理與知識

  2. 正則表達式中的字符串匹配原理與知識

  3. 正則表達式中的括號的妙用

  4. 14 個常見正則表達式解析幫助理解知識點

相信我,看完這篇文章,對於工作中 90% 以上的正則問題你都能找到解決思路和方案。

相信我,看完這篇文章,對於工作中 90% 以上的正則問題你都能找到解決思路和方案。

相信我,看完這篇文章,對於工作中 90% 以上的正則問題你都能找到解決思路和方案。

默唸三聲

正則表達式是匹配模式,要麼匹配字符,要麼匹配位置

正則表達式是匹配模式,要麼匹配字符,要麼匹配位置

正則表達式是匹配模式,要麼匹配字符,要麼匹配位置

  1. 搞懂位置能幹啥? ===========

題目 1:數字的千分位分割法

將 123456789 轉化爲 123,456,789

題目 2:手機號 3-4-4 分割

將手機號 18379836654 轉化爲 183-7983-6654

題目 3:驗證密碼的合法性

密碼長度是 6-12 位,由數字、小寫字符和大寫字母組成,但必須至少包括 2 種字符

這些題時常出現在面試中,日常業務也少不了它的身影。搞懂位置,不僅能搞定面試,業務你也將寫的飛起

啥是位置?

正則表達式是匹配模式,要麼匹配字符,要麼匹配位置。那什麼是位置呢?

如下圖箭頭所指,位置可以理解爲相鄰字符之間的位置

咱們可以和空字符串進行類比, 字符的首尾、間隙都可以用空字符串進行連接。

'hello' === '' + 'h' + '' + 'e' + '' + 'l' + '' +  'l' + '' + 'o' + '' // true

有哪些位置?

正則中常用來表示位置的符號主要有:

^、$、\b、\B、?=p、(?!p)、(?<=p)、(?<!p)

接下來咱們就一個個把他們全整明白。

^

脫字符,匹配行的開頭

例如要在 hello 的開頭塞一個笑臉 (😄) 怎麼搞, 這個肯定難不倒你

let string = 'hello'

console.log(string.replace(/^/, '😄')) // 😄hello

$

美元符號,匹配行的結尾

同理想在 hello 的結尾塞一個笑臉 (😄) 呢?

let string = 'hello'

console.log(string.replace(/$/, '😄')) // hello😄

這兩個表示首尾位置的符號,相信大家一定都很熟悉。

\b

單詞的邊界,具體講有三點規則。

① \w 和 \ W 之間的位置

②  ^ 與 \ w 之間的位置

③ \w 與 $ 之間的位置

比如藏在你們電腦上學習教程文件夾中的某一集種子長這樣 xxx_love_study_1.mp4,想要把他變成❤️xxx_love_study_1❤️.❤️mp4❤️怎麼搞呢?

其實只需要執行一行代碼就行

'xxx_love_study_1.mp4'.replace(/\b/g, '❤️') // ❤️xxx_love_study_1❤️.❤️mp4❤️

畫圖理解就是

\B

非單詞的邊界,也就是 \ b 反着來的意思,它的規則如下:

① \w 與 \ w 之間的位置

② \W 與 \ W 之間的位置

③^ 與 \ W 之間的位置

④\W 與 $ 之間的位置

同樣還是用學習教程文件夾中的種子,稍稍改造一下,當執行這行代碼之後,會輸出啥?

'[[xxx_love_study_1.mp4]]'.replace(/\B/g, '❤️')

....

沒錯,滿滿的都是愛啊!!!,都快看不清名字了。

❤️[❤️[x❤️x❤️x❤️_❤️l❤️o❤️v❤️e❤️_❤️s❤️t❤️u❤️d❤️y❤️_❤️1.m❤️p❤️4]❤️]❤️

畫圖解釋如下

(?=p)

符合 p 子模式前面的那個位置。換句話說是,有一個位置,緊跟其後需要滿足 p 子模式。也有一個學名叫正向先行斷言。

還是這個例子xxx_love_study_1.mp4,要在 xxx(xxx 可以指代任何你喜歡的那個 TA) 前面塞一個❤️, 怎麼寫呢?

是這樣嗎?不是的,這樣會導致你的 xxx 都不見了,那還要❤️做什麼呢?

'xxx_love_study_1.mp4'.replace('xxx''❤️') // ❤️_love_study_1.mp4

利用 (?=p) 就可以很方便這這件事(可以想想和上面有什麼不同?)

'xxx_love_study_1.mp4'.replace(/(?=xxx)/g, '❤️') // ❤️xxx_love_study_1.mp4

畫圖理解

(?!p)

(?=p)反過來的意思,可以理解爲 (?=p) 匹配到的位置之外的位置都是屬於 (?!p) 的,它也有一個學名叫負向先行斷言。

'xxx_love_study_1.mp4'.replace(/(?!xxx)/g, '❤️') 

// (?=xxx)的輸出
❤️xxx_love_study_1.mp4
// (?!xxx)的輸出
x❤️x❤️x❤️_❤️l❤️o❤️v❤️e❤️_❤️s❤️t❤️u❤️d❤️y❤️_❤️1❤️.❤️m❤️p❤️4❤️

仔細對比一下,是不是除了 (?=xxx) 匹配到最前面那個位置,其他位置都是 (?!xxx) 匹配到的啦。

(?<=p)

符合 p 子模式後面 (注意(?=p) 表示的是前面)的那個位置。換句話說是,有一個位置,其前面的部分需要滿足 p 子模式。

依然是這個例子:我們要在 xxx(xxx 可以指代任何你喜歡的那個 TA) 的後面塞一個❤️, 怎麼寫呢?

'xxx_love_study_1.mp4'.replace(/(?<=xxx)/g, '❤️') //xxx❤️_love_study_1.mp4

畫圖解釋

(?<!p)

(?<=p)反過來的意思,可以理解爲 (?<=p) 匹配到的位置之外的位置都是屬於 (?<!p) 的,

'xxx_love_study_1.mp4'.replace(/(?<!xxx)/g, '❤️') 

// (?<=xxx)的輸出
xxx❤️_love_study_1.mp4
// (?<!xxx)的輸出
❤️x❤️x❤️x_❤️l❤️o❤️v❤️e❤️_❤️s❤️t❤️u❤️d❤️y❤️_❤️1❤️.❤️m❤️p❤️4❤️

仔細對比一下,是不是除了 (?<=xxx) 匹配到後面那個位置,其他位置都是 (?<!xxx) 匹配到的啦。

栗子詳解

學習完位置相關的知識,我們來做一下開頭的幾個題目試試

題目 1:數字的千分位分割法

將 123456789 轉化爲 123,456,789

觀察題目的規律就是從後往前,每三個數字前加一個逗號,(需要注意的是開頭不需要加逗號,)。是不是很符合 (?=p)的規律呢?p 可以表示每三個數字,要添加的逗號所處的位置正好是 (?=p) 匹配出來的位置。

第一步,嘗試先把後面第一個逗號弄出來

let price = '123456789'
let priceReg = /(?=\d{3}$)/

console.log(price.replace(priceReg, ',')) // 123456,789

第二步,把所有的逗號都弄出來

要把所有的逗號都弄出來,主要要解決的問題是怎麼表示三個數字一組, 也就是 3 的倍數。我們知道正則中括號可以把一個 p 模式變成一個小整體,所以利用括號的性質,可以這樣寫

let price = '123456789'
let priceReg = /(?=(\d{3})+$)/g

console.log(price.replace(priceReg, ',')) // ,123,456,789

第三步,去掉首位的逗號,

上面已經基本上實現需求了,但是還不夠,首位會出現, 那怎麼把首位的逗號去除呢?想想前面是不是有一個知識正好滿足這個場景?沒錯 (?!p),就是他了,兩者結合就是從後往前每三個數字的位置前添加逗號,但是這個位置不能是 ^ 首位。

let price = '123456789'
let priceReg = /(?!^)(?=(\d{3})+$)/g

console.log(price.replace(priceReg, ',')) // 123,456,789

題目 2:手機號 3-4-4 分割

將手機號 18379836654 轉化爲 183-7983-6654

有了上面數字的千分位分割法,做這個題相信會簡單很多,也就是從後往前找到這樣的位置:

每四個數字前的位置,並把這個位置替換爲 -

let mobile = '18379836654'
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654

題目 3:手機號 3-4-4 分割擴展

將手機號 11 位以內的數字轉化爲 3-4-4 格式

回想一下這樣的場景,有一個表單需要收集用戶的手機號,用戶是一個個數字輸入的,我們需要在用戶輸入 11 位手機號的過程中把其轉化爲 3-3-4 格式。即

123 =123
1234 => 123-4
12345 => 123-45
123456 => 123-456
1234567 => 123-4567
12345678 => 123-4567-8
123456789 => 123-4567-89
12345678911 => 123-4567-8911

這樣用 (?=p) 就不太合適了,例如 1234 就會變成 - 1234。想想前面的知識點有適合處理這種場景的嗎?是的(?<=p)

第一步, 將第一個 - 弄出來

const formatMobile = (mobile) ={
  return String(mobile).replace(/(?<=\d{3})\d+/, '-')      
}

console.log(formatMobile(123)) // 123
console.log(formatMobile(1234)) // 123-4

將第二個 - 弄出來

將第一個 - 弄出來之後,字符的長度多了一位,原本 1234567(這個位置插入 -)8,要變成往後移一位

const formatMobile = (mobile) ={
  return String(mobile).slice(0,11)
      .replace(/(?<=\d{3})\d+/, ($0) ='-' + $0)
      .replace(/(?<=[\d-]{8})\d{1,4}/, ($0) ='-' + $0)
}

console.log(formatMobile(123)) // 123
console.log(formatMobile(1234)) // 123-4
console.log(formatMobile(12345)) // 123-45
console.log(formatMobile(123456)) // 123-456
console.log(formatMobile(1234567)) // 123-4567
console.log(formatMobile(12345678)) // 123-4567-8
console.log(formatMobile(123456789)) // 123-4567-89
console.log(formatMobile(12345678911)) // 123-4567-8911

題目 4:驗證密碼的合法性

密碼長度是 6-12 位,由數字、小寫字符和大寫字母組成,但必須至少包括 2 種字符

題目由三個條件組成

① 密碼長度是 6-12 位

② 由數字、小寫字符和大寫字母組成

③ 必須至少包括 2 種字符

第一步寫出條件①和②和正則

let reg = /^[a-zA-Z\d]{6,12}$/

第二步,必須包含某種字符(數字、小寫字母、大寫字母)

let reg = /(?=.*\d)/
// 這個正則的意思是,匹配的是一個位置,這個位置需要滿足`任意數量的符號,緊跟着是個數字`,注意它最終得到的是個位置,而不是數字或者是數字前面有任意的東西

console.log(reg.test('hello')) // false
console.log(reg.test('hello1')) // true
console.log(reg.test('hel2lo')) // true

// 其他類型同理

第三步,寫出完整的正則

必須包含兩種字符,有下面四種排列組合方式

① 數字和小寫字母組合

② 數字和大寫字母組合

③ 小寫字母與大寫字母組合

④ 數字、小寫字母、大寫字母一起組合(但其實前面三種已經覆蓋了第四種了)

// 表示條件①和②
// let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))/
// 表示條件條件③
// let reg = /(?=.*[a-z])(?=.*[A-Z])/
// 表示條件①②③
// let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))|(?=.*[a-z])(?=.*[A-Z])/
// 表示題目所有條件
let reg = /((?=.*\d)((?=.*[a-z])|(?=.*[A-Z])))|(?=.*[a-z])(?=.*[A-Z])^[a-zA-Z\d]{6,12}$/


console.log(reg.test('123456')) // false
console.log(reg.test('aaaaaa')) // false
console.log(reg.test('AAAAAAA')) // false
console.log(reg.test('1a1a1a')) // true
console.log(reg.test('1A1A1A')) // true
console.log(reg.test('aAaAaA')) // true
console.log(reg.test('1aA1aA1aA')) // true
  1. 字符串匹配原來這麼簡單 ==============

兩種模糊匹配

正則如果只有精確匹配,那麼便完全沒有了意義

橫向

一個正則可匹配的字符串的長度不是固定的,可以是多種情況,通過量詞 +、*、?、{m,n},可實現橫向匹配

let reg = /ab{2,5}c/
let str = 'abc abbc abbbc abbbbc abbbbbc abbbbbbc'

str.match(reg) // [ 'abbc''abbbc''abbbbc''abbbbbc' ]

縱向

一個正則匹配的字符串,具體到某一位字符時,可以不是某個確定的字符串,可以有多種可能,實現方式是字符組 (其實多選分支 | 也可以實現)

let reg = /a[123]c/
let str = 'a0b a1b a2b a3b a4b'

str.match(reg) // [ 'a1b''a2b''a3b' ]

字符組

不要被名字給糊弄了,雖然他叫做字符組,但其實只是代表一個字符的可能性

範圍表示法

[123456abcdefABCDEF] =[1-6a-fA-F]

排除字符組

某位字符可以是任何東西,但是就是不能是 xxx, 使用 ^ 符號

問題:如何要表示除了某個單詞之外的任意東西呢?

[^abc]

常見簡寫形式

\d // 數字
\D // 非數字
\w // [0-9a-zA-Z_]
\W // [^0-9a-zA-Z_]
\s // [\t\v\n\r\f]
\S // [^\t\v\n\r\f]
.

量詞

量詞 & 簡寫

1. {m,} // 至少出現m次
2. {m} // 出現m次
3. ? // 出現0次或者1次,等價於{0,1}    
4. + // 至少出現1次,等價於{1,} 
5. * // 出現人一次,等價於{0,}

貪婪匹配 VS 惰性匹配

正則本身是貪婪的,會盡可能的多匹配符合模式的字符

let regex = /\d{2,5}/g
let string = '123 1234 12345 123456'
// 貪婪匹配
// string.match(regex) // [ 123, 1234, 12345, 12345 ]

// 惰性匹配
let regex2 = /\d{2,5}?/g
// string.match(regex) // [ 12, 12, 34, 12, 34, 12, 34, 56  ]

量詞後面加一個?,即變成了惰性匹配

貪婪量詞        惰性量詞
{m,n}            {m,n}?
{m,}             {m,}?
?                       ??
+                       +?
*                   *?

多選分支

一個模式可以實現橫向和縱向的模糊匹配,而多選分支可以支持多個子模式任選其一,形式是 (p1|p2|p3)

let regex = /good|nice/
let string = 'good idea, nice try.'

// string.match(regex) // [ 'good''nice' ]

// 注意,用/good|goodbye/去匹配'goodbye' 匹配到的是good
// 因爲分支結構是惰性的,前面的匹配上了,後面的就不再嘗試了

案例分析

1. 匹配 id

// 1
let regex = /id=".*?"/ // 想想爲什麼要加? 不加的話 連後面的class都會匹配到
let string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
// 2
let regex = /id="[^"]*"/ 
let string = '<div id="container" class="main"></div>'; 
console.log(string.match(regex)[0]);

2. 匹配 16 進制的顏色值

// 要求匹配如下顏色
/*
#ffbbad
#Fc01DF
#FFF
#ffE
*/

let regex = /#([a-fA-F\d]{6}|[a-fA-F\d]{3})/g
let string = "#ffbbad #Fc01DF #FFF #ffE";

console.log(string.match(regex))
//  ["#ffbbad""#Fc01DF""#FFF""#ffE"]

3. 匹配 24 小時制時間

/*
    要求匹配
  23:59
  02:07
*/
// 解析:
// 第一位:可以是0、1、2
// 第二位:當第一位位0或者1的時候,可以是0到9、第一位是2的時候,只可以是0到3
// 第三位:固定是冒號:
// 第四位:可以是0到5
// 第五位:0到9

let regex = /^([01]\d|2[0-3]):[0-5]\d$/

console.log(regex.test('23:59')) // true
console.log(regex.test('02:07'))// true

// 衍生題,可以是非0
let regex = /^(0?\d|1\d|2[0-3]):(0?|[1-5])\d/

console.log( regex.test("23:59") ) // true
console.log( regex.test("02:07") ) // true
console.log( regex.test("7:09") ) // true

4. 匹配日期

/*
    要求匹配
  yyyy-mm-dd格式的日期
  注意月份、和日的匹配
*/

let regex = /\d{4}-(0\d|1[0-2])-(0[1-9]|[12]\d|3[01])/

console.log( regex.test("2017-06-10") ) // true
console.log( regex.test("2017-11-10") ) // true
  1. 括號的神奇作用 ==========

括號的作用是提供了分組 (括號內的正則是一個整體,即提供子表達式),便於我們引用它

分組

如何讓量詞作用於一個整體?

let reg = /(ab)+/g
let string = 'ababa abbb ababab'

console.log(string.match(reg)) // ["abab""ab""ababab"]

分支結構

分支結構有點像編程裏面或的概念 ||

/*
匹配 
I love JavaScript
I love Regular Expression
*/

let reg = /I love (JavaScript|Regular Expression)/

console.log(reg.test('I love JavaScript')) // true
console.log(reg.test('I love Regular Expression')) // true

分組引用

通過括號創建子表達式,可以進行數據提取和強大的替換操作,也可以通過 js 來引用分組內容

提取數據

/*
提取年月日
2021-08-14
*/

let reg = /(\d{4})-(\d{2})-(\d{2})/

console.log('2021-08-14'.match(reg))
//  ["2021-08-14""2021""08""14", index: 0, input: "2021-08-14", groups: undefined]

// 第二種解法,通過全局的$1...$9讀取 引用的括號數據
let reg = /(\d{4})-(\d{2})-(\d{2})/
let string = '2021-08-14'

reg.test(string)

console.log(RegExp.$1) // 2021
console.log(RegExp.$2) // 08
console.log(RegExp.$3) // 14

數據替換

/*
將以下格式替換爲mm/dd/yyy
2021-08-14
*/
// 第一種解法
let reg = /(\d{4})-(\d{2})-(\d{2})/
let string = '2021-08-14'
// 第一種寫法
let result1 = string.replace(reg, '$2/$3/$1')
console.log(result1) // 08/14/2021
// 第二種寫法
let result2 = string.replace(reg, () ={
    return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1
})
console.log(result2) // 08/14/2021
// 第三種寫法
let result3 = string.replace(reg, ($0$1$2$3) ={
    return $2 + '/' + $3 + '/' + $1
})
console.log(result3) // 08/14/2021

反向引用(很重要)

除了通過 js 引用分組的內容,也可以通過正則來引用分組內容

/*
    寫一個正則支持以下三種格式
  2016-06-12
  2016/06/12
  2016.06-12
*/
let 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

注意

  1. 引用不存在的分組會怎樣?

  2. 即匹配的就是 \ 1 \2 本身

  3. 分組後面有量詞會怎樣?

  4. 分組後面如果有量詞,分組最終 (注意是分組,不是說整體) 捕獲的數據是最後一次的匹配

'12345'.match(/(\d)+/) // ["12345""5", index: 0, input: "12345", groups: undefined]

/(\d)\1/.test('12345 1') // false
/(\d)\1/.test('12345 5') // true

非捕獲性括號

上面使用的括號都會匹配他們匹配到的數據,以便後續引用,所以也可以稱爲捕獲型分組和捕獲型分支。

如果想要括號最原始的功能,但不會引用它,也就是既不會出現在 API 引用裏,也不會出現在正則引用裏,可以使用

非捕獲性括號(?:p)

// 非捕獲型引用
let reg = /(?:ab)+/g
console.log('ababa abbb ababab'.match(reg)) // ["abab""ab""ababab"]
// 注意這裏,因爲是非捕獲型分組,所以使用match方法時,不會出現在數組的1位置了
let reg = /(?:ab)+/
console.log('ababa abbb ababab'.match(reg)) // ["abab", index: 0, input: "ababa abbb ababab", groups: undefined]
let reg = /(ab)+/
console.log('ababa abbb ababab'.match(reg)) // ["abab""ab", index: 0, input: "ababa abbb ababab", groups: undefined]

案例

1.trim 方法模擬

// 1. 提取中間關鍵字符, 使用的分組引用
const trim1 = (str) ={
  return str.replace(/^\s*(.*?)\s*$/, '$1')
}
// 2. 去掉開頭和結尾的空字符
const trim2 = (str) ={
    return str.replace(/^\s*|\s*$/g, '')
}

2. 將每個單詞的首字母大寫

關鍵是要找到每個單詞的首字母

// my name is epeli

const titleize = (str) ={
  return str.toLowerCase().replace(/(?:^|\s)\w/g, (c) => c.toUpperCase())
}  

console.log(titleize('my name is epeli')) // My Name Is Epeli

// 拓展,橫向轉駝峯,例如base-act-tab => BaseActTab
'base-act-tab'.replace(/(?:^|-)(\w)/g, ($0$1) =$1.toUpperCase()) // BaseActTab

3. 駝峯化

// -moz-transform => MozTransform
const camelize = (str) ={
    return str.replace(/[-_\s]+(\w)/g, (_, $1) =$1.toUpperCase())     
}

console.log(camelize('-moz-transform')) // MozTransform

4. 中劃線化

// MozTransform => -moz-transform
const dasherize = (str) ={
    return str.replace(/[A-Z]/g, ($0) =('-' + $0).toLowerCase())
}

console.log(dasherize('MozTransform')) // -moz-transform

5.HTML 轉義和反轉義

// html轉義規則見https://blog.wpjam.com/m/character-entity/

const escapeHTML = (str) ={
    const escapeChars = {
    '<''lt',
    '>''gt',
    '"''quot',
    ''': '#39',
    '&''amp'
  }
  
  let regexp = new RegExp(`[${Object.keys(escapeChars).join('')}]`'g') // 爲了得到字符組[<>"'&]
    
    return str.replace(regexp, (c) => `&${escapeChars[ c ]};`)
}

console.log( escapeHTML('<div>Blah blah blah</div>')) // <div>Blah blah blah</div>


// 反轉義
const unescapseHTML = (str) => {
    const htmlEntities = {
    nbsp: ' ',
    lt: '<',
    gt: '>',
    quot: '"',
    amp: '&',
    apos: '''
  }
  
  return str.replace(/&([^;]+);/g, ($0$1) ={
        return htmlEntities[ $1 ] || ''
    })
}

console.log(unescapseHTML('<div>Blah blah blah</div>')) // <div>Blah blah blah</div>

6. 匹配成對的標籤

/*
    匹配
      <title>regular expression</title>
        <p>laoyao bye bye</p>
  不匹配
    <title>wrong!</p>
*/
let reg = /<([^>]+)>.*?</\1>/g

console.log(reg.test('<title>regular expression</title>')) // true
console.log(reg.test('<p>laoyao bye bye</div>')) // false

相約再見

強烈推薦老姚的正則表達式迷你小書,筆者也是讀了這本書之後慢慢開始對正則有些理解,不再抗拒它,這篇文章主要也是基於這本書的內容做了總結。

「歡迎在評論區討論,掘金官方將在掘力星計劃 [1] 活動結束後,在評論區抽送 100 份掘金周邊,抽獎詳情見活動文章」

參考

  1. JS 正則表達式完整教程(略長)

  2. 三十分鐘包會——正則表達式

  3. 聊聊讓人頭疼的正則表達式

參考資料

[1]

https://juejin.cn/post/7012210233804079141:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/knKR4CPR8o7SFVaNiPtJjw