何為正則?一句話總結:正則是匹配模式,要麼匹配字符,要麼匹配位置。
字符匹配#
模糊匹配#
正則除了精確匹配,還能實現模糊匹配,模糊匹配又分為橫向模糊和縱向模糊。
橫向模糊匹配#
橫向模糊指的是,一個正則可匹配的字符串的長度不是固定的。其實現方式是使用量詞,譬如 {m, n},表示連續出現最少 m 次,最多 n 次。
const regex = /ab{2,4}c/g
const string = 'abc abbc abbbc abbbbc abbbbbc'
console.log(string.match(regex)) // ["abbc", "abbbc", "abbbbc"]
正則 g
修飾符表示全局匹配,強調 “所有” 而不是 “第一個”。
// 無全局修飾符的情況
const regex = /ab{2,4}c/
const string = 'abc abbc abbbc abbbbc abbbbbc'
console.log(string.match(regex))
// ["abbc", index: 4, input: "abc abbc abbbc abbbbc abbbbbc", groups: undefined]
縱向模糊匹配#
縱向模糊指的是,一個正則匹配的字符串,具體到某一位字符時,它可以不是某個確定的字符。其實現方式是使用字符組,譬如 [abc]
,表示該字符是可以字符 "a"、"b"、"c" 中的任何一個。
const regex = /a[123]b/g
const string = 'a0b a1b a2b a3b a4b'
console.log(string.match(regex)) // ["a1b", "a2b", "a3b"]
字符組#
雖然稱為字符組,但匹配的其實只是一个字符。譬如字符組 [abc]
只是匹配一個字符。字符組有範圍表示法、排除法和簡寫形式。
範圍表示法#
字符組 [0-9a-zA-Z]
表示數字、大小寫字母中任意一個字符。由於連字符 "-" 有特殊含義,所以要匹配 "a"、"-"、"c" 中的任何一個字符,可以寫成如下形式:[-az]、[az-]、[a\-z]
,連字符要麼開頭,要麼結尾,要麼轉義。
排除字符組#
排除字符組(反義字符組)表示是一個除 "a"、"b"、"c" 之外的任意一個字 符。字符組的第一位放 ^
(脫字符),表示求反。^ 可以配合範圍表示法使用,如 。
簡寫形式#
正則簡寫形式如下:
字符組 | 含義 |
---|---|
\d | [0-9],表示數字 |
\D | [^0-9],表示非數字 |
\w | [0-9a-zA-Z_],表示數字、大小寫字符和下劃線 |
\W | [^0-9a-za-z_],表示非單詞字符 |
\s | [\t\v\n\r\f],表示空白符 |
\S | [^ \t\v\n\r\f],表示非空白符 |
. | 通配符 |
需要注意:[ \t\v\n\r\f]
分別表示空白符、水平制表符、垂直制表符、換行符、回車符、換頁符。
通配符 .
可以表示幾乎任意字符。換行符、回車符、行分隔符和段分隔符除外。如果想要匹配任意字符,可以使用組合寫法:[\d\D]
、[\w\W]
、[\s\S]
和 [^]
中任何的一个。
量詞#
簡寫形式#
量詞 | 含義 |
---|---|
{m, n} | 表示出現 m 到 n 次 |
{m,} | 至少出現 m 次 |
{m} | 等價 {m, m},表示出現 m 次 |
? | 等價 {0, 1},表示出現或不出現 |
+ | 等價 {1,},表示至少出現 1 次 |
\* | 等價 {0,},表示出現任意次 |
貪婪匹配與惰性匹配#
貪婪匹配會盡可能多的匹配,表現如下:
const regex = /\d{2,5}/g
const string = '123 1234 12345 123456'
console.log(string.match(regex))
// ["123", "1234", "12345", "12345"]
通過在量詞後面加 ?
實現惰性匹配,惰性匹配會盡可能少的匹配,表現如下:
const regex = /\d{2,5}?/g
const string = '123 1234 12345 123456'
console.log(string.match(regex))
// ["12", "12", "34", "12", "34", "12", "34", "56"]
多選分支#
多選分支可以支持多個子模式任選其一。具體形式如下:(p1|p2|p3),其中 p1、p2 和 p3 是子模式,用 |
(管道符)分隔,表示其中任何之一。需要注意:多選分支是從左到右惰性匹配的,前面匹配成功之後後面的模式便不再嘗試。可以通過更改子模式的順序來改變匹配的結果。
const regex = /good|goodbye/g
const string = 'goodbye'
console.log(string.match(regex))
// ["good"]
實例應用#
匹配文件路徑#
文件路徑格式如 盤符:\文件夾\文件夾\文件夾\
。匹配符盤:[a-zA-Z]:\\
。匹配文件名或文件夾名,不能包含一些特殊字符,需要排除字符組 來表示合法字符,且文件名或文件夾名不能為空,至少有一個字符,需要使用量詞 +
。文件夾可以出現任意次,最後可能是文件而不是文件夾,不需要帶 \\
。
const regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/
console.log(regex.test('F:\\study\\regular expression.pdf'))
匹配 id#
const regex = /id=".*"/
const string = '<div id="container" class="main"></div>'
// id="container" class="main"
量詞 .
是通配符,可以匹配雙引號,同時是貪婪匹配,所以出錯。可以將其改造成惰性匹配:
const regex = /id=".*?"/
但以上正則匹配效率低,因為其匹配原理設計” 回溯 “ 概念,最優解如下:
const regex = /id="[^"]*"/
位置匹配#
位置的概念#
位置(錨)是相鄰字符之間的位置。可以將位置理解成空字符串。在 ES5 中,一共有六個錨:^
、$
、\b
、\B
、(?=p)
、(?!p)
。
^
匹配開頭,多行匹配則匹配行開頭&
匹配結尾,多行匹配則匹配行結尾\b
匹配單詞邊界,即\w
與\W
、^
、$
之間的位置\B
匹配非單詞邊界- (?=p) 為正向先行斷言(positive lookhead),匹配模式 p 前的位置
- (?!p) 為負向先行斷言(negative lookhead), 匹配非 p 前的位置
實例應用#
數字千分位分隔符#
千分位分隔符的插入位置為三位一組數字的前面,且不能是開頭位置。
const result = '123456789'
const regex = /(?!^)(?=(\d{3})+$)/g
console.log(result.replace(regex, ','))
// 123,456,789
密碼驗證#
密碼長度 6-12 位,由數字、大小寫字母組成,但必須至少包括 2 種字符。首先考慮匹配 6-12 位的數字、大小寫字母:
const regex = /^[0-9A-Za-z]{6-12}$/g
然後需要判斷至少包含兩種字符,有兩種解法。
第一種解法:首先判斷是否包含數字,正則可以表示如下:
const regex = /(?=.*[0-9])^[0-9A-Za-z]{6-12}$/
重點需要理解 (?=.*[0-9])^
,該正則表示開頭前的位置,同時也表示開頭,因為位置可以表示為空字符串。該正則表示在任意多個字符後有數字。依次類推,如果需要同時包含數組和大寫字母可以表示為:
const regex = /(?=.*[0-9])(?=.*[A-Z])^[0-9A-Za-z]{6-12}$/
最終正則可以表示為:
const regex = /((?=.*[0-9])(?=.*[A-Z])|(?=.*[0-9])(?=.*[a-z])|(?=.*[A-Z])(?=.*[a-z]))^[0-9A-Za-z]{6-12}$/
const str1 = '123456'
const str2 = '123456a'
const str3 = 'abcdefgA'
console.log(str1, regex.test(str1)) // false
console.log(str2, regex.test(str2)) // true
console.log(str3, regex.test(str3)) // true
第二種解法:“至少包含兩種字符” 表示不能全為數字、大寫字母或小寫字母,不能全為數字可以表示如下:
const regex = /(?!^[0-9]{6-12}$)^[0-9A-Za-z]{6-12}$/
所以最終正則可以表示為:
const regex = /(?!^[0-9]{6,12}$)(?!^[A-Z]{6,12}$)(?!^[a-z]{6,12}$)^[0-9A-Za-z]{6,12}$/
括號的作用#
分組和分支結構#
括號提供了分組,用於引用。引用分兩種:在 JavaScript 裡引用和在正則裡引用。分組和分支結構是括號最直接的功能,強調括號內是一個整體,即提供子表達式。
// 分組的情況,強調 ab 是一個整體
const regex1 = /(ab)+/g
// 分支的情況,強調分支結構是一個整體
const regex = /this is (ab|cd)/g
分組引用#
使用括號分組,可以進行數據提取和替換操作。以提取形如 yyyy-mm-dd 日期年月日:
const regex = /(\d{4})-(\d{2})-(\d{2})/g
const date = '2018-01-01'
const regex = /(\d{4})-(\d{2})-(\d{2})/
const date = '2018-01-01'
console.log(regex.exec(date))
// console.log(date.match(regex))
// ["2018-01-01", "2018", "01", "01", index: 0, input: "2018-01-01", groups: undefined]
console.log(RegExp.$1, RegExp.$2, RegExp.$3)
// 2018 01 01
擴展:在 JavaScript 裡,exec
和 match
方法作用基本一致,主要有兩點區別:
exec
是 RegExp 類分方法,而match
是 String 類的方法exec
只匹配第一個符合的字符串,而match
行為跟是否配置 g 修飾符有關,在非全局匹配情況下,兩者表現一致
此外,括號分組還可方便進行替換操作,如將 yyyy-mm-dd 替換為 dd-mm-yyyy:
const date = '2018-01-31'
const regex = /^(\d{4})-(\d{2})-(\d{2})$/
const result = date.replace(regex, '$3-$2-$1')
console.log(result) // 31-01-2018
// 等價於
const result2 = data.replace(regex, function () {
return RegExp.$3 + '-' + RegExp.$2 + '-' + RegExp.$1
})
// 等價於
const result3 = data.replace(regex, function (match, year, month, day) {
return day + '-' + month + '-' + year
})
反向引用#
除了在 JavaScript 裡引用分組,還可以在正則裡引用,即反向引用。舉個栗子,以匹配日期為例:
const date1 = '2018-01-31'
const date2 = '2018-01.31'
const regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/
console.log(regex.test(date1)) // true
console.log(regex.test(date2)) // false
如果出現括號嵌套,則以首次出現的左括號順序為分組順序。反向引用有三個 Tips:
- Tip1:如果出現類似 10,則表示第 10 個分組而不是 1 和 0,如果需要表示後者,需要使用非捕獲括號,表示成 (?:1) 0 或 1 (?:0)。
- Tip2:如果引用不存在分組,則只匹配反向引用的字符本身,如 2 只匹配 2,反斜杠表示轉義。
- Tip3:如果分組後面有量詞,則以最後一次捕獲的數據為分組。
非捕獲括號#
之前的例子,括號裡的分組或捕獲數據,以便後續引用,稱之為捕獲型分組和捕獲型分支。如果只想使用括號原始功能,可以使用非捕獲型括號 (?:p)
和 (?:p1|p2|p3)
。
回溯法原理#
回溯法也稱試探法,它的基本思想是:從問題的某一種狀態(初始狀態)出發,搜索從這種狀態出發所能達到的所有 “狀態”,當一條路走到 “盡頭” 的時候(不能再前進),再後退一步或若干步,從另一種可能 “狀態” 出發,繼續搜索,直到所有的 “路徑”(狀態)都試探過。這種不斷 “前進”、不斷 “回溯” 尋找解的方法,就稱作 “回溯法”。
” 回溯法 “本質上是深度優先算法。舉個栗子,以正則 /ab{1,3}/c
來匹配字符串 'abbc',其匹配流程如下:
正則回溯
圖中第 5 步有紅顏色,表示匹配不成功。此時 b {1,3} 已經匹配到了 2 個字符 "b",準備嘗試第三個時,結果發現接下來的字符是 "c"。那麼就認為 b {1,3} 就已經匹配完畢。然後狀態又回到之前的狀態,最後再用子表達式 c,去匹配字符 "c"。此時整個表達式匹配成功了。圖中第 6 步便稱為” 回溯 “。
以上為貪婪匹配情況下的回溯,在惰性匹配中也存在回溯。再舉個惰性匹配的栗子:
const string = '12345'
const regex = /^(\d{1,3}?)(\d{1,3})$/
console.log(string.match(regex))
// => ["1234", "12", "2345", index: 0, input: "12345"]
儘管是惰性匹配,但為了整體匹配成功,第一个分組還是會多分配一個字符,其整體匹配流程如下:
惰性正則回溯
此外,分支結構也可視為一種回溯,在當前分支不滿足匹配條件時,會切換到另一條分支。
形象類比一下回溯的幾種情況:
- 貪婪量詞 “試” 的策略是:買衣服砍價。價錢太高了,便宜點,不行,再便宜點。
- 惰性量詞 “試” 的策略是:賣東西加價。給少了,再多給點行不,還有點少啊,再給點。
- 分支結構 “試” 的策略是:貨比三家。這家不行,換一家吧,還不行,再換。
正則的拆分#
結構和操作符#
JavaScript 裡正則表達式由_字符字面量、字符組、量詞、錨、分組、選擇分支、反向引用_等結構組成。
結構 | 說明 |
---|---|
字符字面量 | 匹配一個具體字符,包括轉義與非轉義 |
字符組 | 匹配一個多種可能的字符 |
量詞 | 匹配連續出現的字符 |
錨 | 匹配一個位置 |
分組 | 匹配一個括號整體 |
選擇分支 | 匹配多個子表達式之一 |
其中涉及的操作符有:
操作符描述 | 操作符 | 優先級 | |
---|---|---|---|
轉義符 | \ | 1 | |
括號和方括號 | \(...\)、\(?:...\)、\(?=...\)、\(?!...\)、\[...\] | 2 | |
量詞限定符 | {m}、{m,n}、{m,}、?、\*、+ | 3 | |
位置和序列 | ^、\$、\ 元字符、一般字符 | 4 | |
管道符 | \` | \` | 5 |
元字符#
JavaScript 正則裡用到的元字符有 ^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、-
,當匹配到上面字符本身時,可以一律轉義。
正則的構建#
構建正則的平衡法則:
- 匹配預期的字符串
- 不匹配非預期的字符串
- 可讀性和可維護性
- 效率
这里只談如何改善匹配效率的幾種方式:
- 使用具體型字符組來代替通配符,來消除回溯
- 使用非捕獲分組。因為捕獲分組需要占用內存來存儲捕獲分組和分支裡的數據
- 獨立出確定字符,如
a+
可以修改為aa*
,後者比前者多確定了字符 a。 - 提取分支公共部分,如
this|that
修改為th(:?is|at)
- 減少分支數量,如
red|read
修改為rea?d
正則編程#
在 JavsScript 裡,關於正則常用的相關 API 有 6 個,其中字符串實例 4 個,正則實例 2 個:
String#search
String#split
String#match
String#replace
RegExp#test
RegExp#exec
字符串實例的 match
和 search
方法,會把字符串轉換為正則:
const str = '2018.01.31'
console.log(str.search('.'))
// 0
//需要修改成下列形式之一
console.log(str.search('\\.'))
console.log(str.search(/\./))
// 4
console.log(str.match('.'))
// ["2", index: 0, input: "2018.01.31"]
//需要修改成下列形式之一
console.log(str.match('\\.'))
console.log(str.match(/\./))
// [".", index: 4, input: "2018.01.31"]
字符串的四個方法,每次匹配時,都是從 0 開始的,即 lastIndex 屬性始終不變。而正則實例的兩個方法 exec、test,當正則是全局匹配時,每一次匹配完成後,會修改 lastIndex。