前端如何理解正則-由淺入深的學習

rudy_zhou發表於2020-04-03

引言

  正則在平時工作中用得蠻多的,比如說驗證文字搜尋文字替換服務配置...。之前就常有同事直接發我規則,讓我寫個正則給他。自己也因為在編輯一個公眾號的內容,需要將圖片上的文字錄入圖文(文章)。於是就想著呼叫百度的圖片識別API,將返回的資料格式化 (通過正則判斷需要獲取的值) 後,再插入網頁版公眾號編輯器,所以對於正則用得更多了。   所以我覺得正則這東西,只需要掌握其中幾個核心的元字元,然後簡單練習一下,再找幾個稍複雜的案例詳細解釋,這樣就能掌握書寫正則的規律,最後再學習縮寫後的常用符號就能完全理解了。

正則簡單語法

除了普通字元,還有一些元字元則具有特殊的含義,比如下面的這些:

元字元描述
\ 正則的轉義符,有三種情況:
1. \ 加上元字元,表示匹配元字元所使用的普通字元,比如要匹配普通字元 \,就要寫\\
2. \ 加上非元字元,組成一種由具體實現方式規定其意義的元字元序列 如\d表示匹配一個數字字元
3. \ 加上任意其他字元,預設情況就是匹配此字元,也就是說,反斜線被忽略了。
^ 匹配文字行首。如果設定了RegExp物件的Multiline屬性, ^也匹配\n\r之後的位置。用到[]元字元中第一位時是取反的意思。
例如:/^abc/ 匹配 abc 開頭的字串。
   /^abc/m 匹配多行 abc 開頭的字串。
$ 匹配文字行尾。如果設定了RegExp物件的Multiline屬性, ^也匹配\n\r之後的位置。
例如:/abc$/ 匹配 abc 結尾的字串。
   /abc$/m 匹配多行 abc 結尾的字串。
| 邏輯 的意思。例如:/a|b/ 匹配 a 或者 b
() () 之間的表示式定義為“”(group),並且將匹配這個表示式的字元儲存到一個臨時區域(一個正規表示式中最多可以儲存9個),它們可以用\1\9 的符號來引用。
例如:/([a-z])\1/,假如第一個括號內的[a-z]匹配到字母 d,那麼\1就相當於d
   以此類推,\2就是第二個括號內匹配到的內容。(後面深入部分舉例講)
[] 帶有 關係的一組資料,並可定義區間。
例如:[abc]匹配abc
   [a-z]匹配az的小寫字母。
   [^a-z]匹配除az之間字元以外的任意單字元,包括空字元。
{} 包含一個(段)數量的量詞,給匹配符新增數量,不能為負整數。
例如:/a{2}/,匹配連續的2a
   /a{2,}/,匹配連續的>=2a
   /a{0,5}/,匹配連續的>=0 && <=5a

當然元字元並不止這麼一點,還有更多。 但是隻要知道以上幾種元字元,就能書寫大部分正則規則了,以下用例子把上面描述的內容實際展示一下。


正則語法練習

獲取字串內[](含)內的資料(使用字串方法 match

var str = '今天學習了[RegExp]物件';
var reg = /\[[a-zA-Z]{0,}\]/;
console.log( str.match(reg) );
// => ["[RegExp]"] 
複製程式碼

這裡就用到了[a-zA-Z],裡面規則是匹配大小寫字母,而緊跟著的{0,},是匹配0個或多個大小寫字母。 前後的\[ \],就是用到了\元字元的第一種情況。 -[a-z][0-9]等等之間屬於連字元,表示之間的意思 [a-z-]z後面的-表示匹配普通字元-,實在不清楚就用\轉義 [a-z]匹配所有小寫字母 [a\-z]匹配a-z

判斷字串是否存在英文以外的字元(使用正則方法 test

var str = 'StackOverflow';
var str2 = '我在TrendyTech上班';

var reg = /[^a-zA-Z]$/;

console.log( reg.test(str) ); // => false
console.log( reg.test(str2) ); // => true
複製程式碼

這裡在[]內用到了^,意思就是取反中括號內的匹配項,整體的意思就是匹配除大小寫字母以外的任意字元。

判斷字串是否為xxxx-xx-xx格式的日期(使用正則方法 test

var str = '2020-01-12';
var str2 = '2020年1月1日';

var reg = /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[01])$/;

console.log( reg.test(str) ); // => true
console.log( reg.test(str2) ); // => false
複製程式碼

這一段正則看起來長,其實拆分一下很簡單,總共分為三部分 第一部分[0-9]{4} 匹配年份,年份為四個數字組成 第二部分(0[1-9]|1[0-2]) 匹配月份,0[1-9] 匹配 01~091[0-2] 匹配 10~12。 第三部分(0[1-9]|[1-2][0-9]|3[01]) 匹配日期,0[1-9] 匹配 0~9[1-2][0-9] 匹配 10~293[01] 匹配 30,31

雖然此正則不是很嚴謹,比如小月和平月沒有31天,不過能說明規則就好。

從字串中獲取日期(使用字串方法 match

var str = '今天是2020-01-12,馬上就放假了';

var reg = /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[01])/;

console.log( str.match(reg)[0] ); // => 2020-01-12
複製程式碼

這次的正則對比上面的只移除了^ $,使用match方法,獲取到了字串中的 xxxx-xx-xx 格式的時間字串。

常用正則分析

好了,以上幾個例子已經能夠把正則基礎的資訊完整講明瞭,那我們再解析幾個常用的正則,最終你會發現,其實看起來很複雜的正則也是一個一個短的邏輯段拼湊而成。

IP 正則的驗證與或獲取

大多數情況,驗證獲取的區別在於是否新增了行首^、行尾$驗證。

var ipReg = /^(((2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2})\.){3}((2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2})$/;
複製程式碼

IP是由 xxx.xxx.xxx.xxx 格式組成,xxx 的值為 0~255,所以我們第一步寫個0~255 的正則。

0~255 的正則 就是 (2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2}

太長了我們在拆分一下,分為 0~199200~255 0~199 的正則是 [0-1]{0,1}[0-9]{1,2}解釋:百位是0-1,匹配0-1次就是可以沒有百位。個位十位取值0-9,匹配1-2次就是0-99之間的數。 200~255 的正則是 2(5[0-5]|[0-4][0-9])解釋:百位固定為2,十位這裡分為5和0-4,5的情況下個位為0-5,0-4的情況下,個位是0-9。

因為0~199200~255 拼接起來就要用()+或|連線起來,就成了上面0~255的正則。

0~255.拼接,就成了 0~255.0~255.0~255.0~255。 這裡由於.是匹配除換行和回車符以外的任意單字元的元字元,所以加斜線\.轉義為普通字元.

因為上面 有重複規律就是 0~255. 出現三次所以用()括起來,再用量詞{3}乘以三。 0~255.0~255.0~255. 的正則就是 ((2(5[0-5]|[0-4][0-9]))|[0-1]{0,1}[0-9]{1,2})\.){3}

加上最後的0~255就是完整匹配IP的正則了。 用一張圖表明:

在這裡插入圖片描述
正則很長,其實可以稍微減短一點,之前說過,元字元\加非元字元,會有一些常用匹配的集合,比如: [0-9] 可以用 \d替換,{0,1}可以用?替換。 簡寫一下上面的規則就是

var ipReg = /^(((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})\.){3}((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})$/;
複製程式碼

像這樣的常用匹配集合有很多,在未熟練掌握正則之前先不要使用,可以把寫完的正則再一一對應替換。

去掉首尾的^ $,用來匹配字串中的IP。

var ipReg = /(((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})\.){3}((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})/;

var str = '這個專案部署在192.168.101.255上面。'

console.log( str.match(ipReg)[0] ); // => 192.168.101.255
複製程式碼

郵箱 正則的驗證與獲取

var emailReg = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,}@([a-zA-Z0-9_-]{1,}\.){1,}[a-zA-Z0-9_-]{1,}$/;
複製程式碼

普通郵箱格式:郵箱名稱由 字母、數字、._-組成,首字母為字母或數字        域名部分由 字母、數字、_-組成,.連線

郵箱名稱正則 [a-zA-Z0-9][a-zA-Z0-9_.-]{1,}解釋:字母、數字開頭,後面跟著字母、數字、_、.、-,重複1次或多次

中間加 @連線

郵箱域名正則 ([a-zA-Z0-9_-]{1,}\.){1,}[a-zA-Z0-9_-]{1,} 拆分為 [a-zA-Z0-9_-]{1,}.,然後組合成xxx.xxx.xxx格式的郵箱域名正則。  解釋:字母、數字、-、_,重複1次或多次

用一張圖表示:

在這裡插入圖片描述
同樣郵箱域名也可以縮寫,元字元 +{1,} 等價,\w 類似 [a-zA-Z0-9_] (這裡是類似,不是等價)。

縮寫後的正則就是

var emailReg = /^[a-zA-Z0-9][\w.-]+@([\w-]+\.){1,}[\w-]+$/;
複製程式碼

還有很多郵箱的規則這裡並不完全匹配,如果要匹配比較特殊的郵箱,比如有中文,可以根據以上所學到的自行新增。

去掉首尾的 ^ $ ,用來匹配字串中的郵箱。

var emailReg = /[a-zA-Z0-9][\w.-]+@([\w-]+\.){1,}[\w-]+/;

var str = '我的google郵箱是zhouyu0229@gmail.com,你的郵箱呢?。'

console.log( str.match(emailReg)[0] ); // => zhouyu0229@gmail.com
複製程式碼

正則深入學習

匹配ASCII碼與Unicode碼錶資料

比如說匹配 @ ,我們不單可以用普通字元@,也可以使用 ASCII碼的八進位制、十六進位制匹配和Unicode碼匹配,看下面例子。

var str = '我是@符號';
var OCTReg = /\100/; // 八進位制ASCII碼
var sexadecimalReg = /\x40/; // 十六進位制ASCII碼
var unicodeReg = /\u0040/; // Unicode碼

console.log( str.match(OCTReg)[0] ); // => @
console.log( str.match(sexadecimalReg)[0] ); // => @
console.log( str.match(unicodeReg)[0] ); // => @
複製程式碼

可以看到,都能匹配到 @ 符號,不但能單個匹配也能區間匹配,比如匹配AD

var str = '我是在AB幼兒園上學,小明在CD幼兒園上學,小剛在EG幼兒園上學';
var OCTReg = /[\101-\104]/g; // 八進位制ASCII碼
var sexadecimalReg = /[\x41-\x44]/g;  // 十六進位制ASCII碼
var unicodeReg = /[\u0041-\u0044]/g; // Unicode碼

console.log( str.match(OCTReg) ); // => ["A", "B", "C", "D"]
console.log( str.match(sexadecimalReg) ); // => ["A", "B", "C", "D"]
console.log( str.match(unicodeReg) ); // => ["A", "B", "C", "D"]
複製程式碼

可以看到把字串內的 A B C D,都取出來了,最後的 g ,是修飾符。(下面解釋) 所以可以用過區間方式匹配兩個碼錶的所有字元,比如用unicdoe匹配中文字元,中文字元的編碼範圍是4E00-9FA5,正則就寫成[\u4E00-\=u9FA5],另外還有很多。完整的ASCIIUnicode碼錶參考。

修飾符g m i

gglobal全域性匹配,預設情況下是非全域性匹配,匹配到一個就結束,全域性匹配是匹配所有資料。還有兩個修飾符分別是 i m iignoreCase 忽略大小寫的意思很好理解 mmultiline 多行匹配,比如 /^a/ 只匹配第一行開頭是否為 a ,而加了m ,就是每一行開頭都匹配。比如下面:

var ignoreCaseStr = '[a-z]和[A-Z]是不同的';
var multilineStr = `a同學大了b同學,b同學不滿a同學打了他,就還手打了
a同學`;

var ignoreCaseReg = /[A-Z]/gi;
var multilineReg = /^a/gm;

console.log( multilineStr.replace(multilineReg,'A') );
// => A同學大了b同學,b同學不滿a同學打了他,就還手打了
// => A同學
console.log( ignoreCaseStr.replace(ignoreCaseReg, 'x') );
// => [x-x]和[x-x]是不同的
複製程式碼

可以看到行首的 a 替換成了 A,而中間的沒有,第二個正則內只寫了大寫的字母匹配,加了 i 修飾符,小寫的也被匹配到用replace方法替換成了x

()組的用法

元字元語法中說道,將() 之間的表示式定義為“組”(group),並且將匹配這個表示式的字元儲存到一個臨時區域(一個正規表示式中最多可以儲存9個),它們可以用 \1\9 的符號來引用。特殊用法除外。比如:

var groupReg = /([A-Z])([A-Z])\2/g;
var groupStr = '我們公司有很多ABB格式名字的同事,ABC、AB格式的不多,我們吃飯一般都是AA制';

console.log( groupStr.match(groupReg) ); // => ["ABB"]
複製程式碼

上面輸出了包含 ABB 的陣列,正則的意思就是第一個() 內的匹配到A ,如果後面要引用 就用\1 。但我們這裡的例子用的 \2,就是用的第二個() 內匹配到的資料,也就是B,所以\2 內臨時存的就是 B,因此這裡只能匹配第二、第三個字母相同的資料。

用上面寫過的獲取日期格式的正則來描述一下()組的用法。

var str = '今天是2020-01-12,天氣很好。';

var reg = /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[01])/;

console.log( str.replace(reg, `$1年$2月$3日`) ); // => 今天是2020年01月12日,天氣很好。
複製程式碼

這裡可以看到2020-01-12 替換成了 2020年01月12日,因為上面有三個()組,分別\1 存了年、\2存月、\3存 日,然後使用replace。這裡在替換中的用法就是$ + number,和在正則中使用\+ number,是一樣的,都是一一對應的,並且也最多支援臨時存9個。

零寬斷言 正則的預查

斷言用來宣告一個應該為真的事實。正規表示式中只有當斷言為 時才會繼續進行匹配。 零寬斷言分為以下四種: (?=pattern) 零寬度 正預測先行 斷言(也叫正向肯定預查) 舉例:查詢15歲的小夥伴

var str = '小明17歲,小剛15歲,小紅16歲,小茗15歲';
var reg = /[\u4E00-\u9FA5]{2}(?=15歲)/g;

console.log( str.match(reg) ); // => ["小剛", "小茗"]
複製程式碼

以上正則匹配 15歲 之前的兩個中文字(不包含斷言內的資料),所以輸出了 小剛小茗

(?<=pattern) 零寬度 正回顧後發 斷言(也叫反向肯定預查) 舉例:查詢小茗多少歲

var str = '小明17歲,小剛15歲,小紅16歲,小茗15歲';
var reg = /(?<=小茗)\d{2}/g;

console.log( str.match(reg) ); // => ["15"]
複製程式碼

以上正則查詢小茗後面的 兩個數字(不包含斷言內的資料),所以輸出了 15 (?<=pattern)(?=pattern) 同時使用就可以查某某區間的值,比如:

var str = '<div>我是div裡的內容</div><div>我是第二個div的內容</div>';
var reg = /(?<=<(div)>).*(?=<\/\1>)/;
var reg2 = /(?<=<(div)>).*?(?=<\/\1>)/;

console.log( str.match(reg)[0] ); // => 我是div裡的內容</div><div>我是第二個div的內容
console.log( str.match(reg2)[0] ); // => 我是div裡的內容
複製程式碼

這裡就輸出了 div 標籤裡的內容,但是我們看到了兩種情況。 第一種輸出最前端和最後端div之間的資料,第二種是隻輸出了前面div內的資料。 這也涉及到貪婪模式*+?{n}{n,}{n,m}預設是貪婪模式),這些限制符後加上?,就是非貪婪模式,就像上方的例子一樣,中間的 . 元字元 一個是儘可能的多匹配,一個是儘可能的少匹配。

我們在這裡也使用了前面學到的,第一個() (零寬斷言的括號不存資料)把取到的 div 暫存,在後面用 \1 取了出來,相當於<\/div>/ 需要轉義所以使用了 \/

(?!pattern) 零寬度 負預測先行 斷言(也叫正向否定預查) 舉例:查詢不是 15歲 的小夥伴

var str = '小明17歲,小剛15歲,小紅16歲,小茗15歲';
var reg = /[\u4E00-\u9FA5]{2}(?!15歲)/g;

console.log( str.match(reg) ); // =>  ["小明", "小紅"]
複製程式碼

這個正則就查詢了不是 15歲 結尾的前兩個字(不包含斷言內的資料),輸出了不是15歲的 小明小紅

(?<!pattern) 零寬度 負回顧後發 斷言(也叫反向否定預查) 舉例:查詢 小紅以外 的小夥伴年齡

var str = '小明17歲,小剛15歲,小紅16歲,小茗15歲';
var reg = /(?<!小紅)\d{2}/g;

console.log( str.match(reg) ); // =>  ["17", "15", "15"]
複製程式碼

這個正則就查詢了 小紅以外 後面跟著兩個數字的資料(不包含斷言內的資料),輸出了 小紅以外 其他小夥伴年齡。

零寬斷言之密碼複雜度

零寬斷言不但能匹配資料,同樣也能判斷資料,比如設定判斷密碼複雜度的正則: 規則:密碼必須包含 字母、數字、_,6~32位。

var reg = /(?=.*[a-zA-Z])(?=.*\d)(?=.*_)^\w{6,32}$/;
複製程式碼

在這裡插入圖片描述
這個正則前面的零寬斷言 (?=.*[a-zA-Z])(?=.*\d)(?=.*_) 判斷字串是否出現 字母、數字、_ ,有的話正則就繼續往下執行,直到執行消耗匹配 ^\w{6,32}$ ,判斷字元必須是字母、數字、_ 開頭和結尾。 至於為什麼前面要寫 .*,用兩個連續的零寬斷言測試就能知道了。 (?=[a-zA-Z]) 能判斷字串內是否出現字母 (?=[a-zA-Z])(?=\d) 無法判斷字母或數字是否出現,因為判斷存在衝突。
在這裡插入圖片描述
從這張gif動圖不難發現,當要匹配的資料是字母開頭跟著數字時,斷言數字的正則前方必須寫已斷言匹配到的a,反過來毅然。

因為我們不能控制使用者先輸入字母、數字、_ 中的哪一個,所以我們在 零寬斷言 上加 .* 匹配0個或多個.,是為了不管哪個型別的字元先輸入,或者間隔多少字元再輸入其它型別字元時,判斷其它型別的零寬斷言也能繼續判斷下去(再講一遍 . 是匹配除換行和回車符以外任意單字元的)。

簡單講,不管單個零寬斷言還是多個零寬斷言,都是斷言的字串位置,多個斷言組合起來判斷字串中是否出現這個組合規則的位置,出現就返回true,不出現就返回fasle

以上內容不易理解就多讀幾遍,或者根據gif內容自行測試幾遍。

再寫一個密碼驗證 規則:密碼必須包含 字母、數字、_中的至少兩種6~32位。

var reg = /(?=.*[a-zA-Z\d])(?=.*[\d_])(?=.*[a-zA-Z_])^\w{6,32}$/;
複製程式碼

在這裡插入圖片描述
這裡是三種型別字元取兩種,這裡的 零寬斷言 判斷的就是所有兩兩組合的情況,以此類推,四種型別取三種或者兩種也是可以的,只是組合情況太多正則就比較長,就建議通過程式碼分開判斷。

結語

正則使用的地方有很多,用熟悉了能極大的提高工作效率,比如多數編輯器的搜尋替換都是支援正則的,如果要替換或者搜尋某個規律的欄位,用正則無疑是最方便快捷的方式了。

以上對正則的介紹就這些,看完這篇資訊後,可以去百度百科上看更加詳細的文件介紹,不過要注意,上面也有疏漏的地方,畢竟誰都可以改百度百科的內容。

以上內容如有疏漏,或錯誤的地方歡迎指正,謝謝。

相關文章