我所認識的JavaScript正規表示式

codeceo發表於2015-03-20

如果說這是一篇關於正規表示式的小結,我更願意把它當做一個手冊。

RegExp 三大方法

本文的RegExp採用直接量語法表示:/pattern/attributes。attributes有三個選擇,i、m和g,m(多行匹配)不常用直接省略,所以一個pattern(匹配模式)可以表示如下:

var pattern = /hello/ig;

i(ignore)表示不區分大小寫(地搜尋匹配),比較簡單,以下例子中不加述說;g(global)表示全域性(搜尋匹配),即找到一個後繼續找下去,相對複雜,以下各種方法中會特別介紹。

既然是RegExp的三大方法,所以都是pattern.test/exec/complie的格式。

  • test

主要功能:檢測指定字串是否含有某個子串(或者匹配模式),返回true或者false。

示例如下:

var s = 'you love me and I love you';
var pattern = /you/;
var ans = pattern.test(s);
console.log(ans); // true

如果attributes用了g,則可以繼續找下去,其中還會涉及lastIndex屬性(參照exec中搭配g的介紹)。

  • exec

主要功能:提取指定字串中的符合要求的子串(或者匹配模式),返回一個陣列存放匹配結果;如果沒有,則返回null。(也可自己寫方法迴圈提取所有或者指定index的資料)

exec可以說是test的升級版本,因為它不僅可以檢測,而且檢測到了可以直接提取結果。

示例如下:

var s = 'you love me and I love you';
var pattern = /you/;
var ans = pattern.exec(s);
console.log(ans); // ["you", index: 0, input: "you love me and I love you"]
console.log(ans.index); // 0
console.log(ans.input); // you love me and I love you

輸出的東西很有意思。此陣列的第 0 個元素是與正規表示式相匹配的文字,第 1 個元素是與 RegExpObject 的第 1 個子表示式相匹配的文字(如果有的話),第 2 個元素是與 RegExpObject 的第 2 個子表示式相匹配的文字(如果有的話),以此類推。

啥叫“與子表示式相匹配的文字”?看下面的例子:

var s = 'you love me and I love you';
var pattern = /y(o?)u/;
var ans = pattern.exec(s);
console.log(ans);   // ["you", "o", index: 0, input: "you love me and I love you"]
console.log(ans.length) // 2

所謂的子表示式就是pattern裡()內的東西(具體可以參考下文對子表示式的介紹)。再看上面例子的陣列長度,是2!!index和input只是陣列屬性(chrome中以上的輸出可能會讓人誤會)。

除了陣列元素和 length 屬性之外,exec() 方法還返回兩個屬性。index 屬性宣告的是匹配文字的第一個字元的位置。input 屬性則存放的是被檢索的字串 string。我們可以看得出,在呼叫非全域性的 RegExp 物件的 exec() 方法時,返回的陣列與呼叫方法 String.match() 返回的陣列是相同的

如果使用 “g” 引數,exec() 的工作原理如下(還是以上的例子 ps:如果test使用g引數類似):

  1. 找到第一個 “you”,並儲存其位置
  2. 如果再次執行 exec(),則從儲存的位置(lastIndex)開始檢索,並找到下一個 “you”,並儲存其位置

當 RegExpObject 是一個全域性正規表示式時,exec() 的行為就稍微複雜一些。它會在 RegExpObject 的 lastIndex 屬性指定的字元處開始檢索字串 string。當 exec() 找到了與表示式相匹配的文字時,在匹配後,它將把 RegExpObject 的 lastIndex 屬性設定為匹配文字的最後一個字元的下一個位置。這就是說,我們可以通過反覆呼叫 exec() 方法來遍歷字串中的所有匹配文字。當 exec() 再也找不到匹配的文字時,它將返回 null,並把 lastIndex 屬性重置為 0。這裡引入lastIndex屬性,這貨只有跟g和test(或者g和exec)三者搭配時才有作用。它是pattern的一個屬性,一個整數,標示開始下一次匹配的字元位置。

例項如下:

var s = 'you love me and I love you';
var pattern = /you/g;
var ans;
do {
  ans = pattern.exec(s);
  console.log(ans);
  console.log(pattern.lastIndex);
}
while (ans !== null)

結果如下:

應該還容易理解,當第三次迴圈時,找不到“you”了,於是返回null,lastIndex值也變成0了。

如果在一個字串中完成了一次模式匹配之後要開始檢索新的字串(仍然使用舊的pattern),就必須手動地把 lastIndex 屬性重置為 0。

  • compile

主要功能:改變當前匹配模式(pattern)

這貨是改變匹配模式時用的,用處不大,略過。詳見JavaScript compile() 方法

String 四大護法

和RegExp三大方法分庭抗禮的是String的四大護法,四大護法有些和RegExp三大方法類似,有的更勝一籌。

既然是String家族下的四大護法,所以肯定是string在前,即str.search/match/replace/split形式。

既然是String的方法,當然引數可以只用字串而不用pattern。

  • search

主要功能:搜尋指定字串中是否含有某子串(或者匹配模式),如有,返回子串在原串中的初始位置,如沒有,返回-1。

是不是和test類似呢?test只能判斷有木有,search還能返回位置!當然test()如果有需要能繼續找下去,而search則會自動忽略g(如果有的話)。例項如下:

var s = 'you love me and I love you';
var pattern = /you/;
var ans = s.search(pattern);
console.log(ans);  // 0

話說和String的indexOf方法有點相似,不同的是indexOf方法可以從指定位置開始查詢,但是不支援正則。

  • match

主要功能:和exec類似,從指定字串中查詢子串或者匹配模式,找到返回陣列,沒找到返回null

match是exec的輕量版,當不使用全域性模式匹配時,match和exec返回結果一致;當使用全域性模式匹配時,match直接返回一個字串陣列,獲得的資訊遠沒有exec多,但是使用方式簡單。

例項如下:

var s = 'you love me and I love you';
console.log(s.match(/you/));    // ["you", index: 0, input: "you love me and I love you"]
console.log(s.match(/you/g));   // ["you", "you"]
  • replace

主要功能:用另一個子串替換指定字串中的某子串(或者匹配模式),返回替換後的新的字串  str.replace(‘搜尋模式’,'替換的內容’)  如果用的是pattern並且帶g,則全部替換;否則替換第一處。

例項如下:

var s = 'you love me and I love you';
console.log(s.replace('you', 'zichi')); // zichi love me and I love you
console.log(s.replace(/you/, 'zichi')); // zichi love me and I love you
console.log(s.replace(/you/g, 'zichi'));    // zichi love me and I love zichi

如果需要替代的內容不是指定的字串,而是跟匹配模式或者原字串有關,那麼就要用到$了(記住這些和$符號有關的東東只和replace有關哦)。

怎麼用?看個例子就明白了。

var s = 'I love you';
var pattern = /love/;
var ans = s.replace(pattern, '$`' + '$&' + "$'");
console.log(ans); // I I love you you

沒錯,’$`’ + ‘$&’ + “$’”其實就相當於原串了!

replace的第二個引數還能是函式,看具體例子前先看一段介紹:

注意:第一個引數是匹配到的子串,接下去是子表示式匹配的值,如果要用子表示式引數,則必須要有第一個引數(表示匹配到的串),也就是說,如果要用第n個引數代表的值,則左邊引數都必須寫出來。最後兩個引數跟exec後返回的陣列的兩個屬性差不多。

var s = 'I love you';
var pattern = /love/;
var ans = s.replace(pattern, function(a) {  // 只有一個引數,預設為匹配到的串(如還有引數,則按序表示子表示式和其他兩個引數)
  return a.toUpperCase();
});
console.log(ans); // I LOVE you
  • split

主要功能:分割字串

字串分割成字串陣列的方法(另有陣列變成字串的join方法)。直接看以下例子:

var s = 'you love me and I love you';
var pattern = 'and';
var ans = s.split(pattern);
console.log(ans);   // ["you love me ", " I love you"]

如果你嫌得到的陣列會過於龐大,也可以自己定義陣列大小,加個引數即可:

var s = 'you love me and I love you';
var pattern = /and/;
var ans = s.split(pattern, 1);
console.log(ans);   // ["you love me "]

RegExp 字元

  • \s 任意空白字元 \S相反 空白字元可以是: 空格符 (space character) 製表符 (tab character) 回車符 (carriage return character) 換行符 (new line character) 垂直換行符 (vertical tab character) 換頁符 (form feed character)
  • \b是正規表示式規定的一個特殊程式碼,代表著單詞的開頭或結尾,也就是單詞的分界處。雖然通常英文的單詞是由空格,標點符號或者換行來分隔的,但是\b並不匹配這些單詞分隔字元中的任何一個,它只匹配一個位置。(和^ $ 以及零寬斷言類似)
  • \w 匹配字母或數字或下劃線   [a-z0-9A-Z_]完全等同於\w

貪婪匹配和懶惰匹配

什麼是貪婪匹配?貪婪匹配就是在正規表示式的匹配過程中,預設會使得匹配長度越大越好。

var s = 'hello world welcome to my world';
var pattern = /hello.*world/;
var ans = pattern.exec(s);
console.log(ans)  // ["hello world welcome to my world", index: 0, input: "hello world welcome to my world"]

以上例子不會匹配最前面的Hello World,而是一直貪心的往後匹配。

那麼我需要最短的匹配怎麼辦?很簡單,加個‘?’即可,這就是傳說中的懶惰匹配,即匹配到了,就不往後找了。

var s = 'hello world welcome to my world';
var pattern = /hello.*?world/;
var ans = pattern.exec(s);
console.log(ans)  // ["hello world", index: 0, input: "hello world welcome to my world"]

懶惰限定符(?)新增的場景如下:

子表示式

  • 表示方式

用一個小括號指定:

var s = 'hello world';
var pattern = /(hello)/;
var ans = pattern.exec(s);
console.log(ans);
  • 子表示式出現場景

在exec中陣列輸出子表示式所匹配的值:

var s = 'hello world';
var pattern = /(h(e)llo)/;
var ans = pattern.exec(s);
console.log(ans); // ["hello", "hello", "e", index: 0, input: "hello world"]

在replace中作為替換值引用:

var s = 'hello world';
var pattern = /(h\w*o)\s*(w\w*d)/;
var ans = s.replace(pattern, '$2 $1')
console.log(ans); // world hello

後向引用 & 零寬斷言

  • 子表示式的序號問題

簡單地說:從左向右,以分組的左括號為標誌,第一個出現的分組的組號為1,第二個為2,以此類推。

複雜地說:分組0對應整個正規表示式實際上組號分配過程是要從左向右掃描兩遍的:第一遍只給未命名組分配,第二遍只給命名組分配--因此所有命名組的組號都大於未命名的組號。可以使用(?:exp)這樣的語法來剝奪一個分組對組號分配的參與權.

後向引用

如果我們要找連續兩個一樣的字元,比如要找兩個連續的c,可以這樣/c{2}/,如果要找兩個連續的單詞hello,可以這樣/(hello){2}/,但是要在一個字串中找連續兩個相同的任意單詞呢,比如一個字串hellohellochinaworldworld,我要找的是hello和world,怎麼找?

這時候就要用後向引用。看具體例子:

var s = 'hellohellochinaworldworld';
var pattern = /(\w+)\1/g;
var a = s.match(pattern);
console.log(a); // ["hellohello", "worldworld"]

這裡的\1就表示和匹配模式中的第一個子表示式(分組)一樣的內容,\2表示和第二個子表示式(如果有的話)一樣的內容,\3 \4 以此類推。(也可以自己命名,詳見參考文獻)

或許你覺得陣列裡兩個hello兩個world太多了,我只要一個就夠了,就又要用到子表示式了。因為match方法裡是不能引用子表示式的值的,我們回顧下哪些方法是可以的?沒錯,exec和replace是可以的!

exec方式:

var s = 'hellohellochinaworldworld';
var pattern = /(\w+)\1/g;
var ans;
do {
  ans = pattern.exec(s);
  console.log(ans);
} while(ans !== null);

// result
// ["hellohello", "hello", index: 0, input: "hellohellochinaworldworld"] index.html:69
// ["worldworld", "world", index: 15, input: "hellohellochinaworldworld"] index.html:69
// null

如果輸出只要hello和world,console.log(ans[1])即可。

replace方式:

var s = 'hellohellochinaworldworld';
var pattern = /(\w+)\1/g;
var ans = [];
s.replace(pattern, function(a, b) {
 ans.push(b);
});
console.log(ans);   // ["hello", "world"]

如果要找連續n個相同的串,比如說要找出一個字串中出現最多的字元:

String.prototype.getMost = function() {
  var a = this.split('');
  a.sort();
  var s = a.join('');
  var pattern = /(\w)\1*/g;
  var a = s.match(pattern);
  a.sort(function(a, b) {
    return a.length < b.length;
  });
  var letter = a[0][0];
  var num = a[0].length;
  return letter + ': ' + num;
}

var s = 'aaabbbcccaaabbbcccccc';
console.log(s.getMost()); // c: 9

如果需要引用某個子表示式(分組),請認準後向引用!

零寬斷言

別被名詞嚇壞了,其實解釋很簡單。

它們用於查詢在某些內容(但並不包括這些內容)之後的東西,也就是說它們像\b,^,$那樣用於指定一個位置,這個位置應該滿足一定的條件(即斷言)

  • (?=exp)

零寬度正預測先行斷言,它斷言自身出現的位置的後面能匹配表示式exp。

// 獲取字串中以ing結尾的單詞的前半部分
var s = 'I love dancing but he likes singing';
var pattern = /\b\w+(?=ing\b)/g;
var ans = s.match(pattern);
console.log(ans); // ["danc", "sing"]
  • (?!exp)

零寬度負預測先行斷言,斷言此位置的後面不能匹配表示式exp

// 獲取第五位不是i的單詞的前四位
var s = 'I love dancing but he likes singing';
var pattern = /\b\w{4}(?!i)/g;
var ans = s.match(pattern);
console.log(ans); // ["love", "like"]

javascript正則只支援前瞻,不支援後瞻((?<=exp)和(?<!exp))。

關於零寬斷言的具體應用可以參考綜合應用一節給字串加千分符。

其他

  • 字元轉義

因為某些字元已經被正規表示式用掉了,比如. * ( ) / \  [],所以需要使用它們(作為字元)時,需要用\轉義

var s = 'http://www.cnblogs.com/zichi/';
var pattern = /http:\/\/www\.cnblogs\.com\/zichi\//;
var ans = pattern.exec(s);
console.log(ans); // ["http://www.cnblogs.com/zichi/", index: 0, input: "http://www.cnblogs.com/zichi/"]
  • 分支條件

如果需要匹配abc裡的任意字母,可以用[abc],但是如果不是單個字母那麼簡單,就要用到分支條件。

分支條件很簡單,就是用|表示符合其中任意一種規則。

var s = "I don't like you but I love you";
var pattern = /I.*(like|love).*you/g;
var ans = s.match(pattern);
console.log(ans); // ["I don't like you but I love you"]

答案執行了貪婪匹配,如果需要懶惰匹配,則:

var s = "I don't like you but I love you";
var pattern = /I.*?(like|love).*?you/g;
var ans = s.match(pattern);
console.log(ans); // ["I don't like you", "I love you"]

綜合應用

  •  去除字串首尾空格(replace)
String.prototype.trim = function() {
  return this.replace(/(^\s*)|(\s*$)/g, "");
};
var s = '    hello  world     ';
var ans = s.trim();
console.log(ans.length);    // 12
  • 給字串加千分符(零寬斷言)
String.prototype.getAns = function() {
  var pattern = /(?=((?!\b)\d{3})+$)/g;
  return this.replace(pattern, ',');
}

var s = '123456789';
console.log(s.getAns());  // 123,456,789
  • 找出字串中出現最多的字元(後向引用)
String.prototype.getMost = function() {
  var a = this.split('');
  a.sort();
  var s = a.join('');
  var pattern = /(\w)\1*/g;
  var a = s.match(pattern);
  a.sort(function(a, b) {
    return a.length < b.length;
  });
  var letter = a[0][0];
  var num = a[0].length;
  return letter + ': ' + num;
}

var s = 'aaabbbcccaaabbbcccccc';
console.log(s.getMost()); // c: 9

常用匹配模式(持續更新)

  1.  只能輸入漢字:/^[\u4e00-\u9fa5]{0,}$/

總結

  1. test:檢查指定字串中有沒有某子串(或某匹配模式),返回true或者false;如有必要可以進行全域性模式搜尋。
  2. exec:檢查指定字串中有沒有某子串(或者匹配模式),如有返回陣列(陣列資訊豐富,可參考上文介紹),如沒有返回null;如有必要可以進行全域性搜尋找出所有子串(或者匹配模式)的資訊,資訊中含有匹配模式中子表示式所對應的字串
  3. compile:修改正規表示式中的pattern
  4. search:檢查指定字串中有沒有某子串(或者匹配模式),如有返回子串(或者匹配模式)在原串中的開始位置,如沒有返回-1。不能進行全域性搜尋。
  5. match:檢查指定字串中有沒有某子串(或者匹配模式),非全域性模式下返回資訊和exec一致;如進行全域性搜尋,直接返回字串陣列。(如不需要關於每個匹配的更多資訊,推薦用match而不是exec)
  6. replace:檢查指定字串中有沒有某子串(或者匹配模式),並用另一個子串代替(該子串可以跟原字串或者搜尋到的子串有關);如啟動g,則全域性替換,否則只替換第一個。replace方法可以引用子表示式所對應的值
  7. split:用特定模式分割字串,返回一個字串陣列;與Array的join方法正好相反。
  8. 子表示式:用括號括起來的正則匹配表示式,用後向引用可以對其進行引用;也可以和exec或者replace搭配獲取其真實匹配值。
  9. 後向引用 :對子表示式所在分組進行引用。
  10. 零寬斷言:和\b ^ 以及$類似的某個位置概念。

相關文章