談談 JavaScript 的正則表示式
一、背景
最近在做 CMS 系統中不同身份登入使用者的許可權管理,涉及到對 api 路徑的識別去判斷是否放行。以前對正則表示式都是敬而遠之,要用到的話都是直接複製貼上現成網上的表示式,看也看不太懂,借這次機會熟悉下,不求鑽的多深,但求有個整體的認知,滿足我目前的簡單需求即可。
二、介紹
正則表示式
(Regular Expression)是一種用來匹配字串的強有力的工具。
各個程式語言對正則表示式的支援標準有所差異,這裡只以 JavaScript 為例。
三、用法
本文對 非捕獲括號、正向肯定查詢、正向否定查詢 不做介紹。
1、匹配符
匹配符 | 可以匹配 |
---|---|
Iamstring | Iamstring |
. | 一個任意字元 |
\d | 一個數字 |
\D | 一個非數字 |
[a-zA-Z] | 一個字母 |
[^a-zA-Z] | 一個非字母 |
\w | 一個字母、數字或者下劃線 |
[0-9a-zA-Z_] | 同上 |
\s | 一個空格(還包括製表符、換頁符和換行符) |
[\u4e00-\u9fa5] | 中文 |
A|B | A 或 B |
[ABC] | A 或 B 或 C |
[A-C] | 同上 |
2、特殊符號
上面的匹配符可以搭配下表的符號,如 /[A-Z]{3}/
:
如果遇到歧義可使用括號 (),如
/bo+/
和/(bo)+/
表達的意思不一樣。
特殊符號 | 表示 | 放匹配符的前面 | 放匹配符的後面 | 備註 |
---|---|---|---|---|
{n} |
n個字元 | √ | ||
{n,m} |
n-m個字元 | √ | ||
* |
0或多個字元 | √ | 等價於{0,} | |
+ |
至少一個字元 | √ | 等價於{1,} | |
? |
0或1個字元 | √ | 等價於{0,1} | |
^ |
表示行的開頭 | √ | ||
$ |
表示行的結束 | √ | ||
\b |
表示單詞的邊界 | √ | √ | |
\B |
表示單詞的非邊界 | √ | √ | |
如果上述特殊符號是普通字元的話,需要用
\
轉義,如[A-Z]?{3}
不對而[A-Z]\?{3}
可以正確匹配上 "D???"。
3、貪婪模式和懶惰模式
(1)如何設定?
JavaScript 的正則表示式預設為貪婪模式
(匹配儘量多的字元)。
但如果在 *、 +、? 或 {} 的後面出現?
,將會變為非貪婪的懶惰模式
(匹配儘量少的字元)。
例如,對 "123abc" 應用 /\d+/ 將會返回 "123",如果使用 /\d+?/,那麼就只會匹配到 "1"。
(2)有何區別?
上圖裡有個術語叫回溯
,會影響到正則匹配字串的效能,在平時寫正則的時候需要注意,詳細請看下文的三、效能問題
注:java 的正則裡還有一種模式叫
獨佔模式
,貌似 JS 裡沒有(待求證)。
4、JavaScript 裡使用
(1)建立和 function
// *** 建立正則表示式 ***
// 方法一:/正則表示式主體/修飾符(可選)
var reg1 = /\d/;
// 若 RegExp 是全域性模式
// var reg1 = /\d/g;
// 方法二:new RegExp('正則表示式')
// 注意:不用新增/***/,且由於是字串,所以需要考慮轉義!
// var reg2 = new RegExp("\\d");
// 若 RegExp 是全域性模式
// var reg2 = new RegExp("\\d",'g');
var string = '你好 123 123 js'
// *** RegExp 的方法 - test() exec() ***
console.log(reg1.test(string)); // true or false
console.log(reg1.exec(string)); // [ '1', index: 3, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // [ '1', index: 3, input: '你好 123 123 js' ] (跟上面一樣)
// 若 RegExp 是全域性模式
console.log(reg1.exec(string)); // [ '1', index: 3, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // [ '2', index: 4, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // [ '3', index: 5, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // [ '1', index: 7, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // [ '2', index: 8, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // [ '3', index: 9, input: '你好 123 123 js' ]
console.log(reg1.exec(string)); // null
console.log(reg1.exec(string)); // [[ '1', index: 3, input: '你好 123 123 js' ]
// *** String 的方法 - search() match() replace() split() ***
console.log(string.search(reg1)); // 返回第一個匹配的下標,沒有即是 -1
console.log(string.match(reg1)); // [ '1', index: 3, input: '你好 123 123 js' ] (跟RegExp.exec()一樣)
// 若 RegExp 是全域性模式
console.log(string.match(reg1)); // [ '1', '2', '3', '1', '2', '3' ]
console.log(string.replace(reg1,'替換')); // "你好 替換23 123 js"
// 若 RegExp 是全域性模式
console.log(string.replace(reg1,'$2-$1')); // "你好 替換替換替換 替換替換替換 js"
console.log(string.split(reg1)); // [ '你好 ', '', '', ' ', '', '', ' js' ]
除了
g
表示全域性模式,還有i
表示忽略大小寫,m
表示執行多行匹配。
(2)分組
用捕獲括號()
可以分組,受影響的是 match()、split()、replace() 方法。
var string = '010-12345'
// 無()
var reg1 = /^\d{3}-\d{3,8}$/;
// 有()
var reg2 = /^(\d{3})-(\d{3,8})$/;
// match()
console.log(string.match(reg1));
// [ '010-12345', index: 0, input: '010-12345' ]
console.log(string.match(reg2));
// [ '010-12345', '010', '12345', index: 0, input: '010-12345' ]
// split()
console.log(string.split(reg1));
// [ '', '' ]
console.log(string.split(reg2));
// [ '', '010', '12345', '' ]
// replace() - 注意 $1、$2 的含義
console.log(string.replace(reg1,'替換'));
console.log(string.replace(reg2,'$2-$1')); // 12345-010
三、效能問題
首先,實現正則表示式引擎
有兩種方式:DFA
自動機(Deterministic Final Automata 確定型有窮自動機)和 NFA
自動機(Non deterministic Finite Automaton 不確定型有窮自動機)。
對於這兩種自動機,他們有各自的區別,這裡並不打算深入將它們的原理。簡單地說,DFA 自動機的時間複雜度是線性的,更加簡單穩定,但是功能有限。而 NFA 的時間複雜度比較不穩定,有時候很好,有時候不怎麼好,好不好取決於你寫的正則表示式,但是勝在功能更加強大。
JS 與大多數主流語言的正則引擎選用的都是 NFA。
但需要注意的是,這種正則表示式引擎在進行字元匹配時會發生回溯
(backtracking)。而一旦發生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決於回溯的次數和複雜度。
這裡有兩篇關於 JAVA 中遭遇到由於正則表示式的糟糕效能導致上線後伺服器 CPU 飆到 100% 的血的經驗地分享:
https://zhuanlan.zhihu.com/p/38278481
http://www.cnblogs.com/study-everyday/p/7426862.html
所以,我們更要在平時寫正則表示式的時候,更加注重效能,避免回溯機制帶來的隱患。我們可以用 https://regex101.com/
來測試下match 的時間如何。
四、推薦工具
1、線上匹配
2、檢視正則表示式的具體解析過程
五、參考資料:
1、MDN教程
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions
2、廖雪峰教程