正則表達式一直是困擾不少程序員的一門技術,固然也包括曾經的我。大多數時候咱們在開發過程當中要用到某些正則表達式的時候,都會打開谷歌或百度直接搜索而後拷貝粘貼。當下一次再遇到相同問題的時候,一樣的場景又再來一遍。做爲一門用途很廣的技術,我相信深刻理解正則表達式並能融會貫通是值得的。因此,但願這篇文章能幫助你們理清思路,搞懂正則表達式各類符號之間的內在聯繫,造成知識體系,當下次再遇到正則表達式的時候能夠不借助搜索引擎,本身解決。javascript
正則表達式(Regular Expression)其實就是一門工具,目的是爲了字符串模式匹配,從而實現搜索和替換功能。它起源於上個20世紀50年代科學家在數學領域作的一些研究工做,後來才被引入到計算機領域中。從它的命名咱們能夠知道,它是一種用來描述規則的表達式。而它的底層原理也十分簡單,就是使用狀態機的思想進行模式匹配。你們能夠利用regexper.com這個工具很好地可視化本身寫的正則表達式:java
如/\d\w+/
這個正則生成的狀態機圖:git
對於具體的算法實現,你們若是感興趣能夠閱讀《算法導論》。程序員
咱們學習一個系統化的知識,必定要從其基礎構成來了解。正則表達式的基本組成元素能夠分爲:字符和元字符。字符很好理解,就是基礎的計算機字符編碼,一般正則表達式裏面使用的就是數字、英文字母。而元字符,也被稱爲特殊字符,是一些用來表示特殊語義的字符。如^表示非,|表示或等。利用這些元字符,才能構造出強大的表達式模式(pattern)。接下來,咱們就來從這些基本單位出發,來學習一下如何構建正則表達式。github
最簡單的正則表達式能夠由簡單的數字和字母組成,沒有特殊的語義,純粹就是一一對應的關係。如想在'apple'這個單詞裏找到‘a'這個字符,就直接用/a/
這個正則就能夠了。面試
可是若是想要匹配特殊字符的話,就得請出咱們第一個元字符**\
**, 它是轉義字符字符,顧名思義,就是讓其後續的字符失去其原本的含義。舉個例子:正則表達式
我想匹配*
這個符號,因爲*
這個符號自己是個特殊字符,因此我要利用轉義元字符\
來讓它失去其原本的含義:算法
/\*/
複製代碼
若是原本這個字符不是特殊字符,使用轉義符號就會讓它擁有特殊的含義。咱們經常須要匹配一些特殊字符,好比空格,製表符,回車,換行等, 而這些就須要咱們使用轉義字符來匹配。爲了便於記憶,我整理了下面這個表格,並附上記憶方式:chrome
特殊字符 | 正則表達式 | 記憶方式 |
---|---|---|
換行符 | \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個字符。設想一下,若是你要匹配color
和colour
這兩個單詞,就須要同時保證u
這個字符是否出現都能被匹配到。因此你的正則表達式應該是這樣的:/colou?r/
。
元字符*
用來表示匹配0個字符或無數個字符。一般用來過濾某些無關緊要的字符串。
元字符+
適用於要匹配同個字符出現1次或屢次的狀況。
在某些狀況下,咱們須要匹配特定的重複次數,元字符{
和}
用來給重複匹配設置精確的區間範圍。如'a'我想匹配3次,那麼我就使用/a{3}/
這個正則,或者說'a'我想匹配至少兩次就是用/a{2,}/
這個正則。
如下是完整的語法:
- {x}: x次
- {min, max}: 介於min次到max次之間
- {min, }: 至少min次
- {0, 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('');
複製代碼
ps: 感謝評論區提醒,從es2018以後,chrome中的正則表達式也支持反向查找了。不過,在實際項目中還須要注意對舊瀏覽器的支持,以防線上出現Bug。詳情請查看http://kangax.github.io/compat-table/es2016plus/#test-RegExp_Lookbehind_Assertions
最後回顧一下這部份內容:
回溯查找 | 正則 | 記憶方式 |
---|---|---|
引用 | \0,\1,\2 和 $0, $1, $2 | 轉義+數字 |
非捕獲組 | (?:) | 引用表達式(()), 自己不被消費(?),引用(:) |
前向查找 | (?=) | 引用子表達式(()),自己不被消費(?), 正向的查找(=) |
前向負查找 | (?!) | 引用子表達式(()),自己不被消費(?), 負向的查找(!) |
後向查找 | (?<=) | 引用子表達式(()),自己不被消費(?), 後向的(<,開口日後),正的查找(=) |
後向負查找 | (?<!) | 引用子表達式(()),自己不被消費(?), 後向的(<,開口日後),負的查找(!) |
計算機科學就是一門包含邏輯的科學。讓咱們回憶一下編程語言當中用到的三種邏輯關係,與或非。
在正則裏面,默認的正則規則都是與的關係因此這裏不討論。
而非關係,分爲兩種狀況:一種是字符匹配,另外一種是子表達式匹配。在字符匹配的時候,須要使用^
這個元字符。在這裏要着重記憶一下:只有在[
和]
內部使用的^
才表示非的關係。子表達式匹配的非關係就要用到前面介紹的前向負查找子表達式(?!regex)
或後向負查找子表達式(?<!regex)
。
或關係,一般給子表達式進行歸類使用。好比,我同時匹配a,b兩種狀況就可使用(a|b)
這樣的子表達式。
邏輯關係 | 正則元字符 |
---|---|
與 | 無 |
非 | [^regex]和! |
或 | | |
對於正則來講,符號之抽象每每讓不少程序員卻步。針對很差記憶的特色,我經過分類和聯想的方式努力讓其變得有意義。咱們先從一對一的單字符,再到多對多的子字符串介紹,而後經過分組、回溯引用和邏輯處理的方式來構建高級的正則表達式。
在最後,出個經常使用的正則面試題吧:請寫出一個正則來處理數字千分位,如12345
替換爲12,345
。請嘗試本身推理演繹得出答案,而不是依靠搜索引擎:)。
——本文首發於我的公衆號,轉載請註明出處———
最後,歡迎你們關注個人公衆號,一塊兒學習交流。