者:前端藏經閣
轉發鏈接:https://www.yuque.com/xwifrr/qbgcq0/vkczro
JS正則表達式完整教程(一)「值得收藏」本篇
JS正則表達式完整教程(二)「值得收藏」
小編建議小伙們從第一篇開始看起,更清晰明了
親愛的讀者朋友,如果你點開了這篇文章,說明你對正則很感興趣。
想必你也了解正則的重要性,在我看來正則表達式是衡量程序員水平的一個側面標準。
關于正則表達式的教程,網上也有很多,相信你也看了一些。
與之不同的是,本文的目的是希望所有認真讀完的童鞋們,都有實質性的提高。
本文內容共有七章,用JavaScript語言完整地討論了正則表達式的方方面面。
如果覺得文章某塊兒沒有說明白清楚,歡迎留言,能力范圍之內,老姚必做詳細解答。
具體章節如下:
下面簡單地說說每一章都討論了什么?
正則是匹配模式,要么匹配字符,要么匹配位置。
第1章和第2章以這個角度去講解了正則的基礎。
在正則中可以使用括號捕獲數據,要么在API中進行分組引用,要么在正則里進行反向引用。
這是第3章的主題,講解了正則中括號的作用。
學習正則表達式,是需要了解其匹配原理的。
第4章,講解了正則了正則表達式的回溯法原理。另外在第6章里,也講解了正則的表達式的整體工作原理。
不僅能看懂別人的正則,還要自己會寫正則。
第5章,是從讀的角度,去拆分一個正則表達式,而第6章是從寫的角度,去構建一個正則表達式。
學習正則,是為了在真實世界里應用的。
第7章講解了正則的用法,和相關API需要注意的地方。
如何閱讀本文?
我的建議是閱讀兩遍。第一遍,不求甚解地快速閱讀一遍。閱讀過程中遇到的問題不妨記錄下來,也許閱讀完畢后就能解決很多。然后有時間的話,再帶著問題去精讀第二遍。
深呼吸,開始我們的正則表達式旅程吧。我在終點等你。
正則表達式是匹配模式,要么匹配字符,要么匹配位置。請記住這句話。
然而關于正則如何匹配字符的學習,大部分人都覺得這塊比較雜亂。
畢竟元字符太多了,看起來沒有系統性,不好記。本章就解決這個問題。
內容包括:
如果正則只有精確匹配是沒多大意義的,比如/hello/,也只能匹配字符串中的"hello"這個子串。
var regex=/hello/;
console.log( regex.test("hello") );
//=> true
復制代碼
正則表達式之所以強大,是因為其能實現模糊匹配。
而模糊匹配,有兩個方向上的“模糊”:橫向模糊和縱向模糊。
1.1 橫向模糊匹配
橫向模糊指的是,一個正則可匹配的字符串的長度不是固定的,可以是多種情況的。
其實現的方式是使用量詞。譬如{m,n},表示連續出現最少m次,最多n次。
比如/ab{2,5}c/表示匹配這樣一個字符串:第一個字符是“a”,接下來是2到5個字符“b”,最后是字符“c”。測試如下:
var regex=/ab{2,5}c/g;
var string="abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(regex) );
//=> ["abbc", "abbbc", "abbbbc", "abbbbbc"]
復制代碼
注意:案例中用的正則是/ab{2,5}c/g,后面多了g,它是正則的一個修飾符。表示全局匹配,即在目標字符串中按順序找到滿足匹配模式的所有子串,強調的是“所有”,而不只是“第一個”。g是單詞global的首字母。
1.2 縱向模糊匹配
縱向模糊指的是,一個正則匹配的字符串,具體到某一位字符時,它可以不是某個確定的字符,可以有多種可能。
其實現的方式是使用字符組。譬如[abc],表示該字符是可以字符“a”、“b”、“c”中的任何一個。
比如/a[123]b/可以匹配如下三種字符串:"a1b"、"a2b"、"a3b"。測試如下:
var regex=/a[123]b/g;
var string="a0b a1b a2b a3b a4b";
console.log( string.match(regex) );
//=> ["a1b", "a2b", "a3b"]
復制代碼
以上就是本章講的主體內容,只要掌握橫向和縱向模糊匹配,就能解決很大部分正則匹配問題。
接下來的內容就是展開說了,如果對此都比較熟悉的話,可以跳過,直接看本章案例那節。
需要強調的是,雖叫字符組(字符類),但只是其中一個字符。例如[abc],表示匹配一個字符,它可以是“a”、“b”、“c”之一。
2.1 范圍表示法
如果字符組里的字符特別多的話,怎么辦?可以使用范圍表示法。
比如[123456abcdefGHIJKLM],可以寫成[1-6a-fG-M]。用連字符-來省略和簡寫。
因為連字符有特殊用途,那么要匹配“a”、“-”、“z”這三者中任意一個字符,該怎么做呢?
不能寫成[a-z],因為其表示小寫字符中的任何一個字符。
可以寫成如下的方式:[-az]或[az-]或[a\-z]。即要么放在開頭,要么放在結尾,要么轉義。總之不會讓引擎認為是范圍表示法就行了。
2.2 排除字符組
縱向模糊匹配,還有一種情形就是,某位字符可以是任何東西,但就不能是"a"、"b"、"c"。
此時就是排除字符組(反義字符組)的概念。例如[^abc],表示是一個除"a"、"b"、"c"之外的任意一個字符。字符組的第一位放^(脫字符),表示求反的概念。
當然,也有相應的范圍表示法。
2.3 常見的簡寫形式
有了字符組的概念后,一些常見的符號我們也就理解了。因為它們都是系統自帶的簡寫形式。
\d就是[0-9]。表示是一位數字。記憶方式:其英文是digit(數字)。
\D就是[^0-9]。表示除數字外的任意字符。
\w就是[0-9a-zA-Z_]。表示數字、大小寫字母和下劃線。記憶方式:w是word的簡寫,也稱單詞字符。
\W是[^0-9a-zA-Z_]。非單詞字符。
\s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、換行符、回車符、換頁符。記憶方式:s是space character的首字母。
\S是[^ \t\v\n\r\f]。 非空白符。
.就是[^\n\r]。通配符,表示幾乎任意字符。換行符、回車符、行分隔符和段分隔符除外。記憶方式:想想省略號...中的每個點,都可以理解成占位符,表示任何類似的東西。
如果要匹配任意字符怎么辦?可以使用[\d\D]、[\w\W]、[\s\S]和[^]中任何的一個。
量詞也稱重復。掌握{m,n}的準確含義后,只需要記住一些簡寫形式。
3.1 簡寫形式
{m,} 表示至少出現m次。
{m} 等價于{m,m},表示出現m次。
? 等價于{0,1},表示出現或者不出現。記憶方式:問號的意思表示,有嗎?
+ 等價于{1,},表示出現至少一次。記憶方式:加號是追加的意思,得先有一個,然后才考慮追加。
* 等價于{0,},表示出現任意次,有可能不出現。記憶方式:看看天上的星星,可能一顆沒有,可能零散有幾顆,可能數也數不過來。
3.2 貪婪匹配和惰性匹配
看如下的例子:
var regex=/\d{2,5}/g;
var string="123 1234 12345 123456";
console.log( string.match(regex) );
//=> ["123", "1234", "12345", "12345"]
復制代碼
其中正則/\d{2,5}/,表示數字連續出現2到5次。會匹配2位、3位、4位、5位連續數字。
但是其是貪婪的,它會盡可能多的匹配。你能給我6個,我就要5個。你能給我3個,我就3萬個。反正只要在能力范圍內,越多越好。
我們知道有時貪婪不是一件好事(請看文章最后一個例子)。而惰性匹配,就是盡可能少的匹配:
var regex=/\d{2,5}?/g;
var string="123 1234 12345 123456";
console.log( string.match(regex) );
//=> ["12", "12", "34", "12", "34", "12", "34", "56"]
復制代碼
其中/\d{2,5}?/表示,雖然2到5次都行,當2個就夠的時候,就不再往下嘗試了。
通過在量詞后面加個問號就能實現惰性匹配,因此所有惰性匹配情形如下:
{m,n}? {m,}???+?*?
對惰性匹配的記憶方式是:量詞后面加個問號,問一問你知足了嗎,你很貪婪嗎?
一個模式可以實現橫向和縱向模糊匹配。而多選分支可以支持多個子模式任選其一。
具體形式如下:(p1|p2|p3),其中p1、p2和p3是子模式,用|(管道符)分隔,表示其中任何之一。
例如要匹配"good"和"nice"可以使用/good|nice/。測試如下:
var regex=/good|nice/g;
var string="good idea, nice try.";
console.log( string.match(regex) );
//=> ["good", "nice"]
復制代碼
但有個事實我們應該注意,比如我用/good|goodbye/,去匹配"goodbye"字符串時,結果是"good":
var regex=/good|goodbye/g;
var string="goodbye";
console.log( string.match(regex) );
//=> ["good"]
復制代碼
而把正則改成/goodbye|good/,結果是:
var regex=/goodbye|good/g;
var string="goodbye";
console.log( string.match(regex) );
//=> ["goodbye"]
復制代碼
也就是說,分支結構也是惰性的,即當前面的匹配上了,后面的就不再嘗試了。
匹配字符,無非就是字符組、量詞和分支結構的組合使用罷了。
下面找幾個例子演練一下(其中,每個正則并不是只有唯一寫法):
5.1 匹配16進制顏色值
要求匹配:
#ffbbad
#Fc01DF
#FFF
#ffE
分析:
表示一個16進制字符,可以用字符組[0-9a-fA-F]。
其中字符可以出現3或6次,需要使用量詞和分支結構。
使用分支結構時,需要注意順序。
正則如下:
var regex=/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
var string="#ffbbad #Fc01DF #FFF #ffE";
console.log( string.match(regex) );
//=> ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
復制代碼
5.2 匹配時間
以24小時制為例。
要求匹配:
23:59
02:07
分析:
共4位數字,第一位數字可以為[0-2]。
當第1位為2時,第2位可以為[0-3],其他情況時,第2位為[0-9]。
第3位數字為[0-5],第4位為[0-9]
正則如下:
var regex=/^([01][0-9]|[2][0-3]):[0-5][0-9]$/;
console.log( regex.test("23:59") );
console.log( regex.test("02:07") );
//=> true
//=> true復制代碼
如果也要求匹配7:9,也就是說時分前面的0可以省略。
此時正則變成:
var regex=/^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
console.log( regex.test("23:59") );
console.log( regex.test("02:07") );
console.log( regex.test("7:9") );
//=> true
//=> true
//=> true復制代碼
5.3 匹配日期
比如yyyy-mm-dd格式為例。
要求匹配:
2017-06-10
分析:
年,四位數字即可,可用[0-9]{4}。
月,共12個月,分兩種情況01、02、……、09和10、11、12,可用(0[1-9]|1[0-2])。
日,最大31天,可用(0[1-9]|[12][0-9]|3[01])。
正則如下:
var regex=/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
console.log( regex.test("2017-06-10") );
//=> true
復制代碼
5.4 window操作系統文件路徑
要求匹配:
F:\study\javascript\regex\regular expression.pdf
F:\study\javascript\regex\
F:\study\javascript
F:\
分析:
整體模式是: 盤符:\文件夾\文件夾\文件夾\
其中匹配F:\,需要使用[a-zA-Z]:\,其中盤符不區分大小寫,注意\字符需要轉義。
文件名或者文件夾名,不能包含一些特殊字符,此時我們需要排除字符組[^\:*<>|"?\r\n/]來表示合法字符。另外不能為空名,至少有一個字符,也就是要使用量詞+。因此匹配“文件夾\”,可用[^\:*<>|"?\r\n/]+\。
另外“文件夾\”,可以出現任意詞。也就是([^\:*<>|"?\r\n/]+\)*。其中括號提供的表達式。
路徑的最后一部分可以是“文件夾”,沒有\,因此需要添加([^\:*<>|"?\r\n/]+)?。
最后拼接成了一個看起來比較復雜的正則:
var regex=/^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") );
console.log( regex.test("F:\\study\\javascript\\regex\\") );
console.log( regex.test("F:\\study\\javascript") );
console.log( regex.test("F:\\") );
//=> true
//=> true
//=> true
//=> true復制代碼
其中,JS中字符串表示\時,也要轉義。
5.5 匹配id
要求從
<div id="container" class="main"></div>
提取出id="container"。
可能最開始想到的正則是:
var regex=/id=".*"/
var string='<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
//=> id="container" class="main"
復制代碼
因為.是通配符,本身就匹配雙引號的,而量詞*又是貪婪的,當遇到container后面雙引號時,不會停下來,會繼續匹配,直到遇到最后一個雙引號為止。
解決之道,可以使用惰性匹配:
var regex=/id=".*?"/
var string='<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
//=> id="container"
復制代碼
當然,這樣也會有個問題。效率比較低,因為其匹配原理會涉及到“回溯”這個概念(這里也只是順便提一下,第四章會詳細說明)。可以優化如下:
var regex=/id="[^"]*"/
var string='<div id="container" class="main"></div>';
console.log(string.match(regex)[0]);
//=> id="container"復制代碼
字符匹配相關的案例,挺多的,不一而足。
掌握字符組和量詞就能解決大部分常見的情形,也就是說,當你會了這二者,JS正則算是入門了。
正則表達式是匹配模式,要么匹配字符,要么匹配位置。請記住這句話。
然而大部分人學習正則時,對于匹配位置的重視程度沒有那么高。
本章講講正則匹配位置的總總。
內容包括:
位置是相鄰字符之間的位置。比如,下圖中箭頭所指的地方:
在ES5中,共有6個錨字符:
^ $ \b \B (?=p) (?!p)
2.1 ^和$
^(脫字符)匹配開頭,在多行匹配中匹配行開頭。
$(美元符號)匹配結尾,在多行匹配中匹配行結尾。
比如我們把字符串的開頭和結尾用"#"替換(位置可以替換成字符的!):
var result="hello".replace(/^|$/g, '#');
console.log(result);
//=> "#hello#"
復制代碼
多行匹配模式時,二者是行的概念,這個需要我們的注意:
var result="I\nlove\njavascript".replace(/^|$/gm, '#');
console.log(result);
/*
#I#
#love#
#javascript#
*/
復制代碼
2.2 \b和\B
\b是單詞邊界,具體就是\w和\W之間的位置,也包括\w和^之間的位置,也包括\w和$之間的位置。
比如一個文件名是"[JS] Lesson_01.mp4"中的\b,如下:
var result="[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
//=> "[#JS#] #Lesson_01#.#mp4#"
復制代碼
為什么是這樣呢?這需要仔細看看。
首先,我們知道,\w是字符組[0-9a-zA-Z_]的簡寫形式,即\w是字母數字或者下劃線的中任何一個字符。而\W是排除字符組[^0-9a-zA-Z_]的簡寫形式,即\W是\w以外的任何一個字符。
此時我們可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一個"#",是怎么來的。
知道了\b的概念后,那么\B也就相對好理解了。
\B就是\b的反面的意思,非單詞邊界。例如在字符串中所有位置中,扣掉\b,剩下的都是\B的。
具體說來就是\w與\w、\W與\W、^與\W,\W與$之間的位置。
比如上面的例子,把所有\B替換成"#":
var result="[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result);
//=> "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
復制代碼
2.3 (?=p)和(?!p)
(?=p),其中p是一個子模式,即p前面的位置。
比如(?=l),表示'l'字符前面的位置,例如:
var result="hello".replace(/(?=l)/g, '#');
console.log(result);
//=> "he#l#lo"
復制代碼
而(?!p)就是(?=p)的反面意思,比如:
var result="hello".replace(/(?!l)/g, '#');
console.log(result);
//=> "#h#ell#o#"
復制代碼
二者的學名分別是positive lookahead和negative lookahead。
中文翻譯分別是正向先行斷言和負向先行斷言。
ES6中,還支持positive lookbehind和negative lookbehind。
具體是(?<=p)和(?<!p)。
也有書上把這四個東西,翻譯成環視,即看看右邊或看看左邊。
但一般書上,沒有很好強調這四者是個位置。
比如(?=p),一般都理解成:要求接下來的字符與p匹配,但不能包括p的那些字符。
而在本人看來(?=p)就與^一樣好理解,就是p前面的那個位置。
對于位置的理解,我們可以理解成空字符""。
比如"hello"字符串等價于如下的形式:
"hello"=="" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
復制代碼
也等價于:
"hello"=="" + "" + "hello"
復制代碼
因此,把/^hello$/寫成/^^hello$$$/,是沒有任何問題的:
var result=/^^hello$$$/.test("hello");
console.log(result);
//=> true
復制代碼
甚至可以寫成更復雜的:
var result=/(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
console.log(result);
//=> true
復制代碼
也就是說字符之間的位置,可以寫成多個。
把位置理解空字符,是對位置非常有效的理解方式。
4.1 不匹配任何東西的正則
讓你寫個正則不匹配任何東西
easy,/.^/
因為此正則要求只有一個字符,但該字符后面是開頭。
4.2 數字的千位分隔符表示法
比如把"12345678",變成"12,345,678"。
可見是需要把相應的位置替換成","。
思路是什么呢?
4.2.1 弄出最后一個逗號
使用(?=\d{3}$)就可以做到:
var result="12345678".replace(/(?=\d{3}$)/g, ',')
console.log(result);
//=> "12345,678"
復制代碼
4.2.2 弄出所有的逗號
因為逗號出現的位置,要求后面3個數字一組,也就是\d{3}至少出現一次。
此時可以使用量詞+:
var result="12345678".replace(/(?=(\d{3})+$)/g, ',')
console.log(result);
//=> "12,345,678"
復制代碼
4.2.3 匹配其余案例
寫完正則后,要多驗證幾個案例,此時我們會發現問題:
var result="123456789".replace(/(?=(\d{3})+$)/g, ',')
console.log(result);
//=> ",123,456,789"
復制代碼
因為上面的正則,僅僅表示把從結尾向前數,一但是3的倍數,就把其前面的位置替換成逗號。因此才會出現這個問題。
怎么解決呢?我們要求匹配的到這個位置不能是開頭。
我們知道匹配開頭可以使用^,但要求這個位置不是開頭怎么辦?
easy,(?!^),你想到了嗎?測試如下:
var string1="12345678",
string2="123456789";
reg=/(?!^)(?=(\d{3})+$)/g;
var result=string1.replace(reg, ',')
console.log(result);
//=> "12,345,678"
result=string2.replace(reg, ',');
console.log(result);
//=> "123,456,789"
復制代碼
4.2.4 支持其他形式
如果要把"12345678 123456789"替換成"12,345,678 123,456,789"。
此時我們需要修改正則,把里面的開頭^和結尾$,替換成\b:
var string="12345678 123456789",
reg=/(?!\b)(?=(\d{3})+\b)/g;
var result=string.replace(reg, ',')
console.log(result);
//=> "12,345,678 123,456,789"
復制代碼
其中(?!\b)怎么理解呢?
要求當前是一個位置,但不是\b前面的位置,其實(?!\b)說的就是\B。
因此最終正則變成了:/\B(?=(\d{3})+\b)/g。
4.3 驗證密碼問題
密碼長度6-12位,由數字、小寫字符和大寫字母組成,但必須至少包括2種字符。
此題,如果寫成多個正則來判斷,比較容易。但要寫成一個正則就比較困難。
那么,我們就來挑戰一下。看看我們對位置的理解是否深刻。
4.3.1 簡化
不考慮“但必須至少包括2種字符”這一條件。我們可以容易寫出:
var reg=/^[0-9A-Za-z]{6,12}$/;
復制代碼
4.3.2 判斷是否包含有某一種字符
假設,要求的必須包含數字,怎么辦?此時我們可以使用(?=.*[0-9])來做。
因此正則變成:
var reg=/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;
復制代碼
4.3.3 同時包含具體兩種字符
比如同時包含數字和小寫字母,可以用(?=.*[0-9])(?=.*[a-z])來做。
因此正則變成:
var reg=/(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;
復制代碼
4.3.4 解答
我們可以把原題變成下列幾種情況之一:
以上的4種情況是或的關系(實際上,可以不用第4條)。
最終答案是:
var reg=/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是數字
console.log( reg.test("abcdef") ); // false 全是小寫字母
console.log( reg.test("ABCDEFGH") ); // false 全是大寫字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大寫字母和數字
console.log( reg.test("abcdEF234") ); // true 三者都有
復制代碼
4.3.5 解惑
上面的正則看起來比較復雜,只要理解了第二步,其余就全部理解了。
/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/
對于這個正則,我們只需要弄明白(?=.*[0-9])^即可。
分開來看就是(?=.*[0-9])和^。
表示開頭前面還有個位置(當然也是開頭,即同一個位置,想想之前的空字符類比)。
(?=.*[0-9])表示該位置后面的字符匹配.*[0-9],即,有任何多個任意字符,后面再跟個數字。
翻譯成大白話,就是接下來的字符,必須包含個數字。
4.3.6 另外一種解法
“至少包含兩種字符”的意思就是說,不能全部都是數字,也不能全部都是小寫字母,也不能全部都是大寫字母。
那么要求“不能全部都是數字”,怎么做呢?(?!p)出馬!
對應的正則是:
var reg=/(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;
復制代碼
三種“都不能”呢?
最終答案是:
var reg=/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是數字
console.log( reg.test("abcdef") ); // false 全是小寫字母
console.log( reg.test("ABCDEFGH") ); // false 全是大寫字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大寫字母和數字
console.log( reg.test("abcdEF234") ); // true 三者都有
復制代碼
位置匹配相關的案例,挺多的,不一而足。
掌握匹配位置的這6個錨字符,給我們解決正則問題一個新工具。
不管哪門語言中都有括號。正則表達式也是一門語言,而括號的存在使這門語言更為強大。
對括號的使用是否得心應手,是衡量對正則的掌握水平的一個側面標準。
括號的作用,其實三言兩語就能說明白,括號提供了分組,便于我們引用它。
引用某個分組,會有兩種情形:在JavaScript里引用它,在正則表達式里引用它。
本章內容雖相對簡單,但我也要寫長點。
內容包括:
這二者是括號最直覺的作用,也是最原始的功能。
1.1 分組
我們知道/a+/匹配連續出現的“a”,而要匹配連續出現的“ab”時,需要使用/(ab)+/。
其中括號是提供分組功能,使量詞+作用于“ab”這個整體,測試如下:
var regex=/(ab)+/g;
var string="ababa abbb ababab";
console.log( string.match(regex) );
//=> ["abab", "ab", "ababab"]
復制代碼
1.2 分支結構
而在多選分支結構(p1|p2)中,此處括號的作用也是不言而喻的,提供了子表達式的所有可能。
比如,要匹配如下的字符串:
I love JavaScript
I love Regular Expression
可以使用正則:
var regex=/^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") );
console.log( regex.test("I love Regular Expression") );
//=> true
//=> true復制代碼
如果去掉正則中的括號,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",當然這不是我們想要的。
這是括號一個重要的作用,有了它,我們就可以進行數據提取,以及更強大的替換操作。
而要使用它帶來的好處,必須配合使用實現環境的API。
以日期為例。假設格式是yyyy-mm-dd的,我們可以先寫一個簡單的正則:
var regex=/\d{4}-\d{2}-\d{2}/;
復制代碼
然后再修改成括號版的:
var regex=/(\d{4})-(\d{2})-(\d{2})/;
復制代碼
為什么要使用這個正則呢?
2.1 提取數據
比如提取出年、月、日,可以這么做:
var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
console.log( string.match(regex) );
//=> ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
復制代碼
match返回的一個數組,第一個元素是整體匹配結果,然后是各個分組(括號里)匹配的內容,然后是匹配下標,最后是輸入的文本。(注意:如果正則是否有修飾符g,match返回的數組格式是不一樣的)。
另外也可以使用正則對象的exec方法:
var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
console.log( regex.exec(string) );
//=> ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
復制代碼
同時,也可以使用構造函數的全局屬性至來獲取:
var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
regex.test(string); // 正則操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
復制代碼
2.2 替換
比如,想把yyyy-mm-dd格式,替換成mm/dd/yyyy怎么做?
var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
var result=string.replace(regex, "$2/$3/$1");
console.log(result);
//=> "06/12/2017"
復制代碼
其中replace中的,第二個參數里用、、指代相應的分組。等價于如下的形式:
var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
var result=string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result);
//=> "06/12/2017"
復制代碼
也等價于:
var regex=/(\d{4})-(\d{2})-(\d{2})/;
var string="2017-06-12";
var result=string.replace(regex, function(match, year, month, day) {
return month + "/" + day + "/" + year;
});
console.log(result);
//=> "06/12/2017"
復制代碼
除了使用相應API來引用分組,也可以在正則本身里引用分組。但只能引用之前出現的分組,即反向引用。
還是以日期為例。
比如要寫一個正則支持匹配如下三種格式:
2016-06-12
2016/06/12
2016.06.12
最先可能想到的正則是:
var regex=/\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1="2017-06-12";
var string2="2017/06/12";
var string3="2017.06.12";
var string4="2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true
復制代碼
其中/和.需要轉義。雖然匹配了要求的情況,但也匹配"2016-06/12"這樣的數據。
假設我們想要求分割符前后一致怎么辦?此時需要使用反向引用:
var regex=/\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1="2017-06-12";
var string2="2017/06/12";
var string3="2017.06.12";
var string4="2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
復制代碼
注意里面的,表示的引用之前的那個分組(-|\/|\.)。不管它匹配到什么(比如-),都匹配那個同樣的具體某個字符。
我們知道了的含義后,那么和的概念也就理解了,即分別指代第二個和第三個分組。
看到這里,此時,恐怕你會有三個問題。
3.1 括號嵌套怎么辦?
以左括號(開括號)為準。比如:
var regex=/^((\d)(\d(\d)))\1\2\3\4$/;
var string="1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
復制代碼
我們可以看看這個正則匹配模式:
這個問題,估計仔細看一下,就該明白了。
3.2 表示什么呢?
另外一個疑問可能是,即是表示第10個分組,還是和0呢?
答案是前者,雖然一個正則里出現比較罕見。測試如下:
var regex=/(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
var string="123456789# ######"
console.log( regex.test(string) );
//=> true復制代碼
3.3 引用不存在的分組會怎樣?
因為反向引用,是引用前面的分組,但我們在正則里引用了不存在的分組時,此時正則不會報錯,只是匹配反向引用的字符本身。例如,就匹配""。注意""表示對"2"進行了轉意。
var regex=/\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") );
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
復制代碼
chrome瀏覽器打印的結果:
之前文中出現的分組,都會捕獲它們匹配到的數據,以便后續引用,因此也稱他們是捕獲型分組。
如果只想要括號最原始的功能,但不會引用它,即,既不在API里引用,也不在正則里反向引用。此時可以使用非捕獲分組(?:p),例如本文第一個例子可以修改為:
var regex=/(?:ab)+/g;
var string="ababa abbb ababab";
console.log( string.match(regex) );
//=> ["abab", "ab", "ababab"]
復制代碼
至此括號的作用已經講完了,總結一句話,就是提供了可供我們使用的分組,如何用就看我們的了。
5.1 字符串trim方法模擬
trim方法是去掉字符串的開頭和結尾的空白符。有兩種思路去做。
第一種,匹配到開頭和結尾的空白符,然后替換成空字符。如:
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim(" foobar ") );
//=> "foobar"
復制代碼
第二種,匹配整個字符串,然后用引用來提取出相應的數據:
function trim(str) {
return str.replace(/^\s*(.*?)\s*$/g, "$1");
}
console.log( trim(" foobar ") );
//=> "foobar"
復制代碼
這里使用了惰性匹配*?,不然也會匹配最后一個空格之前的所有空格的。
當然,前者效率高。
5.2 將每個單詞的首字母轉換為大寫
function titleize(str) {
return str.toLowerCase().replace(/(?:^|\s)\w/g, function(c) {
return c.toUpperCase();
});
}
console.log( titleize('my name is epeli') );
//=> "My Name Is Epeli"
復制代碼
思路是找到每個單詞的首字母,當然這里不使用非捕獲匹配也是可以的。
5.3 駝峰化
function camelize(str) {
return str.replace(/[-_\s]+(.)?/g, function(match, c) {
return c ? c.toUpperCase() : '';
});
}
console.log( camelize('-moz-transform') );
//=> "MozTransform"
復制代碼
其中分組(.)表示首字母。單詞的界定是,前面的字符可以是多個連字符、下劃線以及空白符。正則后面的?的目的,是為了應對str尾部的字符可能不是單詞字符,比如str是'-moz-transform '。
5.4 中劃線化
function dasherize(str) {
return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') );
//=> "-moz-transform"
復制代碼
駝峰化的逆過程。
5.5 html轉義和反轉義
// 將HTML特殊字符轉換成等值的實體
function escapeHTML(str) {
var escapeChars={
'¢' : 'cent',
'£' : 'pound',
'¥' : 'yen',
'': 'euro',
'?' :'copy',
'?' : 'reg',
'<' : 'lt',
'>' : 'gt',
'"' : 'quot',
'&' : 'amp',
'\'' : '#39'
};
return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), function(match) {
return '&' + escapeChars[match] + ';';
});
}
console.log( escapeHTML('<div>Blah blah blah</div>') );
//=> "<div>Blah blah blah</div>";
復制代碼
其中使用了用構造函數生成的正則,然后替換相應的格式就行了,這個跟本章沒多大關系。
倒是它的逆過程,使用了括號,以便提供引用,也很簡單,如下:
// 實體字符轉換為等值的HTML。
function unescapeHTML(str) {
var htmlEntities={
nbsp: ' ',
cent: '¢',
pound: '£',
yen: '¥',
euro: '',
copy: '?',
reg: '?',
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: '\''
};
return str.replace(/\&([^;]+);/g, function(match, key) {
if (key in htmlEntities) {
return htmlEntities[key];
}
return match;
});
}
console.log( unescapeHTML('<div>Blah blah blah</div>') );
//=> "<div>Blah blah blah</div>"
復制代碼
通過key獲取相應的分組引用,然后作為對象的鍵。
5.6 匹配成對標簽
要求匹配:
<title>regular expression</title>
<p>laoyao bye bye</p>
不匹配:
<title>wrong!</p>
匹配一個開標簽,可以使用正則<[^>]+>,
匹配一個閉標簽,可以使用<\/[^>]+>,
但是要求匹配成對標簽,那就需要使用反向引用,如:
var regex=/<([^>]+)>[\d\D]*<\/\1>/;
var string1="<title>regular expression</title>";
var string2="<p>laoyao bye bye</p>";
var string3="<title>wrong!</p>";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false
復制代碼
其中開標簽<[^>]+>改成<([^>]+)>,使用括號的目的是為了后面使用反向引用,而提供分組。閉標簽使用了反向引用,<\/>。
另外[\d\D]的意思是,這個字符是數字或者不是數字,因此,也就是匹配任意字符的意思。
正則中使用括號的例子那可是太多了,不一而足。
重點理解括號可以提供分組,我們可以提取數據,應該就可以了。
例子中的代碼,基本沒做多少分析,相信你都能看懂的。
本篇未完結,請見下一篇
義、編碼和加密是開發中很常見也很基礎的概念。對于初學開發的開發者,可能有時會無法準確的區分著幾個詞。我們將通過這篇文章來了解一下“轉義、編碼和加密”這幾個詞的關聯和區別。
絕大多數的開發者都曾經在自己學習第一個編程語言時,就遇到了這個概念。以經典的C語言中字符串中的字符轉義為例。
如果在一個字符串中存在一個",那么就需要在"前添加\才能夠正常的表示,比如下面這樣。
char* universal_law="月老板說:\"世界上本也不存在'銀彈'。一套框架解決不了所有問題。\""
之所以需要這樣,是因為對于字符串來說,"本身就是表示一個字符串的起止符號。如果不進行轉義,那么編譯器將無法正確的識別其中的"哪些是分隔符,哪些是字符串內部的"。
所以,第一種需要轉義的場景就是:如果不進行轉義就可能與語法規定的某些內容產生混淆,所以這些內容都被設計為需要轉義。
基于這種場景,可以在很多的編程語言和概念中找到這種場景的體現:
可發帖可群聊的技術交流方式已經上線,歡迎通過鏈接,加入我們一起討論。 https://www.newbe.pro/links/
當然,另外還有一種場景,同樣還是以C語言為例,看一下下面這個例子:
char* hammurabi_no1="月落大佬:\"業務復雜度不會因為系統設計變化而減少,\r\n它只是從一個地方轉移到了另外的地方。\""
其中的\r和\n也是一種轉義場景的使用。他們分別表示一個回車符和換行符。之所以要轉義,是因為正常情況下,這樣的字符是不可見的,對于這種字符,不過不采用轉義的形式進行表達,那么會比較困難,因為語言設計者設計了這種轉義的方式來表達不容易表達的字符。
因此,可以總結出第二種需要轉義的場景:轉義可以使得表達內容的方式更加容易,更加容易理解,所以設計了這類轉義規則。
基于這種場景,也可以在很多編程語言和概念中找到對應的體現:
除了在IT領域,在其他領域其實也存在類似第二場景的應用。例如在中國的航空領域,對于數字的念法有特殊的處理:7讀作拐,0讀作洞,1讀作幺,2讀作兩。經過這樣的“轉義”處理,可以避免誤聽而造成的困擾。
總結來說,轉義規則的設計,主要解決了兩種場景下對代碼的表達問題:
值得一提的是,很多名稱中包含有escape或者unescape的函數或者方法都表明了它們與轉義有關。
編碼也是一個非常常見的概念。比如經常會聽到UTF8編碼、GBK編碼、Base64編碼、URL編碼、HTML編碼、摩斯電碼等等一些和編碼有關的概念。
在了解編碼之前,首先通過一個生活化的例子來了解一下“什么是信息,什么是信息的載體”。
全世界,對于“我愛你”這樣一句話的表達方式千差萬別。口頭表達,書面表達,肢體表達,普通話表達,英語表達,音樂表達,繪畫表達。甚至有生之年我們可以腦電波表達。但不論表達方式是如何的,其中包含的信息可以是一致的。都是為了傳達“我愛你”這樣的一個核心價值。
在以上這段表述中,可以將“我愛你”這樣的概念理解為“信息”。而各種表達方式理解為這個信息的各種載體。
那么,回到編程的世界中來。計算機中的信息主要的載體是以電磁信號的物理載體存在于計算機世界中。那么如果要將現實世界復雜的內容都依靠這種載體來表達,就需要進行轉化,我們可以將這種轉化理解為編碼。結合前文生活化的例子,使用普通話來表達“我愛你”這個信息,就可以理解為使用普通話來編碼這個信息。
因此,編碼,其可以理解為,采用一種新的載體來表示前一個載體所表達的信息。
可以套用類似這樣一個公式來理解:XX編碼,將A編碼為B,以實現通過B進行存儲或傳輸傳輸的目的。
那么,采用這樣的概念,我們來理解一下以往見到的各種技術概念:
總的來說,通過編碼,可以轉化信息表達的載體。這樣就可以利用新載體帶來的好處。這里也有一些生活化的例子:
值得一提的是,很多名稱中包含有encode或者decode的函數或者方法都表明了它們與編碼有關。
根據上文提到的公式,編碼是完成A->B的載體轉化過程。那么同樣可以定義A->B的逆過程B->A為“解碼”。
一般,如果解碼之后無法正確還原原來A所表達的信息,我們會說出現了亂碼。例如,使用GB2312的方式去解碼一個UTF8編碼的文件,那么就會出現亂碼。
當然,更加常見的情況是,當開發者,特別是初入的新晉工程師,看到自己無法理解的文本,就說:“這是亂碼。”
總的來說,亂碼通常來說只是因為選用的解碼方式和編碼方式不同,而導致信息失真的情況。選用正確的編碼就能夠解讀出正確的信息。
加密很好理解,在日常生活中也不乏加密的使用場景。特別是在以前的戰爭中的無線電技術應用歷史中,確保己方軍事信息不被敵方破解,采用優秀的加密算法是極為重要的軍事內容。
加密,可以這樣概括:按照一定的算法,將需要表達的信息進行處理,以達到除了信息的發送者和接收者之外,其他人無法識別信息真實內容的目的。
技術上,有需要使用加密的場景:
這里需要特別說的是編碼和加密的區別和聯系:
所以要簡單區分是編碼還是加密,可以簡單套用這個理解:在算法完全公開的情況下,如果還需要密鑰,那么是加密。如果不需要密鑰,只能算是編碼。
結合生活例子理解一下加密和編碼的區別:存在這樣一段字符串Мистер Мун, Навсегда Бог.這并不是加密,因為這是一段正常的俄語。不能因為看不懂就說他是加密,因為如果懂俄語,會用俄語解碼這段信息,就能知道他表達的意思是:“月先生,永遠的神”。
值得一提的是,很多名稱中包含有encrypt或者decrypt的函數或者方法都表明了它們與加密有關。
可發帖可群聊的技術交流方式已經上線,歡迎通過鏈接,加入我們一起討論。 https://www.newbe.pro/links/
以下是關于本文章的一些概念的測試題,以便讀者更好的理解。
不必擔心這些語言你沒有學過,因為概念其實和語言關系不大。
所有的問題都只有三個選項:
在很多編程語言中都存在“字符串內插”的語法,例如:C#、ES6、Powershell。
以C#為例,以下就是一個示例:
var dalao="月落大佬";
var hammurabi_no1=$@"{dalao}:
""業務復雜度不會因為系統設計變化而減少,
它只是從一個地方轉移到了另外的地方。""
";
Console.WriteLine(hammurabi_no1);
那么,以上代碼中""輸出時只表示一個",這是(A)處理。
如果需要在$@開頭的“多行字符串內插”字符串中,輸出一個},那么需要使用}}來進行(B)處理。
A:轉義
B:轉義
在Powershell中如果要定義一個多行字符串變量,那么需要采用下面這樣的寫法:
$template=@"
## [version]
[content]
"@
那么,如果需要在這個字符串中插入一個@或者",可以直接寫進去,因為powershell是使用@"和"@,作為多行字符串的起止符,而且要求起止符需要單行。因此中間出現的@和#都不需要進行(A)處理。
A:轉義
在javascript中有一個函數名稱為escape。按照MDN的解釋,該函數已經被標記為棄用了。建議使用encodeURI和encodeURIComponent代替。從相應的函數解釋上也可以看出,原來的escape是進行表達的意思是進行(A)處理。或許就是大佬們意識到這個名字其實不對,所以換了函數?新函數看名字直接理解應該是對URI進行(B)處理,似乎更加準確喲。
A:轉義
B:編碼
曾經有的網站使用 base64 的方式,處理登錄票據,并且保存在 Cookie 中。盡管這似乎比明文保存要高明一點,但這是不安全的,因為 base64 只是一種(A)算法,不能夠安全的防止信息被篡改。可以選用例如 DES 這樣的(B)算法,來確保信息不被篡改。
A:編碼
B:加密
轉義、編碼和加密都是在開發過程中常常遇到的概念。注意區分學習,進行正確的表達能夠更好溝通。
感謝您的閱讀,如果您覺得本文有用,請點贊、關注和轉發。
可發帖可群聊的技術交流方式已經上線,歡迎通過鏈接,加入我們一起討論。 https://www.newbe.pro/links/
義符:一般都是在字符串中的字符才需要轉義
1)JS中需要轉義符的情況
1.1路徑中的反斜杠 比如 c:\b\a.txt;在JS中不能使用@符號進行轉義
1.2常見轉義符比如 \t,\n,\’,\”,\
1.3 在正則表達式中
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JavaScript</title>
<script type="text/javascript">
//轉義字符串中文件路徑中的\
var a1='c:\\b\\a.txt';
alert(a1);
//轉義字符串中的單引號
var a2="c'b'a"; //第一種方式
var a3='c\'b\'a'; //第二種方式
alert(a2);
alert(a3);
//轉義字符串中的雙引號
var a4='a"b';//第一種方式
var a5="a\"b";//第二種方式
alert(a4);
alert(a5);
//其他的不再舉例說明
</script>
</head>
<body>
</body>
</html>
2)JS中的等于(==)與全等于(===)
JS中的等于只要變量值相同即可;全等于需要值與類型全部相同
使用等于判斷兩個變量是否相同,忽略了數據類型(不嚴謹),推薦使用全等于
3)JS中的選擇循環語句
if-else,switch; for,while,do-while,continue,break的用法與C#中幾乎一樣
for循環與C#中不同的點是:js中聲明變量使用的是var(let等暫時忽略);C#中一般使用int
switch中的判斷條件使用的也是全等于
switch語句
*請認真填寫需求信息,我們會在24小時內與您取得聯系。