技術趣講 | 60 分鐘搞懂「正則表達式」

力扣leetcode 發佈 2019-12-19T02:46:41+00:00

第一回 初來乍到NPC: "歡迎來到正則表達式的國度,勇士!這裡的每一個人都使用正則表達式,我是這裡的 NPC,每一個來到這裡的人都將由我代為介紹正則世界的規則,至於能領悟到何種境界,就看你的造化了。祝你好運,勇士!"你:"啊,好的,正則表達式......


第一回 初來乍到

NPC: "歡迎來到正則表達式的國度,勇士!這裡的每一個人都使用正則表達式,我是這裡的 NPC,每一個來到這裡的人都將由我代為介紹正則世界的規則,至於能領悟到何種境界,就看你的造化了。祝你好運,勇士!"

你:"啊,好的,正則表達式......有點奇怪的名字,它是什麼呢?"

NPC:"什麼?你還沒有聽過正則表達式,真是一個莽撞的小伙子。看來你也和外面世界的人一樣,每次只有用到字符串匹配 時,才會通過「谷鴿」來我們的國度尋找答案。一群知其然不知其所以然的傢伙。"

說著,NPC 身前浮現出幾個鎏金大字:

正則表達式:用來匹配一系列符合某個規則的字符串的表達式。

"正則的意思是正規、規則。正則表達式的英文名是 Regular Expression,可以直譯為描述某種規則的表達式,一般縮寫為 regex" ,NPC 緩緩說道。


第二回 牛刀小試

NPC:"我先來考考你吧:你如何判斷一個字符串是不是有效的電話號碼?這可是一個非常常見的需求。"

你:"沒問題,我以前確實寫過一份類似的代碼。首先判斷字符串是否是 11 位,再判斷每一位是否都是數字就可以了。"

NPC:"好了好了,快把你這份代碼藏好,這份代碼放到我們正則的國度是會被笑掉大牙的。看看我們國度的人是怎麼實現這份需求的吧!"

你:"啊?如此簡潔的實現,正則強者竟恐怖如斯!"

NPC:"這可不是什麼強者寫的代碼,充其量算是牛刀小試罷了。"


第三回 初窺門徑

NPC:"我先給你講講正則表達式的精確匹配。一個普通的字符串,比如 abc,它如果用來做正則表達式匹配的話,只能匹配自己。也就是說它只能匹配字符串 abc,不能匹配 ab,Abc 等其他任何字符串。"

你:"這好像沒什麼用,需要精確匹配的話,我們可以用 String.equals()函數,不需要用正則吧?"

NPC:"沒錯,正則表達式的精確匹配很少用到。我只是在給你介紹正則表達式的一條基本規則而已。"

NPC:"如果需要匹配的字符串含有特殊字符,那就需要用 \轉義。比如 a&b,在用正則表達式匹配時,需要使用 a\&b,又由於在 Java 字符串中,\ 也是特殊字符,它也需要轉義,所以 a\&b 對應的 Java 字符串是 a\\&b,它是用來匹配 a&b 的。"

你:"這麼說來,這兩個反斜槓的意義竟然還不一樣:一個是正則的轉義,一個是 Java 字符串的轉義。那麼我們之前那個匹配電話號碼的例子裡面, \\d的本意也是 \d嗎?"

NPC:"不錯不錯,算你還有點悟性。\d在正則表達式中表示匹配任意數字,d 是 digital 的簡寫。比如 00\d就可以匹配 000, 007,008等等。"

你:"那麼,00\d可以匹配 0066嗎?"

NPC:"不能,\d只能匹配單個數字。"

你:"那我要怎麼才能匹配多個數字呢?"

NPC:"你可以寫多次,比如 \d\d就能匹配兩個數字,\d\d\d能匹配三個數字,需要匹配幾個數字就寫幾次就行了。"

你:"那我如果要匹配 10000 個數字呢?總不能寫一萬次吧?"

NPC:"那就像我們剛才匹配電話號碼的例子一樣,在 \d 後面打上花括號 {},{n} 表示匹配 n 次。\d{10000} 就表示匹配 10000 個數字。"

你:"原來如此,現在我能完全看懂剛才寫的匹配電話號碼的例子了!"

NPC:"趁熱打鐵,如果要匹配 n ~ m 次,用 {n,m}即可,如果要匹配至少 n次,用 {n,}即可。需要注意,後不能有空格。"

"按照這個寫法,如果要匹配最多 m次,是不是用 {,m}? "你若有所思。
NPC:"剛誇了你有點悟性又被你蠢哭了,最多 m 次需要這麼寫嗎?直接用 {0,m}不就行了嗎?只是因為正無窮不好表示我們才用的 {n,},在正則國度根本沒有 {,m}這樣的寫法。 "

你:"啊,原來如此,我想多了。"


第四回 小有所成

NPC:"正則的基礎規則中,除了 \d,還有 \w和\s,w 是 word 的簡寫,表示匹配一個常用字符,包括字母、數字、下劃線。s 是 space 的簡寫,表示匹配一個空格,包括三種:

  • 空格鍵打出來的空格
  • Tab 鍵打出來的空格
  • 回車鍵打出來的空格"

你:"Tab 鍵打出來的空格和回車鍵打出來的空格?是指 \t和 \n嗎?"

NPC:"完全正確。"

你:"我明白了,我來測試一下。"

NPC:"非常棒,我的勇士!希望這三個基本規則還不至於讓你記昏了頭。不過請放心,沒有其他字母需要記憶了,只有這三個而已。"


第五回 更進一步

NPC:"記住上面三個規則之後,你還可以順帶獲得幾個新的規則。因為正則國度規定:將字母換成大寫,就表示相反的意思。用 \d你可以匹配一個數字,\D則表示匹配一個非數字。"

你:"哈,設計者真是太機智了,大大減少了我這種新手的學習成本。"

NPC:"是的,這非常好記。類似地,\W 可以匹配 \w 不能匹配的字符,\S 可以匹配 \s 不能匹配的字符。"


第六回 漸入佳境

NPC:"有時候,我們對某些位置的字符沒有要求,僅需要占個位置即可。這時候我們就可以用 . 字符。"

你:"那是不是也可以理解為:.可以匹配任意字符。"

NPC:"是的,可以這麼理解。還記得之前說的 {n}表示匹配 n次嗎?有時候,我們對匹配的次數沒有要求,匹配任意次均可,這時,我們就可以用 *字符。"

你:"我有疑問,為什麼第三個表達式也會輸出 true 呢?明明沒有出現數字啊?"

NPC:"那意味著出現了 0 次,* 是指 可以匹配任意次,包括 0 次。也就是說,* 等價於 {0,}"

你:"我感覺比較常見的需求應該是某個字符至少出現一次吧?"

NPC:"那就可以用 +匹配,+表示 至少匹配一次。它等價於 {1,}"

你:"哈哈,看來設計者也發現了這個需求更常用。平時 +號比 *號用得多吧"!你感覺自己猜到了語法設計者的想法,洋洋得意地對 NPC 說道。

"這倒沒人統計過",NPC 白了你一眼,"在我們正則的國度,常常是一個場景一個正則,不存在誰比誰更常用的對比,按照實際場景使用就行了。"

NPC:"還有一種場景,如果某個字符要麼匹配 0 次,要麼匹配 1 次,我們就可以用 ? 匹配。它等價於 {0,1}"

你:" .匹配任意字符;*匹配任意次,包括 0 次;+號匹配至少 1 次,?匹配 0 次或 1 次。我記住了!"


第七回 心浮氣躁

一下子掌握了這麼多的正則匹配規則的你有點飄飄然,於是你對 NPC 說道:"我感覺我已經掌握了夠多的匹配規則,足以應付所有的字符串匹配場景了!"

NPC:"是的,你已經掌握了足夠多的規則,勇士。可先別得意得太早,我再考考你吧。看看匹配電話號碼的程序,如果我們規定電話號碼不能以 0 開頭,應該怎麼寫正則表達式呢?"

"不能以 0 開頭,那就不能用\d{11}了,這......",你抓耳撓腮,為難起來。

這時,調皮的 NPC 學著你剛才的樣子,說道:"我已經掌握了足夠多的匹配規則,足以應付所有的字符串匹配場景了!"

你:"呃,還差一點......快別取笑我了,快告訴我這個要用什麼新的規則吧!"

"年輕人啊,總是心浮氣躁",NPC 搖了搖頭,"這樣的場景需要用 [] 來匹配,[] 用於匹配指定範圍內的字符,比如[123456789] 可以匹配 1~9。"

你:"啊哈,那我就知道怎麼寫了, 這個問題的正則匹配規則是[123456789]\d{10}。"

NPC:"就是這樣。這裡還有一個語法糖,[123456789] 寫起來太麻煩,可以寫作 [1-9]。"

你:"只能用於數字嗎?可以用在字母身上嗎?"

NPC:"當然可以,比如 [a-g] 表示 [abcdefg],[U-Z] 表示 [UVWXYZ]。"

你:"但如果既可以是數字 1~9,又可以是字母 a~g,還可以是字母 U~Z,還是得把所有範圍列出來。"

NPC:"不必,你還可以這麼寫:[1-9a-gU-Z]。"

你:"這可真是太方便了!如果是 0~1,8~9 可以這樣組合嗎?"

NPC:"那樣的話,你寫 [0189] 不是更簡潔嗎?"

你:"我想學習(裝 X)。"

NPC:"那當然也是可以的,[0-18-9] 正是你想要的。由於正則一次只匹配一個字符,所以這樣寫並不會有歧義,也就是說計算機不會把這種寫法誤解成要匹配 0~18 之類的。"

NPC:"還有一種寫法可以實現這一點,那就是用 運算符,正則的 運算符是 |,[0189]也可以寫作 0|1|8|9。"

你:"所以說範圍就是 的簡寫,對嗎?"

NPC:"不對, 可以實現更多的功能,它並不局限於單個字符。"

你:"如果我想排除某些字符呢?比如這個位置不能是 [123]。我記得你之前說正則王國以大寫表示取反,[]要怎麼大寫呢?"

NPC:"[]可沒有大寫之說,[]取反的方式是:[^],比如不能是 [123]的表示方法為 [^123]或者 [^1-3]"

你:"原來如此,我懂了。現在還有什麼規則我沒有學到的嗎?"

NPC:"新手教程到這裡就結束了,這已經足夠你應付許多應用場景了。但我這還有兩本高手秘籍,你想不想學呢?"

你:"高手秘籍!聽著都讓人激動啊,快講講!"


第八回 探囊取物

NPC:"這第一本秘籍的名字叫 探囊取物。考慮一個實際需求,有許許多多以下格式的字符串,你需要用正則表達式匹配出其姓名和年齡。

  • Name:Aurora Age:18
  • 其中還夾雜著一些無關緊要的數據
  • Name:Bob Age:20
  • 錯誤的數據有著各種各樣錯誤的格式
  • Name:Cassin Age:22
  • ..."

你:"沒問題,這已經難不倒我了。讓我想想......觀察字符串的規則,只需要用 Name:\w+\s*Age:\d{1,3} 就能匹配了。"

NPC:"很好!一般來說,下一步你要做的就是取出這些表達式中的姓名和年齡,以便把它們存到資料庫中。"

你:"那我可以用 indexOf 和 subString 函數來取這些值。 "

NPC:"的確可行,但你現在不需要那個蠢辦法了,我的勇士。你已經掌握了正則的力量,在我們正則國度有更簡潔的取值方式。"

NPC:"看吧,只要用 ()將需要取值的地方括起來,傳給 Pattern 對象,再用 Pattern 對象匹配後獲得的 Matcher 對象來取值就行了。每個匹配的值將會按照順序保存在 Matcher 對象的 group 中。"

NPC:"你可以看到我用 ()把 \\w+和 \\d{1,3}分別括起來了,判斷 Pattern 對象與字符串是否匹配的方法是 Matcher.matches(),如果匹配成功,這個函數將返回 true,如果匹配失敗,則返回 false。"

你:"這裡是不是寫錯了,為什麼 group 是從下標 1 開始取值的,計算機不都從 0 開始數嗎?"

NPC:"並沒有寫錯,這是因為 group(0) 被用來保存整個匹配的字符串了。"

你:"原來是這樣,分組可真是太方便了。但我們之前都是用的 String.matches方法來匹配的正則表達式,這裡用的 Pattern 又是什麼呢?"

NPC:"想知道這個問題的答案的話,我們不妨來看一下 String.matches方法的源碼。"

"源碼中調用了 Pattern.matches方法,我們再跟進去。"

你:"啊,我明白了!原來 Pattern 並不是什麼新鮮東西,String.matches內部就是調用的 Pattern,兩種寫法的原理是一模一樣的!"

NPC:"沒錯,並且閱讀源碼之後,你可以發現,每次調用 String.matches函數,都會新建出一個 Pattern 對象。所以如果要用同一個正則表達式多次匹配字符串的話,最佳的做法不是直接調用 String.matches方法,而應該先用正則表達式新建一個 Pattern 對象,然後反覆使用,以提高程序運行效率。"


第九回 移花接木

NPC:"我這第二本秘籍名為 移花接木。再考慮一個實際場景:你有一個讓用戶輸入標籤的輸入框,用戶可以輸入多個標籤。可是你並沒有提示用戶,標籤之前用什麼間隔符號隔開。"

你:"你還別說,我之前真遇到過這個問題。結果用戶的輸入五花八門,有用逗號的,有用分號的,有用空格的,還有用制表符的......"

  • 二分,回溯,遞歸,分治
  • 搜索;查找;旋轉;遍歷
  • 數論 圖論 邏輯 機率

NPC:"那你是怎麼解決的呢?"

你:"用 String.split 函數唄,這個函數我已經用得很熟練了。將各種分隔符號依次傳入嘗試,最後總算是解決了。"

輸出為:

這時,你看到 NPC 露出了心痛的表情:"暴殄天物啊!你這種行為就好比拿著精心打磨的鑽石當電鑽頭,這樣的代碼在我們正則王國是會遭人唾罵的。"

你:"String.split 函數不就是用來分割字符串的嗎?"

NPC:"當然是,但 split 函數可不是你這樣用的,不知你是否看過 split 函數的源碼,這個函數傳入的參數實際上是一個正則表達式。"

你:"啊?但我之前沒寫過正則表達式,分割出來也沒出錯啊!"

NPC:"當然,你忘了我最開始給你講的了嗎?你直接使用字符串,在正則王國屬於精確匹配,只能匹配你寫死的那個字符串。"

你:"原來如此。那麼我應該怎麼做呢?"

NPC:"當然是用正則表達式模糊匹配,只要能匹配成功,就以其分割。"

輸出為:

你:"原來 split 函數這麼強大,我以後不會犯這種錯誤了!"

NPC:"字符串中,可不止這一個函數是傳入的正則表達式,你還記得替換所有匹配字符串用的什麼函數嗎?"

你:"用的是 replaceAll 函數,這個函數不會也是傳的正則表達式吧!"

NPC:"正是這樣,所以我們可以用正則表達式模糊匹配,將符合規則的字符串全部替換掉。比如就現在這個例子,我們可以把用戶輸入的所有數據統一規範為使用 ; 分隔,那我們就可以這樣寫。"

輸出為:

你:"果然是 移花接木,模糊匹配比精確匹配效率高多了!"

NPC:"還不止這一點,在 replaceAll 的第二個參數中,我們可以通過 $1,$2,...來反向引用匹配到的子串。只要將需要引用的部分用 ()括起來就可以了。"

輸出為:

你:"哈,有時候我們不需要替換,只需要將正則匹配出來的部分添加一些前綴或後綴,就可以用這種方式!"

NPC:"完全正確。"


第十回 驀然回首

NPC:"恭喜你學完了所有的正則教程,現在你知道正則表達式是什麼了吧。"

你:"沒錯,以前總感覺正則表達式晦澀難懂,每次用到時就去網上搜索答案,現在看來也不過如此。"

NPC:"說 不過如此 倒是有些託大了,雖然我給你介紹了正則表達式的基本規則,但正則表達式裡面還有不少的學問可以去挖掘的。每種技術都有一個熟能生巧的過程。"

你:"什麼?還有學問?我感覺我已經學完了啊!還有什麼學問,一併給我講了吧!"

NPC:"那你看這樣一道題:給你一些字符串,統計其末尾 e 的個數:

  • LeetCode
  • LeetCodeeee
  • LeetCodeee"

你:"看起來並不難,用 (\w+)(e*) 匹配,再取 group(2) 判斷即可。"

NPC:"你運行一下試試看。"

輸出如下:

你:"怎麼會這樣?我期望的結果是 group1 等於 LeetCod,group2 等於 e 才對啊!"

NPC:"這是因為 e 仍然屬於 \w 能匹配的範疇,正則表達式默認會儘可能多地向後匹配,我們王國將其稱之為 貪婪匹配。"

你:"貪婪匹配,聽起來和貪心算法有異曲同工之妙。"

NPC:"沒錯,貪婪匹配和貪心算法原理是一致的。與之對應的匹配方式叫做 非貪婪匹配,非貪婪匹配 會在能匹配目標字符串的前提下,儘可能少的向後匹配。"

你:"那麼,我要怎樣指定匹配方式為非貪婪匹配呢?"

NPC:"也很簡單,在需要非貪婪匹配的正則表達式後面加個 ? 即可表示非貪婪匹配。"

運行程序,輸出如下:

你:"這裡也用的是 ?,我記得之前 ?表示的是匹配 0 次或者 1 次,兩個符號不會混淆嗎?"

NPC:"不會混淆的,你仔細想一想就能明白了,如果只有一個字符,那就不存在貪婪不貪婪的問題,如果匹配多次,那麼表示非貪婪匹配的 ?前面必有一個標誌匹配次數的符號。所以不會出現混淆。"

你:"最後一個問題,為什麼這裡沒有匹配成 group1 等於 L,group2 等於 ee...... 哦我明白了,如果這樣匹配的話,字符串 LeetCode就無法和正則表達式匹配起來。怪不得非貪婪匹配的定義是 在能匹配目標字符串的前提下,儘可能少的向後匹配。"

NPC:"就是這個原理,看來你是真的完全明白了。"


第十一回 最終考驗

NPC:"天下沒有不散的宴席,是時候說再見了。雖然我能教你的,或是說想與你探討的,還不止這些內容,但授人以魚不如授人以漁,以後遇到正則相關的問題,還是要靠你自己動腦思考。"

你:"這麼快就要告別了嗎?不知道為什麼,竟然還有點捨不得......"

NPC:"我最後再出一道題考考你,你就可以從正則王國順利畢業了。來看下你的題目吧:我們王國有一個人口吃,請你幫忙矯正他。他今天說:肚...子。。好餓........,....早知道.....當.....初...。。。多.....刷.....點。。。力.....扣了.........!"

你:"ez,只需要用 str.replaceAll(__, __) 就可以解決了!"


互動話題:

嘿,說你呢!在留言區寫下你的答案吧!


本文作者:Alpinist Wang

聲明:本文歸 「力扣」 版權所有,如需轉載請聯繫。

關鍵字: