JS 的正則表達式

轉載自https://juejin.im/post/59b5e50f51882519777c4815git

正則表達式

一種幾乎能夠在全部的程序設計語言裏和全部的計算機平臺上使用的文字處理工具。它能夠用來查找特定的信息(搜索),也能夠用來查找並編輯特定的信息(替換)。 核心是 匹配,匹配位置或者匹配字符正則表達式

先簡單的介紹一下語法

基本元字符

  1. . : 匹配除了換行符以外的任何單個字符
  2. \ : 在非特殊字符以前的反斜槓表示下一個字符是特殊的,不能從字面上解釋。例如,沒有前\的'b'一般匹配小寫'b',不管它們出如今哪裏。若是加了'',這個字符變成了一個特殊意義的字符,反斜槓也能夠將其後的特殊字符,轉義爲字面量。例如,模式 /a*/ 表明會匹配 0 個或者多個 a。相反,模式 /a\*/ 將 '*' 的特殊性移除,從而能夠匹配像 "a*" 這樣的字符串。
  3. | : 邏輯或操做符
  4. [] :定義一個字符集合,匹配字符集合中的一個字符,在字符集合裏面像 .\這些字符都表示其自己
  5. [^]:對上面一個集合取非
  6. - :定義一個區間,例如[A-Z],其首尾字符在 ASCII 字符集裏面

數量元字符

  1. {m,n} :匹配前面一個字符至少 m 次至多 n 次重複,還有{m}表示匹配 m 次,{m,}表示至少 m 次
  2. + : 匹配前面一個表達式一次或者屢次,至關於 {1,},記憶方式追加(+),起碼得有一次
  3. * : 匹配前面一個表達式零次或者屢次,至關於 {0,},記憶方式乘法(*),能夠一次都沒有
  4. ? : 單獨使用匹配前面一個表達式零次或者一次,至關於 {0,1},記憶方式,有嗎?,有(1)或者沒有(1),若是跟在任何量詞*,+,?,{}後面的時候將會使量詞變爲非貪婪模式(儘可能匹配少的字符),默認是使用貪婪模式。好比對 "123abc" 應用 /\d+/ 將會返回 "123",若是使用 /\d+?/,那麼就只會匹配到 "1"。

位置元字符

  1. ^ : 單獨使用匹配表達式的開始
  2. $ : 匹配表達式的結束
  3. \b:匹配單詞邊界
  4. \B:匹配非單詞邊界
  5. (?=p):匹配 p 前面的位置
  6. (?!p):匹配不是 p 前面的位置

特殊元字符

  1. \d[0-9],表示一位數字,記憶方式 digit
  2. \D[^0-9],表示一位非數字
  3. \s[\t\v\n\r\f],表示空白符,包括空格,水平製表符(\t),垂直製表符(\v),換行符(\n),回車符(\r),換頁符(\f),記憶方式 space character
  4. \S[^\t\v\n\r\f],表示非空白符
  5. \w[0-9a-zA-Z],表示數字大小寫字母和下劃線,記憶方式 word
  6. \W[^0-9a-zA-Z],表示非單詞字符

標誌字符

  1. g : 全局搜索 記憶方式global
  2. i :不區分大小寫 記憶方式 ignore
  3. m :多行搜索

在 js 中的使用

支持正則的 String 對象的方法

  1. search search 接受一個正則做爲參數,若是參入的參數不是正則會隱式的使用 new RegExp(obj)將其轉換成一個正則,返回匹配到子串的起始位置,匹配不到返回-1數組

  2. match 接受參數和上面的方法一致。返回值是依賴傳入的正則是否包含 g ,若是沒有 g 標識,那麼 match 方法對 string 作一次匹配,若是沒有找到任何匹配的文本時,match 會返回 null ,不然,會返回一個數組,數組第 0 個元素包含匹配到的文本,其他元素放的是正則捕獲的文本,數組還包含兩個對象,index 表示匹配文本在字符串中的位置,input 表示被解析的原始字符串。若是有 g 標識,則返回一個數組,包含每一次的匹配結果bash

    var str = 'For more information, see Chapter 3.4.5.1';
     var re = /see (chapter \d+(\.\d)*)/i;
     var found = str.match(re);
     console.log(found);
     // (3) ["see Chapter 3.4.5.1", "Chapter 3.4.5.1", ".1", index: 22, input: "For more information, see Chapter 3.4.5.1"]
     // 0:"see Chapter 3.4.5.1"
     // 1:"Chapter 3.4.5.1"
     // 2:".1"
     // index:22
     // input:"For more information, see Chapter 3.4.5.1"
     // length:3
     // __proto__:Array(0)
    
     // 'see Chapter 3.4.5.1' 是整個匹配。
     // 'Chapter 3.4.5.1''(chapter \d+(\.\d)*)'捕獲。
     // '.1' 是被'(\.\d)'捕獲的最後一個值。
     // 'index' 屬性(22) 是整個匹配從零開始的索引。
     // 'input' 屬性是被解析的原始字符串。
    複製代碼
    var str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
     var regexp = /[A-E]/gi;
     var matches_array = str.match(regexp);
    
     console.log(matches_array);
     // ['A', 'B', 'C', 'D', 'E', 'a', 'b', 'c', 'd', 'e']
    複製代碼
  3. replace 接受兩個參數,第一個是要被替換的文本,能夠是正則也能夠是字符串,若是是字符串的時候不會被轉換成正則,而是做爲檢索的直接量文本。第二個是替換成的文本,能夠是字符串或者函數,字符串可使用一些特殊的變量來替代前面捕獲到的子串函數

變量名 表明的值
$$ 插入一個 "$"。
$& 插入匹配的子串。
$` 插入當前匹配的子串左邊的內容。
$' 插入當前匹配的子串右邊的內容。
$n 假如第一個參數是 RegExp對象,而且 n 是個小於100的非負整數,那麼插入第 n 個括號匹配的字符串。
​```
var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
// Smith, John
console.log(newstr);
​```
複製代碼

若是是函數的話,函數入參以下,返回替換成的文本工具

變量名 表明的值
match 匹配的子串。(對應於上述的$&。)
p1,p2,... 假如replace()方法的第一個參數是一個RegExp 對象,則表明第n個括號匹配的字符串。(對應於上述的$,1,$2等。)
offset 匹配到的子字符串在原字符串中的偏移量。(好比,若是原字符串是「abcd」,匹配到的子字符串是「bc」,那麼這個參數將是1)
string 被匹配的原字符串。
function replacer(match, p1, p2, p3, offset, string) {
  // p1 is nondigits, p2 digits, and p3 non-alphanumerics
  return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer);
// newString   abc - 12345 - #$*%
複製代碼
  1. split 接受兩個參數,返回一個數組。第一個是用來分割字符串的字符或者正則,若是是空字符串則會將元字符串中的每一個字符以數組形式返回,第二個參數可選做爲限制分割多少個字符,也是返回的數組的長度限制。有一個地方須要注意,用捕獲括號的時候會將匹配結果也包含在返回的數組中post

    var myString = "Hello 1 word. Sentence number 2.";
     var splits = myString.split(/\d/);
    
     console.log(splits);
     // [ "Hello ", " word. Sentence number ", "." ]
    
     splits = myString.split(/(\d)/);
     console.log(splits);
     // [ "Hello ", "1", " word. Sentence number ", "2", "." ]
    複製代碼

正則對象的方法

  1. test 接受一個字符串參數,若是正則表達式與指定的字符串匹配返回 true 不然返回 false網站

  2. execui

    一樣接受一個字符串爲參數,返回一個數組,其中存放匹配的結果。若是未找到匹配,則返回值爲 null。url

    匹配時,返回值跟 match 方法沒有 g 標識時是同樣的。數組第 0 個表示與正則相匹配的文本,後面 n 個是對應的 n 個捕獲的文本,最後兩個是對象 index 和 input

    同時它會在正則實例的 lastIndex 屬性指定的字符處開始檢索字符串 string。當 exec() 找到了與表達式相匹配的文本時,在匹配後,它將把正則實例的 lastIndex 屬性設置爲匹配文本的最後一個字符的下一個位置。

    有沒有 g 標識對單詞執行 exec 方法是沒有影響的,只是有 g 標識的時候能夠反覆調用 exec() 方法來遍歷字符串中的全部匹配文本。當 exec() 再也找不到匹配的文本時,它將返回 null,並把 lastIndex 屬性重置爲 0。

    var string = "2017.06.27";
    var regex2 = /\b(\d+)\b/g;
    console.log( regex2.exec(string) );
    console.log( regex2.lastIndex);
    console.log( regex2.exec(string) );
    console.log( regex2.lastIndex);
    console.log( regex2.exec(string) );
    console.log( regex2.lastIndex);
    console.log( regex2.exec(string) );
    console.log( regex2.lastIndex);
    // => ["2017", "2017", index: 0, input: "2017.06.27"]
    // => 4
    // => ["06", "06", index: 5, input: "2017.06.27"]
    // => 7
    // => ["27", "27", index: 8, input: "2017.06.27"]
    // => 10
    // => null
    // => 0
    複製代碼

    其中正則實例lastIndex屬性,表示下一次匹配開始的位置。

好比第一次匹配了「2017」,開始下標是0,共4個字符,所以此次匹配結束的位置是3,下一次開始匹配的位置是4。

從上述代碼看出,在使用exec時,常常須要配合使用while循環:

var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g;
var result;
while ( result = regex2.exec(string) ) {
    console.log( result, regex2.lastIndex );
}
// => ["2017", "2017", index: 0, input: "2017.06.27"] 4
// => ["06", "06", index: 5, input: "2017.06.27"] 7
// => ["27", "27", index: 8, input: "2017.06.27"] 10
複製代碼

正則的匹配

字符匹配

精確匹配就不說了,好比/hello/,也只能匹配字符串中的"hello"這個子串。 正則表達式之因此強大,是由於其能實現模糊匹配。

匹配多種數量

{m,n}來匹配多種數量,其餘幾種形式(+*?)均可以等價成這種。好比

var regex = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(regex) ); // ["abbc", "abbbc", "abbbbc", "abbbbbc"]
複製代碼

貪婪和非貪婪:

默認貪婪

var regex = /\d{2,5}/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); // ["123", "1234", "12345", "12345"]
複製代碼

兩次後面加一個 ? 就能夠表示非貪婪,非貪婪時

var regex = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); // ["12", "12", "34", "12", "34", "12", "34", "56"]
複製代碼
匹配多種狀況

用字符組[]來匹配多種狀況,其餘幾種形式(\d\D\s\S\w\W)均可以等價成這種。好比

var regex = /a[123]b/g;
var string = "a0b a1b a2b a3b a4b";
console.log( string.match(regex) ); // ["a1b", "a2b", "a3b"]
複製代碼

若是字符組裏面字符特別多的話能夠用-來表示範圍,好比[123456abcdefGHIJKLM],能夠寫成[1-6a-fG-M],用[^0-9]表示非除了數字之外的字符 多種狀況還能夠是多種分支,用管道符來鏈接|,好比

var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) ); // ["good"]
複製代碼

這個例子能夠看出分支結構也是惰性的,匹配到了就再也不日後嘗試了。

例子

掌握這兩種方式就能夠解決比較簡單的正則問題了。

  1. 最多保留2位小數的數字 /^([1-9]\d*|0)(\.\d{1,2})?$/
  2. 電話號碼 /(\+86)?1\d{10}/
  3. 身份證 /^(\d{15}|\d{17}([xX]|\d))$/

位置匹配

什麼是位置

位置是相鄰字符之間的,好比,有一個字符串 hello ,這個字符串一共有6個位置 *h*e*l*l*o* , *表明位置

image
image

  1. ^$ 匹配字符的開頭和結尾,好比 /^hello$/ 匹配一個字符串,要符合這樣的條件,字符串開頭的位置,緊接着是 h 而後是 e,l,l,o 最後是字符串結尾的位置 位置還能夠被替換成字符串,好比 'hello'.replace(/^|$/g, '#') 結果是 #hello#
  2. /b/B 匹配單詞邊界和非單詞邊界,單詞邊界具體指 \w([a-zA-Z0-9_]) 和 \W 之間的位置,包括 \w^ 以及 $ 之間的位置,好比 'hello word [js]_reg.exp-01'.replace(/\b/g, '#') 結果是 #hello# #word# [#js#]#_reg#.#exp#-#01#
  3. (?=p)(?!p) 匹配 p 前面的位置和不是 p 前面位置,好比 'hello'.replace(/(?=l)/g, '#') 結果是 he#l#lo 'hello'.replace(/(?!l)/g, '#') 結果是 #h#ell#o#
位置的特性

字符與字符之間的位置能夠是多個。在理解上能夠將位置理解成空字符串 '',好比 hello 能夠是通常的 '' + 'h' + 'e' + 'l' + 'l' + 'o' + '',也能夠是 '' + '' + '' + '' + 'h' + 'e' + 'l' + 'l' + 'o' + '', 因此/^h\Be\Bl\Bl\Bo$/.test('hello') 結果是 true,/^^^h\B\B\Be\Bl\Bl\Bo$$$/.test('hello') 結果也是 true

例子
  1. 千分位,將 123123123 轉換成 123,123,123 數字是從後往前數,也就是以一個或者多個3位數字結尾的位置換成 ',' 就行了,寫成正則就是 123123213.replace(/(?=(\d{3})+$)/g, ',') 可是這樣的話會在最前面也加一個 ',' 這明顯是不對的。因此還得繼續改一下正則 要求匹配到的位置不是開頭,能夠用 /(?!^)(?=(\d{3})+$)/g 來表示。 換種思路來想,能不能是以數字開頭而後加上上面的條件呢,得出這個正則 /\d(?=(\d{3})+$)/g,可是這個正則匹配的結果是 12,12,123,發現這個正則匹配的不是位置而是字符,將數字換成了 ',' 能夠得出結論,若是要求一個正則是匹配位置的話,那麼全部的條件必須都是位置。

分組

分組主要是括號的使用

分組和分支結構

在分支結構中,括號是用來表示一個總體的,(p1|p2),好比要匹配下面的字符串

I love JavaScript
I love Regular Expression
複製代碼

能夠用正則/^I love (JavaScript|Regular Expression)$/ 而不是 /^I love JavaScript|Regular Expression$/ 表示一個總體還好比 /(abc)+/ 一個或者多個 abc 字符串 上面這些使用 () 包起來的地方就叫作分組

'I love JavaScript'.match(/^I love (JavaScript|Regular Expression)$/)
// ["I love JavaScript", "JavaScript", index: 0, input: "I love JavaScript"]
複製代碼

輸出的數組第二個元素,"JavaScript" 就是分組匹配到的內容

引用分組

提取數據

好比咱們要用正則來匹配一個日期格式,yyyy-mm-dd,能夠寫出簡單的正則/\d{4}-\d{2}-\d{2}/,這個正則還能夠改爲分組形式的/(\d{4})-(\d{2})-(\d{2})/ 這樣咱們能夠分別提取出一個日期的年月日,用 String 的 match 方法或者用正則的 exec 方法均可以

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-08-09";
console.log( string.match(regex) ); 
// => ["2017-08-09", "2017", "08", "09", index: 0, input: "2017-08-09"]
複製代碼

也能夠用正則對象構造函數的全局屬性 $1 - $9 來獲取

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-08-09";

regex.test(string); // 正則操做便可,例如
//regex.exec(string);
//string.match(regex);

console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "08"
console.log(RegExp.$3); // "09"
複製代碼
替換

若是想要把 yyyy-mm-dd 替換成格式 mm/dd/yyyy 應該怎麼作。 String 的 replace 方法在第二個參數裏面能夠用 $1 - $9 來指代相應的分組

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-08-09";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); // "08/09/2017"
等價
var result = string.replace(regex, function() {
    return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result); // "08/09/2017"
等價
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-08-09";
var result = string.replace(regex, function(match, year, month, day) {
    return month + "/" + day + "/" + year;
});
console.log(result); // "08/09/2017"
複製代碼

反向引用

以前匹配日期的正則在使用的時候發現還有另外兩種寫法,一共三種

2017-08-09

2017/08/09

2017.08.09
複製代碼

要匹配這三種應該怎麼寫正則,第一反應確定是把上面那個正則改一下/(\d{4})[-/.](\d{2})[-/.](\d{2})/,把 - 改爲 [-/.] 這三種均可以 看上去沒問題,咱們多想一想就會發現,這個正則把 2017-08.09 這種字符串也匹配到了,這個確定是不符合預期的。 這個時候咱們就須要用到反向引用了,反向引用能夠在匹配階段捕獲到分組的內容 /(\d{4})([-/.])(\d{2})\2(\d{2})/

那麼出現括號嵌套怎麼辦,好比
var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
複製代碼

嵌套的括號以左括號爲準

引用了不存在的分組呢

若是在正則裏面引用了前面不存在的分組,這個時候正則會匹配字符自己,好比\1就匹配\1

非捕獲分組

咱們有時候只是想用括號本來的功能而不想捕獲他們。這個時候能夠用(?:p)表示一個非捕獲分組

例子

  1. 駝峯改短橫

    function dash(str) {
    return str.replace(/([A-Z])/g, '-$1').toLowerCase();
    }
    複製代碼
  2. 獲取連接的 search 值連接:https://www.baidu.com?name=jawil&age=23

    function getParamName(attr) {
    
    let match = RegExp(`[?&]${attr}=([^&]*)`) //分組運算符是爲了把結果存到exec函數返回的結果裏
     .exec(window.location.search)
    //["?name=jawil", "jawil", index: 0, input: "?name=jawil&age=23"]
    return match && decodeURIComponent(match[1].replace(/\+/g, ' ')) // url中+號表示空格,要替換掉
    }
    console.log(getParamName('name'))  // "jawil"
    複製代碼
  3. 去掉字符串先後的空格

    function trim(str) {
     return str.replace(/(^\s*)|(\s*$)/g, "")
    }
    複製代碼
  4. 判斷一個數是不是質數

    function isPrime(num) {
    return !/^1?$|^(11+?)\1+$/.test(Array(num+1).join('1'))
    }
    複製代碼

這裏首先是把一個數字變成1組成的字符串,好比11就是 '1111111111' 11個1 而後正則分兩部分,第一部分是匹配空字符串或者1,第二部分是先匹配兩個或者多個1,非貪婪模式,那麼先會匹配兩個1,而後將匹配的兩個1分組,後面就是匹配一個或者多個'2個1',就至關於整除2,若是匹配成功就證實不是質數,若是不成功就會匹配3個1,而後匹配多個3個1,至關於整除3,這樣一直下去會一直整除到本身自己。若是仍是不行就證實這個數字是質數。

回溯

正則是怎麼匹配的

有這麼一個字符串 'abbbc' 和這麼一個正則 /ab{1,3}bbc/ /ab{1,3}bbc/.test('abbbc') 咱們一眼能夠看出來是 true,可是 JavaScript 是怎麼匹配的呢

image
image

回溯

例如咱們上面的例子,回溯的思想是,從問題的某一種狀態(初始狀態)出發,搜索從這種狀態出發所能達到的全部「狀態」,當一條路走到「盡頭」的時候(不能再前進),再後退一步或若干步,從另外一種可能「狀態」出發,繼續搜索,直到全部的「路徑」(狀態)都試探過。這種不斷「前進」、不斷「回溯」尋找解的方法,就稱做「回溯法」

貪婪和非貪婪的匹配都會產生回溯,不一樣的是貪婪的是先儘可能多的匹配,若是不行就吐出一個而後繼續匹配,再不行就再吐出一個,非貪婪的是先儘可能少的匹配。若是不行就再多匹配一個,再不行就再來一個 分支結構也會產生回溯,好比/^(test|te)sts$/.test('tests') 前面括號裏面的匹配過程是先匹配到 test 而後繼續日後匹配匹配到字符 s 的時候仍是成功的,匹配到 st 的時候發現不能匹配, 因此會回到前面的分支結構的其餘分支繼續匹配,若是不行的話再換其餘分支。

讀正則

讀懂其餘人寫的正則也是一個很重要的方面。

結構和操做符

結構:字符字面量、字符組、量詞、錨字符、分組、選擇分支、反向引用。 操做符:

  1. 轉義符 \
  2. 括號和方括號 (...)、(?:...)、(?=...)、(?!...)、[...]
  3. 量詞限定符 {m}、{m,n}、{m,}、?、*、+
  4. 位置和序列 ^ 、$、 \元字符、 通常字符
  5. 管道符(豎槓) |

操做符的優先級是從上到下,由高到低的,因此在分析正則的時候能夠根據優先級來拆分正則,好比 /ab?(c|de*)+|fg/

  1. 由於括號是一個總體,因此/ab?()+|fg/,括號裏面具體是什麼能夠放到後面再分析

  2. 根據量詞和管道符的優先級,因此a, b?, ()+和管道符後面的f, g

  3. 同理分析括號裏面的c|de* => cd, e*

  4. 綜上,這個正則描述的是

    image
    image

    以這種模式來分析,再複雜的正則均可以看懂。有一個可視化的

    正則分析網站

相關文章
相關標籤/搜索