筆記是由油管的@The Coding Train老師釋出系列教程。 因為正則我自己看了很多次,但是很快又忘記。所以為了徹底搞懂,一邊學習一邊記筆記,以給別人講課的方式記筆記,我自己的印象會更深,所以就有了以下內容。 小白的晉級路在個人github會持續更新哦: 傳送門,歡迎star
秋招&面試系列:
正規表示式
1.1. 基本語法
通過一張圖表來對正規表示式的基本進行一個回顧
single char | quantifiers(數量) | position(位置) |
---|---|---|
\d 匹配數字 | * 0個或者更多 | ^一行的開頭 |
\w 匹配word(數字、字母) | + 1個或更多,至少1個 | $一行的結尾 |
\W 匹配非word(數字、字母) | ? 0個或1個,一個Optional | \b 單詞"結界"(word bounds) |
\s 匹配white space(包括空格、tab等) | {min,max}出現次數在一個範圍內 | |
\S 匹配非white space(包括空格、tab等) | {n}匹配出現n次的 | |
. 匹配任何,任何的字元 |
1.1.1. single char
假設你有一段字元如下:
\w
將匹配所有word,當然,() - 等字元除外
\w\w\w
發現匹配的有'The
se are
som
e pho
ne number
s ...'
注意正規表示式是匹配一個連續串的規則,所以可以看到三個字母的單詞可以匹配到,6個單詞的也可以匹配到。
\s\s
匹配到一行中連續兩個空格
quantifiers
假設我們有這一段話:
The colors of the rainbow have many colours
and the rainbow does not have a single colour.
複製程式碼
我們想把所有的顏色找出來colors
colours
colour
答案 colou?rs?
嗯,看起來很簡單,很方便。
好了,現在想要匹配一行中的4個數字,或者一行中的5個字母等,這時候用quantifiers就非常方便了。
我現在想找5個字母組成的單詞
-
\w{5}
這樣可以嗎?嗯..不行的,看下它匹配的內容,如下: 'These
are somephone
numbe
rs 915-555-1234...' 的確,我們模板給的很簡單,它只找一行中,連續出現5個字母的序列。所以現在改進一下好了 -
\w{5}\s
為了能找到單詞,所以我希望5個字母后,跟一個空格的序列,這樣應該可以了吧,看下匹配情況: 'These
are somephone
numbers
915-555-1234...' 嗯,是的,只有目前這些方法,是做不到的。 所以,我們需要第三個工具 "position"
1.1.2. position
回到剛才的問題之前,先熟悉下^
$
和 \b
This is somthing
is about
a blah
words
sequence of words
Hello and
GoodBye and
Go gogo!
複製程式碼
來看下各種正則所匹配的內容
-
\w+
這個應該毫無疑問,匹配所有的words -
^\w+
多了一個^
,這樣子,就只能匹配到每一行開頭的單詞了This
is
a
words
sequence
Hello
GoodBye
Go
-
\w+$
這樣就能匹配到每行的最後一個字母
回到剛才的問題
現在想找5個字母組成的單詞
就變得很簡單了,使用單詞結界符\b
答案就是\b\w{5}\b
1.1.3. 找個電話號碼吧
最後,找一個剛才出現的電話號123-456-1231
用以上最基本的正則方法就是 \d{3}-\d{3}-\d{4}
,這樣就找到了。 但是有的時候,電話號碼是123.456.1234
或者 (212)867-4233
的結構怎麼辦呢?
正規表示式中的或
或者其他表達方式,下面一一來介紹。
1.2. 字元分類(char class)
前面記錄了最基本的方法,接下來說一下分類符[]
這個符號用來表示邏輯關係或
,比如[abc]
表示a或者b或c.[-.]
表示符號-
或者.
號(注意這裡,在[]
中的.
號代表的就是這個符號,但是如果在其外面,表示個匹配所有。 所以如果不在[]
之中,想要匹配'.',就要通過轉意符號\.
)
1.2.1. 分類的簡單應用
字元序列:
The lynk is quite a link don't you think? l nk l(nk
複製程式碼
正規表示式:
l[yi (]nk
結果:
lynk link l nk l(nk
複製程式碼
很容易理解的,就是表達或
邏輯。
1.2.2. 匹配所有可能的電話號碼
好了,現在回到之前遺留的問題,有以下欄位,請匹配所有可能的電話號碼:
These are some phone numbers 915-134-3122. Also,
you can call me at 643.123.1333 and of course,
I'm always reachable at (212)867-5509
複製程式碼
好的,一步一步來,剛才我們使用\d{3}-\d{3}-\d{4}
匹配了連字元的情況。現在我們可以很輕鬆的把.
這種情況加進去了
第一步: \d{3}[-.]\d{3}[-.]\d{4}
第二步: 為了能夠匹配括號,可以使用?來,因為這是一個option選擇。所以最後就成了
\(?\d{3}[-.)]\d{3}[-.]\d{4}
這裡還是要說明,在[]中,特殊字元不需要轉義,可以直接使用,比如[.()]
,但是在外面,是需要轉義的\(
\.
等
1.2.3. []的特殊語法
剛才介紹了最簡單和基本的功能,但是有些特殊的地方需要注意
- -連線符是第一個字元時
比如[-.]
的含義是連字元-
或者點符.
。 但是,如果當連字元不是第一個字元時,比如[a-z]
,這就表示是從字母a到字元z。
- []中的^
^
在之前介紹中,是表示一行開頭,但是在[]
中,有著不同的含義。
[ab]
表示a或者b
[^ab]
啥都行,只要不是a或b(anythings except a and b),相當於取反
1.2.4. []和()
除了使用[]
表示或邏輯,()
也是可以的。用法是(a|b)
表示a或者b
比如下面的例子,匹配所有email
gaoyaqi411@126.com
dyumc@google.net
sam@sjtu.edu
複製程式碼
思路:
首先要想我到底相匹配什麼,這裡我想匹配的是
- 任何一個以words開頭的,一個或更多
\w+
- 緊接著是一個
@
符號\w+@
- 接著有一個或者更多的words
\w+@\w+
- 接著一個
.
標點\w+@\w+\.
- 接著一個
com
net
或edu
\w+@\w+\.(com|net|edu)
還是提醒注意第四步的\.
轉義符號
好了,這樣幾可以匹配以上的所有郵箱了。但是還有一個問題,因為郵箱使用者名稱是可以有.
的,比如vincent.ko@126.com
其實仍然很簡單,修復如下:
[\w.]+@\w+\.(com|net|edu)
1.2.5. 總結
[]
的作用,用英文表達就是"alternation",表達一個或的邏輯;/[-.(]/
在符號中的連字元-
放在第一位表示連字元本身,如果放在中間,表示"從..到..",比如[a-z]
表示a-z[.)]
括號中的特殊符號不需要轉義,就表示其本身[^ab]
括號中的^
表示非,anythings excepta
andb
(a|b)
也可表示選擇,但是它有更強大的功能....
所以,()
的強大功能是什麼呢? 分組捕獲,這對序列的替換、交換是很有幫助的。 後面一節進行學習記錄
1.3. 分組捕獲(capturing groups)
什麼是分組捕獲,現在回到之前電話號碼的例子
212-555-1234
915-412-1333
//我想要保留區號,把後面的電話號碼變為通用性的
????????????
212-xxx-xxxx
915-xxx-xxxx
複製程式碼
按照之前的做法\d{3}-\d{3}-\d{4}
,這種匹配的方式,是將整個電話號碼作為一個組(group)匹配起來。 我們把212-555-1234
這樣的叫Group0
。
這個時候,如果我們加了一個括號\d{3}-(\d{3})-\d{4}
,那麼匹配到的555
就叫Group1
。
以此類推,如果有兩個小括號\d{3}-(\d{3})-(\d{4})
那麼分組就是下面的情況:
212-555-1234 Group0
555 Group1
1234 Group2
複製程式碼
1.3.1. 選擇分組
現在組已經分好,那麼如何選擇已經匹配的分組?
這裡有兩種方法,第一種使用$
符號,比如$1
代表555
,$2
代表1234
;第二種,使用\
,比如\1
代表555
。兩種的使用場景不一樣,先講$
現在為了滿足最開始的要求,我們可以這麼做
reg: \(?(\d{3})[-.)]\d{3}[-.]\d{4}
replace: $1-xxx-xxxx
複製程式碼
ps: 這裡可以直接用JS的replace函式進行操作,但是正則不是JS專屬的,所以這裡先介紹通用方法,之後對JS部分進行總結
1.3.2. 實景訓練
- 現在有一個名單列表,但是姓和名稱是反的,我需要把他交換過來
shiffina, Daniel
shifafl, Daniell
shquer, Danny
...
複製程式碼
實現方法:
reg: (\w+),\s(\w+)
replace: $2 $1
複製程式碼
注意:$0
是所有匹配到的,所以第一個加括號的是$1
- 匹配markdown中的link標籤,並替換為html標籤
[google](http://google.com)
[itp](http://itp.nyu.edu)
[Coding Rainbow](http://codingrainbow.com)
複製程式碼
解析: 這道題有些坑,需要慢慢來。
看到這個,第一個想考慮匹配[google]
這個東西,立馬想到正規表示式\[.*\]
。 這個是巨大的坑,在當前來看,它的確能正確匹配到上面的三條。 但是如果文字是這樣的:
看到了,第一行的內容會全部匹配下來,而不能區分[google]
和[test]
。 之所以這樣,是因為.
是貪婪的,他表示所有,所有能匹配到的,所以當然也包括了]
,一直到這一行的最後一個]
,它才停止。
所以為了讓它能正確匹配,需要去掉這種貪婪的屬性。這裡用到?
。 當?
放在了quantifiers
符號後,表示去掉貪婪屬性,匹配到終止條件,即可停下。
\[.*?\]
這樣子,就可以將[google]
和[test]
分開,效果如下:
接下來完成所有內容:
reg: \[(.*?)\]\((http.*?)\)
replace: <a href="$2">$1</a>
複製程式碼
1.3.3. 使用\
選擇器
$
選擇符是在替換的時候進行的標誌或選擇,但是如果在正規表示式本身,就要使用\
選擇了。比如以下的場景
This is is a a dog , I think think this is is really
a a good good dog. Don't you you thinks so so ?
複製程式碼
我們想要匹配比如is is
so so
這樣連續的序列,就用到了下面的表達方式: (\w+)\s\1
效果:
嗯,差不多達到效果,但是有一些小的bug。比如第一句話This is is a
這個就匹配不準確,會把第一個This的後面字母匹配進去。
這就用到第一節說的字元結界 \b
了,就變成了\b(\w+)\s\1\b
好了,大功告成,就不貼效果圖了,自行腦補就好了。
1.3.4. 總結
- 分組捕獲,使用()進行資料分組,編號0代表整個匹配項,選擇的分組從1號開始
- 選擇器可以使用
$1
和\1
,但是使用場景不同,\
用在正規表示式自己身上 ?
符號可以禁止貪婪屬性,放在.*
之後,表示一次匹配遇到重點就可以停止。否則將會一直向後匹配。
1.4. 在JavaScript中的應用
在js中,主要的正規表示式都是涉及到string的應用。
var str = "hello"
var r = /w+/
複製程式碼
這兩個分別是string和reg的字面量建立方法。當要使用正則來進行操作的時候,使用了r.test()
和str.match()
以及str.replace
等方法。
1.4.1. reg.test()
正規表示式本身有一個test的方法,這個方法只能測試是否包含,返回一個bool變數。
var r = /\d{3}/;
var a = '123';
var b = '123ABC';
var c = 'abc';
r.test(a) //true
r.test(b) //true
r.test(c) //false
複製程式碼
嗯,這個很簡單,而且用的實際不多,下面著重講str上的一些方法。
1.4.2. str.match()
與test()不同,不只是返回bool變數,它會返回你所匹配到的內容。
var r = /compus/
var reg = /w+/
var s = "compus, I know something about you"
r.test(s) //true
s.match(r) //["compus"]
s.match(reg) //["compus"]
複製程式碼
等等,好像有點問題,為什麼最後一個返回的也是"compus"?這不科學。
好吧,實際上,match()返回了第一個可以匹配的序列。想要實現之前的效果,就要用到JS裡關於正則的幾個flag
1.4.2.1. flag
這個標誌就在建立正則的時候就要有的,主要有三個
flag | 含義 |
---|---|
g | 全部的,給我匹配全部的 |
i | 忽略大小寫 |
m | 多行匹配 |
所以為了解決剛才的問題,只要這樣子設定reg就可以了
var reg = /w+/g
複製程式碼
看下面一個練習
var str = "Here is a Phone Number 111-2313 and 133-2311"
var r = /\d{3}[-.]\d{4}/
var rg = /\d{3}[-.]d{4}/g
console.log(str.match(r)); //["111-2313"]
console.log(str.match(rg));//["111-2313","133-2311"]
複製程式碼
嗯,找電話號碼,是的,很方便。但是還有一個問題,剛才說的分組,那麼match會返回分組嗎?
var sr = /(\d{3})[-.]\d{4}/
var srg = /(\d{3})[-.]\d{4}/g
console.log(str.match(sr)); //["111-2313","111"]
console.log(str.match(srg)); //["111-2313","133-2311"]
複製程式碼
所以結論是: 當使用了全域性flagg
的時候,不會返回分組,而是全部的匹配結果;如果沒有使用g
,會將匹配到的結果和分組以陣列的形式返回。
那麼如何實現全域性的分組?
1.4.3. reg.exec()
從字面意思來看,正規表示式的執行方法。 這個方法可以實現匹配全域性,並返回分組的結果。
reg.exec()每次呼叫,返回一個匹配的結果,匹配結果和分組以陣列的形式返回,不斷的呼叫即可返回下一個結果,直到返回
null
var str = "Here is a Phone Number 111-2313 and 133-2311" ;
var srg = /(\d{3})[-.]\d{4}/g;
var result = srg.exec(str);
while(result !== null) {
console.log(result);
result = srg.exec(str);
}
複製程式碼
result包含的內容可能比想象中的多,它是一個陣列,比如第一次執行,他的結果為:
["133-2311", "133", index: 36,
input: "Here is a Phone Number 111-2313 and 133-2311" groups: undefined]
複製程式碼
1.4.4. str.split
現在來到了更強的功能上,先說下split,我們知道split是將字串按照某個字元分隔開,比如有以下一段話,需要將其分割成單詞。
var s = "unicorns and rainbows And, Cupcakes"
複製程式碼
分割成單詞,首先想到的是空格隔開,於是可以用下面方式實現
var result = s.split(' ');
var result1 = s.split(/\s/);
//完全一樣的效果
//["unicorns", "and", "rainbows", "And,", "Cupcakes"]
複製程式碼
嗯,這樣體現不出來正則的強大,而且最主要的是沒有實現要求。因為還有一個"And,"。所以要用正則了,匹配條件是逗號或者空格
result = s.split(/[,\s]/);
//["unicorns", "and", "rainbows", "And", "", "Cupcakes"]
複製程式碼
結果仍然和需要的有出入,因為多了一個""。 我們並不是想讓它分割的依據是逗號或者空格
,依據應該是逗號或空格所在的連續序列
。 在原來的基礎上加一個+
,改成/[,\s]+/
,這個含義就是一個單獨的逗號,或者一個單獨的空格
result = s.split(/[,\s]+/);
// ["unicorns", "and", "rainbows", "And", "Cupcakes"]
複製程式碼
1.4.4.1. 單詞分割
好了,擴充一下,實現一個段落的單詞分割,一個正規表示式就是
result = s.split(/[,.!?\s]+/)
複製程式碼
當然,有個最簡單的方法,我們可以這樣去做
result = s.split(/\W+/);
複製程式碼
接著,如果我們想將一個段落的句子都分隔開,一個可以實現的表示式就是
result = s.split(/[.,!?]+/)
複製程式碼
最後,有一個小需求,就是在分割句子的同時,還想把相應的分隔符保留下來。
var s =
"Hello,My name is Vincent. Nice to Meet you!What's your name? Haha."
複製程式碼
這是一個小小的ponit,記住如果想要保留分隔符,只要給匹配的內容分組即可
var result = s.split(/([.,!?]+)/)
//["Hello", ",", "My name is Vincent", ".", " Nice to Meet you", "!", "What's your name", "?", " Haha", ".", ""]
複製程式碼
可以看到,這樣就會把分隔符也儲存起來。
1.4.5. str.replace()
replace也是字串的方法,它的基本用法是str.replace(reg,replace|function)
,第一個引數是正規表示式,代表匹配的內容,第二個引數是替換的字串或者一個回掉函式。
注意,replace不會修改原字串,只是返回一個修改後的字串;除此外,正規表示式如果沒有使用g
標誌,也和match
一樣,只匹配/替換第一個
1.4.5.1. 最簡單的替換
替換一個序列中的母音字母(aeiou),將其替換成一個double。 比如x->xx
var s = "Hello,My name is Vincent."
var result = s.replace(/([aeiou])/g,"$1$1")
//"Heelloo,My naamee iis Viinceent."
複製程式碼
注意,第二個引數必須是字串; 注意不要忘記加g
1.4.5.2. 牛x哄哄的function引數來了
嗯,這才是最強大的地方,第二引數傳入function,先看一個最簡單的示例
var s = "Hello,My name is Vincent. What is your name?"
var newStr = s.replace(/\b\w{4}\b/g,replacer)
console.log(newStr)
function replacer(match) {
console.log(match);
return match.toUpperCase();
}
/*
name
What
your
name
Hello,My NAME is Vincent. WHAT is YOUR NAME?
*/
複製程式碼
所以,函式的引數是匹配到的內容,返回的是需要替換的內容。好了,基本示例解釋了基本用法,那麼之前討論的分組怎麼辦?如何實現分組呢?
//分組
function replacer(match,group1,group2) {
console.log(group1);
console.log(group2);
}
複製程式碼
如果正規表示式分組處理,那麼在回撥函式中,函式的第二個、第三引數就是group1,group2。這樣子,就可以做很多神奇的事情
1.4.5.3. 綜合練習題
- 判斷一個字串中出現次數最多的字元,並統計次數
var s = 'aaabbbcccaaabbbaaa';
var a = s.split('').sort().join(""); //"aaaaaaaaabbbbbbccc"
var ans = a.match(/(\w)\1+/g);
ans.sort(function(a,b) {
return a.length - b.length;
})
console.log('ans is : ' + ans[ans.length-1])
複製程式碼
1.4.6. 總結
- 在js中,正規表示式字面量
/reg/
和字串字面量"str"
用於建立正則和字串。其中正則上有兩個方法reg.test()
和reg.exec()
reg.test(str)
方法,返回布林變數,用於指示是否有所匹配;reg.exec(str)
有點類似與迭代器,每次執行,返回匹配結果和分組,直到返回為null
結束。- 字串方法主要有
str.match(reg)
,str.split(reg)
和str.replace(reg,str|function)
三種方法。 match
比較特殊,如果正則包含了分組,且沒有g
標誌,則返回匹配內容和分組; 如果沒有分組,且有g
標誌,返回所有匹配內容split
方法主要用於字串分割,如果想要儲存分隔符,記得將匹配內容分組(用小括號包起來)replace
是最強大的方法,當使用回掉函式時,返回值就是替換值; 引數分別為匹配值
group1
group2
...