正则表达式不用背
正則表達式一直是困擾很多程序員的一門技術,當然也包括曾經的我。大多數時候我們在開發過程中要用到某些正則表達式的時候,都會打開谷歌或百度直接搜索然后拷貝粘貼。當下一次再遇到相同問題的時候,同樣的場景又再來一遍。作為一門用途很廣的技術,我相信深入理解正則表達式并能融會貫通是值得的。所以,希望這篇文章能幫助大家理清思路,搞懂正則表達式各種符號之間的內在聯系,形成知識體系,當下次再遇到正則表達式的時候可以不借助搜索引擎,自己解決。
正則表達式到底是什么
正則表達式(Regular Expression)其實就是一門工具,目的是為了字符串模式匹配,從而實現搜索和替換功能。它起源于上個20世紀50年代科學家在數學領域做的一些研究工作,后來才被引入到計算機領域中。從它的命名我們可以知道,它是一種用來描述規則的表達式。而它的底層原理也十分簡單,就是使用狀態機的思想進行模式匹配。大家可以利用https://regexper.com這個工具很好地可視化自己寫的正則表達式:
如/\d\w+/這個正則生成的狀態機圖:
對于具體的算法實現,大家如果感興趣可以閱讀《算法導論》。
從字符出發
我們學習一個系統化的知識,一定要從其基礎構成來了解。正則表達式的基本組成元素可以分為:字符和元字符。字符很好理解,就是基礎的計算機字符編碼,通常正則表達式里面使用的就是數字、英文字母。而元字符,也被稱為特殊字符,是一些用來表示特殊語義的字符。如^表示非,|表示或等。利用這些元字符,才能構造出強大的表達式模式(pattern)。接下來,我們就來從這些基本單位出發,來學習一下如何構建正則表達式。
單個字符
最簡單的正則表達式可以由簡單的數字和字母組成,沒有特殊的語義,純粹就是一一對應的關系。如想在'apple'這個單詞里找到‘a'這個字符,就直接用/a/這個正則就可以了。
但是如果想要匹配特殊字符的話,就得請出我們第一個元字符\, 它是轉義字符字符,顧名思義,就是讓其后續的字符失去其本來的含義。舉個例子:
我想匹配*這個符號,由于*這個符號本身是個特殊字符,所以我要利用轉義元字符\來讓它失去其本來的含義:
/\*/如果本來這個字符不是特殊字符,使用轉義符號就會讓它擁有特殊的含義。我們常常需要匹配一些特殊字符,比如空格,制表符,回車,換行等, 而這些就需要我們使用轉義字符來匹配。為了便于記憶,我整理了下面這個表格,并附上記憶方式:
| 換行符 | \n | new line |
| 換頁符 | \f | form feed |
| 回車符 | \r | return |
| 空白符 | \s | space |
| 制表符 | \t | tab |
| 垂直制表符 | \v | vertical tab |
| 回退符 | [\b] | backspace,之所以使用[]符號是避免和\b重復 |
多個字符
單個字符的映射關系是一對一的,即正則表達式的被用來篩選匹配的字符只有一個。而這顯然是不夠的,只要引入集合區間和通配符的方式就可以實現一對多的匹配了。
在正則表達式里,集合的定義方式是使用中括號[和]。如/[123]/這個正則就能同時匹配1,2,3三個字符。那如果我想匹配所有的數字怎么辦呢?從0寫到9顯然太過低效,所以元字符-就可以用來表示區間范圍,利用/[0-9]/就能匹配所有的數字,?/[a-z]/則可以匹配所有的英文小寫字母。
即便有了集合和區間的定義方式,如果要同時匹配多個字符也還是要一一列舉,這是低效的。所以在正則表達式里衍生了一批用來同時匹配多個字符的簡便正則表達式:
| 除了換行符之外的任何字符 | . | 句號,除了句子結束符 |
| 單個數字, [0-9] | \d | digit |
| 除了[0-9] | \D | not?digit |
| 包括下劃線在內的單個字符,[A-Za-z0-9_] | \w | word |
| 非單字字符 | \W | not?word |
| 匹配空白字符,包括空格、制表符、換頁符和換行符 | \s | space |
| 匹配非空白字符 | \S | not?space |
循環與重復
一對一和一對多的字符匹配都講完了。接下來,就該介紹如何同時匹配多個字符。要實現多個字符的匹配我們只要多次循環,重復使用我們的之前的正則規則就可以了。那么根據循環次數的多與少,我們可以分為0次,1次,多次,特定次。
0 | 1
元字符?代表了匹配一個字符或0個字符。設想一下,如果你要匹配color和colour這兩個單詞,就需要同時保證u這個字符是否出現都能被匹配到。所以你的正則表達式應該是這樣的:/colo?r/。
>= 0
元字符*用來表示匹配0個字符或無數個字符。通常用來過濾某些可有可無的字符串。
>= 1
元字符+適用于要匹配同個字符出現1次或多次的情況。
特定次數
在某些情況下,我們需要匹配特定的重復次數,元字符{和}用來給重復匹配設置精確的區間范圍。如'a'我想匹配3次,那么我就使用/a{3}/這個正則,或者說'a'我想匹配至少兩次就是用/a{2,}/這個正則。
以下是完整的語法:
- {x}: x次- {min, max}: 介于min次到max次之間- {min, }: 至少min次- {, max}: 至多max次由于這些元字符比較抽象,且容易混淆,所以我用了聯想記憶的方式編了口訣能保證在用到的時候就能回憶起來。
| 0次或1次 | ? | 且問,此事有還無 |
| 0次或無數次 | * | 宇宙洪荒,辰宿列張:宇宙伊始,從無到有,最后星宿布滿星空 |
| 1次或無數次 | + | 一加, +1 |
| 特定次數 | {x}, {min, max} | 可以想象成一個數軸,從一個點,到一個射線再到線段。min和max分別表示了左閉右閉區間的左界和右界 |
位置邊界
上面我們把字符的匹配都介紹完了,接著我們還需要位置邊界的匹配。在長文本字符串查找過程中,我們常常需要限制查詢的位置。比如我只想在單詞的開頭結尾查找。
單詞邊界
單詞是構成句子和文章的基本單位,一個常見的使用場景是把文章或句子中的特定單詞找出來。如:
The cat scattered his food all over the room.我想找到cat這個單詞,但是如果只是使用/cat/這個正則,就會同時匹配到cat和scattered這兩處文本。這時候我們就需要使用邊界正則表達式\b,其中b是boundary的首字母。在正則引擎里它其實匹配的是能構成單詞的字符(\w)和不能構成單詞的字符(\W)中間的那個位置。
上面的例子改寫成/\bcat\b/這樣就能匹配到cat這個單詞了。
字符串邊界
匹配完單詞,我們再來看一下一整個字符串的邊界怎么匹配。元字符^用來匹配字符串的開頭。而元字符$用來匹配字符串的末尾。注意的是在長文本里,如果要排除換行符的干擾,我們要使用多行模式。試著匹配I am scq000這個句子:
I am scq000. I am scq000. I am scq000.我們可以使用/^I am scq000.$/m這樣的正則表達式,其實m是multiple line的首字母。正則里面的模式除了m外比較常用的還有i和g。前者的意思是忽略大小寫,后者的意思是找到所有符合的匹配。
最后,總結一下:
| 單詞邊界 | \b | boundary |
| 非單詞邊界 | \B | not?boundary |
| 字符串開頭 | ^ | 小頭尖尖那么大個 |
| 字符串結尾 | $ | 終結者,美國科幻電影,美元符$ |
| 多行模式 | m標志 | multiple of lines |
| 忽略大小寫 | i標志 | ignore case, case-insensitive |
| 全局模式 | g標志 | global |
子表達式
字符匹配我們介紹的差不多了,更加高級的用法就得用到子表達式了。通過嵌套遞歸和自身引用可以讓正則發揮更強大的功能。
從簡單到復雜的正則表達式演變通常要采用分組、回溯引用和邏輯處理的思想。利用這三種規則,可以推演出無限復雜的正則表達式。
分組
其中分組體現在:所有以(和)元字符所包含的正則表達式被分為一組,每一個分組都是一個子表達式,它也是構成高級正則表達式的基礎。如果只是使用簡單的(regex)匹配語法本質上和不分組是一樣的,如果要發揮它強大的作用,往往要結合回溯引用的方式。
回溯引用
所謂回溯引用(backreference)指的是模式的后面部分引用前面已經匹配到的子字符串。你可以把它想象成是變量,回溯引用的語法像\1,\2,....,其中\1表示引用的第一個子表達式,\2表示引用的第二個子表達式,以此類推。而\0則表示整個表達式。
假設現在要在下面這個文本里匹配兩個連續相同的單詞,你要怎么做呢?
Hello what what is the first thing, and I am am scq000.利用回溯引用,我們可以很容易地寫出\b(\w+)\s\1這樣的正則。
回溯引用在替換字符串中十分常用,語法上有些許區別,用$1,$2...來引用要被替換的字符串。下面以js代碼作演示:
var str = 'abc abc 123'; str.replace(/(ab)c/g,'$1g'); // 得到結果 'abg abg 123'如果我們不想子表達式被引用,可以使用非捕獲正則(?:regex)這樣就可以避免浪費內存。
var str = 'scq000'. str.replace(/(scq00)(?:0)/, '$1,$2') // 返回scq00,$2 // 由于使用了非捕獲正則,所以第二個引用沒有值,這里直接替換為$2有時,我們需要限制回溯引用的適用范圍。那么通過前向查找和后向查找就可以達到這個目的。
前向查找
前向查找(lookahead)是用來限制后綴的。凡是以(?=regex)包含的子表達式在匹配過程中都會用來限制前面的表達式的匹配。例如happy happily這兩個單詞,我想獲得以happ開頭的副詞,那么就可以使用happ(?=ily)來匹配。如果我想過濾所有以happ開頭的副詞,那么也可以采用負前向查找的正則happ(?!ily),就會匹配到happy單詞的happ前綴。
后向查找
介紹完前向查找,接著我們再來介紹一下它的反向操作:后向查找(lookbehind)。后向查找(lookbehind)是通過指定一個子表達式,然后從符合這個子表達式的位置出發開始查找符合規則的字串。舉個簡單的例子:?apple和people都包含ple這個后綴,那么如果我只想找到apple的ple,該怎么做呢?我們可以通過限制app這個前綴,就能唯一確定ple這個單詞了。
/(?<=app)ple/其中(?<=regex)的語法就是我們這里要介紹的后向查找。regex指代的子表達式會作為限制項進行匹配,匹配到這個子表達式后,就會繼續向后查找。另外一種限制匹配是利用(?<!regex)?語法,這里稱為負后向查找。與正前向查找不同的是,被指定的子表達式不能被匹配到。于是,在上面的例子中,如果想要查找apple的ple也可以這么寫成/(?<!peo)ple。
需要注意的,不是每種正則實現都支持后向查找。在javascript中是不支持的,所以如果有用到后向查找的情況,有一個思路是將字符串進行翻轉,然后再使用前向查找,作完處理后再翻轉回來。看一個簡單的例子:
// 比如我想替換apple的ple為ply var str = 'apple people'; str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');最后回顧一下這部分內容:
| 引用 | \0,\1,\2 和?1, $2 | 轉義+數字 |
| 非捕獲組 | (?:) | 引用表達式(()), 本身不被消費(?),引用(:) |
| 前向查找 | (?=) | 引用子表達式(()),本身不被消費(?), 正向的查找(=) |
| 前向負查找 | (?!) | 引用子表達式(()),本身不被消費(?), 負向的查找(!) |
| 后向查找 | (?<=) | 引用子表達式(()),本身不被消費(?), 后向的(<,開口往后),正的查找(=) |
| 后向負查找 | (?<!) | 引用子表達式(()),本身不被消費(?), 后向的(<,開口往后),負的查找(!) |
邏輯處理
計算機科學就是一門包含邏輯的科學。讓我們回憶一下編程語言當中用到的三種邏輯關系,與或非。
在正則里面,默認的正則規則都是與的關系所以這里不討論。
而非關系,分為兩種情況:一種是字符匹配,另一種是子表達式匹配。在字符匹配的時候,需要使用^這個元字符。在這里要著重記憶一下:只有在[和]內部使用的^才表示非的關系。子表達式匹配的非關系就要用到前面介紹的前向負查找子表達式(?!regex)或后向負查找子表達式(?<!regex)。
或關系,通常給子表達式進行歸類使用。比如,我同時匹配a,b兩種情況就可以使用(a|b)這樣的子表達式。
| 與 | 無 |
| 非 | [^regex]和! |
| 或 | | |
總結
對于正則來說,符號之抽象往往讓很多程序員卻步。針對不好記憶的特點,我通過分類和聯想的方式努力讓其變得有意義。我們先從一對一的單字符,再到多對多的子字符串介紹,然后通過分組、回溯引用和邏輯處理的方式來構建高級的正則表達式。
在最后,出個常用的正則面試題吧:請寫出一個正則來處理數字千分位,如12345替換為12,345。請嘗試自己推理演繹得出答案,而不是依靠搜索引擎:)。
?
轉載自:正則表達式不要背——https://www.cnblogs.com/scq000/p/10875941.html
轉載于:https://www.cnblogs.com/mufengforward/p/10877551.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
- 上一篇: 好程序员大数据独家解析-hadoop五大
- 下一篇: java集合系列之18 spring b