正則中關於環視(lookaround)的小例子

InnoFang發表於2018-07-15

剛開始學正規表示式時,環視(lookaround)經常會給初學者造成一定的困擾。但如果能抓住其中的要點,那麼這種困惑就會立刻消失。

環視(lookaround)其實分為兩個部分:前瞻(lookahead)和後視(lookbehind)。

注:這裡的翻譯是基於我個人的理解,其它地方可能還有別的叫法

引言

我們在學一個東西時,我們需要去考慮一下它的使用場景,如果沒有一個明確的目的的話,學到的知識也不會很深刻。

考慮這樣一個場景,我們在做使用者登入或註冊校驗時,會去判斷使用者輸入的密碼是否合法,比如,現在對於使用者輸入的密碼,我們有如下幾點要求:

  • 至少有 6 個字元
  • 包含一個小寫字母
  • 包含一個大寫字母
  • 包含一個數字

如果不使用正規表示式的話,估計大部分人都會用 if 語句去逐個判斷每個要求是否符合。這樣的程式碼初學時也沒有少寫過,不能說這樣寫的方式有什麼不對,但總歸是沒有那麼優雅。

那麼有沒有更好的解決方案呢?可能大部分人也會想到用正規表示式,但如果對正規表示式的瞭解並不是很深入的話,面對這樣的需求,可能會遇到這樣一個問題:怎麼判斷字串中至少含有一個大小寫字母和數字?如果你動手去寫的話,你就會發現你沒法保證寫出來的表示式不考慮順序。

環視(lookaround)的簡單講解

那麼如何解決這個問題?如何能夠通過正規表示式,在不考慮字元出現順序的情況下判斷密碼是否至少包含一個大小寫字母和數字?

這就將用到我們接下來要介紹的關於環視(lookaround)的兩個部分:前瞻(lookahead)和後視(lookbehind)。

當我們在使用前瞻(lookahead)和後視(lookbehind)時,正規表示式在處理字串的過程中,是不會在字串上移動的,也就是說我們可以使用這種技術或者說手段來提前判定字串是否符合一些情況。

那麼,在繼續深入講解之前,我們先來學習一下 4 種環視(lookaround)的寫法,這裡假設你已經具備了基本的正規表示式語法知識。

環視(lookaround) 名稱 做了什麼
(?=foo) 前瞻(lookahead) 判斷緊跟在字串中當前位置後面的內容是否 foo
(?!foo) 否定前瞻(negative lookahead) 判斷緊跟在字串中當前位置後面的內容是否不是 foo
(?<=foo) 後視(lookbehind) 判斷緊跟在字串中當前位置前面的內容是否 foo
(?<!foo) 否定後視(negative lookbehind) 判斷緊跟在字串中當前位置前面的內容是否不是 foo

注:上面的 foo 可以替換為正規表示式,功能將會更強大

環視(lookaround)的簡單示例

理解不了上面的介紹?那麼為了能先讓讀者簡單理解四種環視的寫法的作用,我先講個簡單例子,現在先假設這個當前的字串是 foobarbarfoo

例子 描述
bar(?=bar) 匹配第一個 bar(因為第一個 bar 後面緊跟著一個 bar
bar(?!bar) 匹配第二個 bar(因為第二個 bar 後面沒有緊跟著一個 bar
(?<=foo)bar 匹配第一個 bar(因為第一個 bar 前面緊跟著 foo
(?<!foo)bar 匹配第二個 bar(因為第二個 bar 前面沒有緊跟著 foo

在上面的例子中,都著重強調了 “緊跟著” 這幾個字,這是相對於括號之外的那個 bar 字串而言的,即要判斷緊挨著這個字串 bar 的前後是否符合要求。

解決問題

現在環視(lookaround)的概念以及簡單的示例已經介紹完了,那麼再回到我們開頭講的那個例子:如何用正規表示式去判斷使用者輸入的密碼是否符合要求?

我們先把幾個需求逐步解決。首先第一個:至少有 6 個字元。這個很好解決,保證密碼內容是由大小寫字母及數字組成的,並且長度至少為 6。

^[A-Za-z0-9]{6,}$
複製程式碼

簡單解釋一下吧,^ 用來匹配開頭,$用來匹配結尾,[A-Za-z0-9]表示要匹配的內容是由大小寫字母及數字組成的,{6,}表示長度至少為6。

那麼第二個要求:包含一個小寫字母。前面我們在描述那幾個例子時,都強調了要緊跟在字串當前位置,那麼可能有讀者會困惑,既然都這樣要求了,那麼就無法不強調一個先後順序了,但是事實是,我們可以通過修改表示式來達到這個目的,修改原來的正規表示式,滿足現在的要求:

^(?=.*[a-z])[A-Za-z0-9]{6,}$
複製程式碼

與之前用字串的寫法不同,這裡使用了一個表示式: .*[a-z].用來匹配任何字元,*即匹配 0 個到多個字元,[a-z]即表示小寫字母,這個寫法表示,後面的字串中至少要有一個小寫字母,而小寫字母的前面有什麼,不需要考慮,即只要滿足一堆字串後面有一個小寫字母即可。注意這裡只是一個判斷,正規表示式在環視掃描時,不會在字串上移動,如果沒有符合要求的字元,那麼就會結束掃描。

當然這是貪婪(greedy)模式的寫法,也可以使用懶惰(lazy)模式的寫法: (?=.*?[a-z]),即在 * 後面加一個 ? ,這樣只要有第一個符合要求的字元出現,就會停止匹配,然後繼續掃描。如果你不理解什麼是貪婪(greedy)模式和懶惰(lazy)模式,可以先跳過這一段,文末會有個簡單的解釋。

後面兩個要求:包含一個大寫字母和包含一個數字。原理同第二個要求,這裡就直接給出最終實現了:

^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])[A-Za-z0-9]{6,}$
複製程式碼

值得注意的是,(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])這三個表示式,會先後進行掃描判斷,只要有不符合的就停止,匹配失敗。正規表示式在掃描時不會在字串上移動,所以這三個表示式的寫法並沒有順序。

以上,就解決了我們本文開頭提出的問題,有了正規表示式,你就可以使用你喜歡或者你正在用的程式語言去進行嘗試了。

本文的靈感來自 CodeWarsRegex Password Validation

解決方案也可以參考 我的實現

關於貪婪(greedy)模式和懶惰(lazy)模式

  • 貪婪意味著匹配最長的字串
  • 懶惰意味著匹配最短的字串

舉例來說,給定一個字串 InnoFang

  • 對貪婪模式來說,正規表示式為 I.*n,匹配文字輸出為 InnoFang
  • 對懶惰模式來說,正規表示式為 I.*?n,匹配文字輸出為 InnoFang

兩者在寫法上的區別,就是懶惰模式相比貪婪模式會在諸如 *+? 等限定匹配數量的符號後面加一個 ?

參考

相關文章