引言
正則在平時工作中用得蠻多的,比如說驗證、文字搜尋、文字替換、服務配置...。之前就常有同事直接發我規則,讓我寫個正則給他。自己也因為在編輯一個公眾號的內容,需要將圖片上的文字錄入圖文(文章)。於是就想著呼叫百度的圖片識別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] 匹配a 或b 或c 。[a-z] 匹配a 到z 的小寫字母。[^a-z] 匹配除a 到z 之間字元以外的任意單字元,包括空字元。
|
{} | 包含一個(段)數量的量詞,給匹配符新增數量,不能為負整數。 例如: /a{2}/ ,匹配連續的2 個a 。/a{2,}/ ,匹配連續的>=2 個a 。/a{0,5}/ ,匹配連續的>=0 && <=5 個a 。 |
當然元字元並不止這麼一點,還有更多。 但是隻要知道以上幾種元字元,就能書寫大部分正則規則了,以下用例子把上面描述的內容實際展示一下。
正則語法練習
獲取字串內[]
(含)內的資料(使用字串方法 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~09,1[0-2]
匹配 10~12。
第三部分(0[1-9]|[1-2][0-9]|3[01])
匹配日期,0[1-9]
匹配 0~9,[1-2][0-9]
匹配 10~29,3[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~199
,200~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~199
和 200~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] ); // => @
複製程式碼
可以看到,都能匹配到 @
符號,不但能單個匹配也能區間匹配,比如匹配A
到 D
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]
,另外還有很多。完整的ASCII和Unicode碼錶參考。
修飾符g
m
i
g
是 global
全域性匹配,預設情況下是非全域性匹配,匹配到一個就結束,全域性匹配是匹配所有資料。還有兩個修飾符分別是 i
m
i
是 ignoreCase
忽略大小寫的意思很好理解
m
是 multiline
多行匹配,比如 /^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}$/;
複製程式碼
這裡是三種型別字元取兩種,這裡的 零寬斷言 判斷的就是所有兩兩組合的情況,以此類推,四種型別取三種或者兩種也是可以的,只是組合情況太多正則就比較長,就建議通過程式碼分開判斷。
結語
正則使用的地方有很多,用熟悉了能極大的提高工作效率,比如多數編輯器的搜尋替換都是支援正則的,如果要替換或者搜尋某個規律的欄位,用正則無疑是最方便快捷的方式了。
以上對正則的介紹就這些,看完這篇資訊後,可以去百度百科上看更加詳細的文件介紹,不過要注意,上面也有疏漏的地方,畢竟誰都可以改百度百科的內容。
以上內容如有疏漏,或錯誤的地方歡迎指正,謝謝。