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