近幾日對自己一直不太擅長的正規表示式做了一次全面的掃盲。心疼自己之餘還是有一些收穫吧,在這裡做一個比較零散的總結,整理一些對理解正則比較有利的點。
一、”?”
你沒有看錯,就是黑人問號中的問號,這個字元在正則裡面算是一個入門中很容易被帶偏的點了。首先要知道什麼是正則中的量詞。
1.量詞
在正則中,通常要表示一個表示式匹配的數量,這個時候量詞就登場了。
主要會使用以下幾個量詞
/(w)*/.exec(str) // 匹配任意次
/(w)+/.exec(str) // 匹配一次到多次
/(w)?/.exec(str) // 匹配零到一次(記住這裡問號的用法!)
/(w){2, 4}/.exec(str) // 匹配兩次到四次
/(w){2, }/.exec(str) // 匹配兩次以上
我們可以發現,在這裡”?”作為一個量詞來使用,表示匹配零到一次。
接下來要理解下一個概念:貪婪匹配
2.貪婪匹配
搜了一下wiki,貌似沒有相關的詞條,通俗的解釋,貪婪匹配模式下,會盡可能多地匹配滿足條件的字元。而正則預設是貪婪模式的。
舉個例子。比如我要匹配”suuuuuuuuuuck”字元中的s和k中間的字元。並沒有什麼問題。
let result = "suuuuuuuuuuck".match(/s(.*)k/)[1]
// uuuuuuuuuuc
但是我現在要搞事情,要你在”suuuuuuuuuuck duck”字串中匹配相同的欄位,同樣的表示式會匹配到以下的結果,因為此時的正則是貪婪的,它一定會匹配到無法匹配的時候才休止。
// uuuuuuuuuuck duc
這時候就需要手動開啟非貪婪模式了
let result = "suuuuuuuuuuck duck".match(/s(.*?)k/)[1]
// uuuuuuuuuuc
區別是在量詞後加了個問號,這時候該捕獲組就算是開啟了非貪婪模式了。
按照上面的理解,如果你是一個新手,肯定會有所疑惑,量詞(*)後面跟著一個量詞(?),這是什麼鬼意思,這麼反人類的?
其實,這裡就涉及到”?”的第二個用法了,即當它跟在一個量詞背後的時候,表示該表示式開啟了非貪婪模式,即對錶達式儘可能少地匹配結果。所以,對應的,配合量詞使用,會產生以下結果。
- “*?”: 可以匹配任意多次,但是儘量少匹配。
- “+?”: 至少必須匹配一次,但是儘量少匹配。
- “{m, n}?”: 至少必須匹配m次,最多隻能匹配n次,但是儘量少匹配。
- “{m, }?”: 至少必須匹配m次,但是儘量少匹配。
思考題:所以,”??” 應該如何匹配呢?
二、捕獲組
正則匹配除了驗證一個字串是否符合條件外,還可以從中提取我們所需要的資訊。比如,我們得到了一個”新中國成立於1949-10-01″的字串,作為一個愛國人士,我們要把這個年月日提取出來謹記於心。所以我寫了一個正則,獲得的結果如下
這裡提取的操作就需要通過小括號進行捕獲。正則會預設對捕獲組分配組數。
"新中國成立於1949-10-01".match(/(d{4})-(d{2})-(d{2})/)
// ["1949-10-01", "1949", "10", "01", index: 6, input: "新中國成立於1949-10-01", groups: undefined]
我們也可以忽略某些分組”(?:exp)”,這樣正則就不會為為其分配組數。
"新中國成立於1949-10-01".match(/(d{4})-(d{2})-(?:d{2})/)
// ["1949-10-01", "1949", "10", index: 6, input: "新中國成立於1949-10-01", groups: undefined]
假如我們有一個疊詞判斷的需求,驗證一個詞語是不是”ABA”格式的,我們可以這麼做
// 首先漢字的unicode範圍是u4e00-u9fa5
// 這裡我們首先對第一個字元進行了捕獲,組數為1
// 然後我們後面通過"1"的方式去複用捕獲組,這樣就意味著匹配到了相同的字元,也就達到了限制的目的。
/([u4e00-u9fa5])[u4e00-u9fa5]1/.test("是不是")
// 當然是true
要記住下標對人類來說還是挺麻煩的,可以說完全沒啥可讀性,當然正則也提供了為分組命名的方式
"新中國成立於1949-10-01".match(/(?<year>d{4})-(?<month>d{2})-(?<date>d{2})/)
// 我們可以發現,這時候捕獲組不僅擁有組數,同時groups屬性不為空了。
// ["1949-10-01", "1949", "10", "01", index: 6, input: "新中國成立於1949-10-01", groups: {…}]
// 展開groups 是這樣的
// {year: "1949", month: "10", date: "01"}
/** 當然命名捕獲組也是可以使用的 */
// (?<name>exp) 命名捕獲組
// k<name> 引用
// 還是疊詞的那個例子
/(?<thx>[u4e00-u9fa5])[u4e00-u9fa5]k<thx>/.test("是不是")
現在有一個需求,匹配出英文語句”I`m singing while you`re dancing”中所有帶有ing字尾的單詞(不包含ing)。要想拿到danc 和 sing,我們需要用到零寬斷言。
三、零寬斷言
零寬斷言用於查詢某些內容之前或之後的東西,只指定一個位置,本身並不佔據字元,這也是為什麼我們稱之為零寬度
對於表示式表示肯定,我們稱之為正向,反之稱之為負向,(注意,這裡的正負指的是對條件的肯定和否定,而不是匹配的方向。)
而對於匹配的方向而言,我們有另外一種稱呼,對向後匹配的稱之為預測先行,向前匹配的稱之為回顧後發
所以,對應的四種組合分別是
- (?=exp) 零寬度正預測先行斷言(斷言自身出現的位置後面能匹配exp)
- (?!exp) 零寬度負預測先行斷言(斷言自身出現的位置後面不能匹配exp)
- (?<=exp) 零寬度正回顧後發斷言(斷言自身出現的位置前面能匹配exp)
- (?<!exp) 零寬度負回顧後發斷言(斷言自身出現的位置前面不能匹配exp)
目前的js引擎對回顧後發斷言的實現還不完全,就我所知在chrome能成功使用,但是在nodejs環境下是不識別的。
現在我們從引言中的例子來實踐一下
"I am singing while you`re dancing".match(/([a-zA-Z]+)(?=ing)/g)
// 我們忽略前面不滿足的匹配,直到index = 4時,s為單詞邊界,滿足條件
// 而第一個捕獲組是貪婪的,他會首先匹配到整個singing,然後將掌控權交給(?=ing),singing不滿足匹配 "singinging"
// 於是開始回溯到單詞 singin,繼續斷言, 匹配到的下一個字元為"g", 不滿足"singining", 又開始回溯到"singi"...
// 直到回溯到"sing"時,斷言後面有一個ing,並且是一個單詞邊界,於是"singing"滿足條件,這時候我們的正則匹配到了第一個結果。
// 由於零寬斷言是不消費字元的,所以我們得到整個表示式匹配的第一個結果是"sing"
// 於是引擎以同樣的方式向後面的位置查詢,得到了danc
// ["sing", "danc"]
我們現在看一下怎麼使用負向斷言,假如我們有一個系統,3月25號要進行維護,不能使用了,這時候有使用者要辦理業務,選擇日期的時候我們要過濾3月25日這一天,所以產品經理要你臨時加上一條規則限定。
選擇後日期輸出的格式是”yyyy-mm-dd”,這時候我們可以這麼寫正則
/(?!2018-03-25)(d{4})-(d{2})-(d{2})/.test("2018-03-11")
// true 通過驗證
/(?!2018-03-25)(d{4})-(d{2})-(d{2})/.test("2018-03-25")
// false
用(?<=exp) 找出 “beep name=wanglihong abcdefg”
"beep name=wanglihong abcdefg".match(/(?<=name=)(w+)/)
// ["wanglihong", "wanglihong", index: 10, input: "beep name=wanglihong abcdefg", groups: undefined]
提取a標籤的屬性的同時,通過(?<!exp) 過濾style屬性
var template = `<a href="/bee" target="_blank" id="o" style="color: black;">點選跳轉</a>`
template.match(/(w+)=(?<!style=)"([^"]+)"/g)
// [href="/bee", target="_blank", id="o"]
摸透了零寬斷言,正則的能力也就算上了一個臺階了,當然還有平衡組這種操作,因為在js不支援,所以就暫時不討論了。