第六章 正則表達式的構(gòu)建

第六章 正則表達式的構(gòu)建

對于一門語言的掌握程度怎么樣干厚,可以有兩個角度來衡量:讀和寫。

不僅要看懂別人的解決方案螃宙,也要能獨立地解決問題蛮瞄。代碼是這樣,正則表達式也是這樣谆扎。

與“讀”相比裕坊,“寫”往往更為重要,這個道理是不言而喻的燕酷。

對正則的運用,首重就是:如何針對問題周瞎,構(gòu)建一個合適的正則表達式苗缩?

本章就解決該問題,內(nèi)容包括:

  1. 平衡法則
  2. 構(gòu)建正則前提
  3. 準確性
  4. 效率

1. 平衡法則

構(gòu)建正則有一點非常重要声诸,需要做到下面幾點的平衡:

  1. 匹配預(yù)期的字符串
  2. 不匹配非預(yù)期的字符串
  3. 可讀性和可維護性
  4. 效率

2. 構(gòu)建正則前提

2.1 是否能使用正則

正則太強大了酱讶,以至于我們隨便遇到一個操作字符串問題時,都會下意識地去想彼乌,用正則該怎么做泻肯。但我們始終要提醒自己,正則雖然強大慰照,但不是萬能的灶挟,很多看似很簡單的事情,還是做不到的毒租。

比如匹配這樣的字符串:1010010001….

雖然很有規(guī)律稚铣,但是只靠正則就是無能為力。

2.2 是否有必要使用正則

要認識到正則的局限墅垮,不要去研究根本無法完成的任務(wù)惕医。同時,也不能走入另一個極端:無所不用正則算色。能用字符串API解決的簡單問題抬伺,就不該正則出馬。

  • 比如灾梦,從日期中提取出年月日峡钓,雖然可以使用正則:
var string = "2017-07-01";
var regex = /^(\d{4})-(\d{2})-(\d{2})/;
console.log( string.match(regex) );
// => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]

其實妓笙,可以使用字符串的split方法來做,即可:

var string = "2017-07-01";
var result = string.split("-");
console.log( result );
// => ["2017", "07", "01"]
  • 比如椒楣,判斷是否有問號给郊,雖然可以使用:
var string = "?id=xx&act=search";
console.log( string.search(/\?/) );
// => 0

其實,可以使用字符串的indexOf方法:

var string = "?id=xx&act=search";
console.log( string.indexOf("?") );
// => 0
  • 比如獲取子串捧灰,雖然可以使用正則:
var string = "JavaScript";
console.log( string.match(/.{4}(.+)/)[1] );
// => Script

其實淆九,可以直接使用字符串的substringsubstr方法來做:

var string = "JavaScript";
console.log( string.substring(4) );
// => Script

2.3 是否有必要構(gòu)建一個復雜的正則

比如密碼匹配問題,要求密碼長度6-12位毛俏,由數(shù)字炭庙、小寫字符和大寫字母組成,但必須至少包括2種字符煌寇。

在第2章里焕蹄,我們寫出了正則是:

/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

其實可以使用多個小正則來做:

var regex1 = /^[0-9A-Za-z]{6,12}$/;
var regex2 = /^[0-9]{6,12}$/;
var regex3 = /^[A-Z]{6,12}$/;
var regex4 = /^[a-z]{6,12}$/;
function checkPassword(string) {
    if (!regex1.test(string)) return false;
    if (regex2.test(string)) return false;
    if (regex3.test(string)) return false;
    if (regex4.test(string)) return false;
    return true;
}

3. 準確性

所謂準確性,就是能匹配預(yù)期的目標阀溶,并且不匹配非預(yù)期的目標腻脏。

這里提到了“預(yù)期”二字,那么我們就需要知道目標的組成規(guī)則银锻。

不然沒法界定什么樣的目標字符串是符合預(yù)期的永品,什么樣的又不是符合預(yù)期的。

下面將舉例說明击纬,當目標字符串構(gòu)成比較復雜時鼎姐,該如何構(gòu)建正則,并考慮到哪些平衡更振。

3.1 匹配固定電話

比如要匹配如下格式的固定電話號碼:

055188888888

0551-88888888

(0551)88888888

第一步炕桨,了解各部分的模式規(guī)則。

上面的電話肯腕,總體上分為區(qū)號和號碼兩部分(不考慮分機號和+86的情形)献宫。

區(qū)號是0開頭的3到4位數(shù)字,對應(yīng)的正則是:0\d{2,3}

號碼是非0開頭的7到8位數(shù)字乎芳,對應(yīng)的正則是:[1-9]\d{6,7}

因此遵蚜,匹配055188888888的正則是:/^0\d{2,3}[1-9]\d{6,7}$/

匹配0551-88888888的正則是:/^0\d{2,3}-[1-9]\d{6,7}$/

匹配(0551)88888888的正則是:/^\(0\d{2,3}\)[1-9]\d{6,7}$/

第二步,明確形式關(guān)系奈惑。

這三者情形是或的關(guān)系吭净,可以構(gòu)建分支:

/^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/

提取公共部分:

/^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/

進一步簡寫:

/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

其可視化形式:

img
img

上面的正則構(gòu)建過程略顯羅嗦,但是這樣做肴甸,能保證正則是準確的导坟。

上述三種情形是或的關(guān)系崩侠,這一點很重要驻粟,不然很容易按字符是否出現(xiàn)的情形把正則寫成:

/^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$/

雖然也能匹配上述目標字符串,但也會匹配(0551-88888888這樣的字符串彤叉。當然,這不是我們想要的村怪。

其實這個正則也不是完美的秽浇,因為現(xiàn)實中,并不是每個3位數(shù)和4位數(shù)都是一個真實的區(qū)號甚负。

這就是一個平衡取舍問題柬焕,一般夠用就行。

3.2 匹配浮點數(shù)

要求匹配如下的格式:

1.23梭域、+1.23斑举、-1.23

10、+10病涨、-10

.2富玷、+.2、-.2

可以看出正則分為三部分既穆。

符號部分:[+-]

整數(shù)部分:\d+

小數(shù)部分:\.\d+

上述三個部分赎懦,并不是全部都出現(xiàn)。如果此時很容易寫出如下的正則:

/^[+-]?(\d+)?(\.\d+)?$/

此正則看似沒問題幻工,但這個正則也會匹配空字符””铲敛。

因為目標字符串的形式關(guān)系不是要求每部分都是可選的。

要匹配1.23会钝、+1.23、-1.23工三,可以用/^[+-]?\d+\.\d+$/

要匹配10迁酸、+10、-10俭正,可以用/^[+-]?\d+$/

要匹配.2奸鬓、+.2、-.2掸读,可以用/^[+-]?\.\d+$/

因此整個正則是這三者的或的關(guān)系串远,提取公眾部分后是:

/^[+-]?(\d+\.\d+|\d+|\.\d+)$/

其可視化形式是:

img
img

如果要求不匹配+.2和-.2,此時正則變成:

img
img

當然儿惫,/^[+-]?(\d+\.\d+|\d+|\.\d+)$/也不是完美的澡罚,我們也是做了些取舍,比如:

  • 它也會匹配012這樣以0開頭的整數(shù)肾请。如果要求不匹配的話留搔,需要修改整數(shù)部分的正則。
  • 一般進行驗證操作之前铛铁,都要經(jīng)過trim和判空隔显。那樣的話却妨,也許那個錯誤正則也就夠用了。
  • 也可以進一步改寫成:/^[+-]?(\d+)?(\.)?\d+$/括眠,這樣我們就需要考慮可讀性和可維護性了彪标。

4. 效率

保證了準確性后,才需要是否要考慮要優(yōu)化掷豺。大多數(shù)情形是不需要優(yōu)化的捞烟,除非運行的非常慢。什么情形正則表達式運行才慢呢萌业?我們需要考察正則表達式的運行過程(原理)坷襟。

正則表達式的運行分為如下的階段:

  1. 編譯
  2. 設(shè)定起始位置
  3. 嘗試匹配
  4. 匹配失敗的話,從下一位開始繼續(xù)第3步
  5. 最終結(jié)果:匹配成功或失敗

下面以代碼為例生年,來看看這幾個階段都做了什么:

var regex = /\d+/g;
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
// => 0 ["123", index: 0, input: "123abc34def"]
// => 3 ["34", index: 6, input: "123abc34def"]
// => 8 null
// => 0 ["123", index: 0, input: "123abc34def"]

具體分析如下:

var regex = /\d+/g;

當生成一個正則時婴程,引擎會對其進行編譯。報錯與否出現(xiàn)這這個階段抱婉。

regex.exec("123abc34def")

當嘗試匹配時档叔,需要確定從哪一位置開始匹配。一般情形都是字符串的開頭蒸绩,即第0位衙四。

但當使用testexec方法,且正則有g時患亿,起始位置是從正則對象的lastIndex屬性開始传蹈。

因此第一次exec是從第0位開始,而第二次是從3開始的步藕。

設(shè)定好起始位置后惦界,就開始嘗試匹配了。

比如第一次exec咙冗,從0開始沾歪,去嘗試匹配,并且成功地匹配到3個數(shù)字雾消。此時結(jié)束時的下標是2灾搏,因此下一次的起始位置是3。

而第二次立润,起始下標是3狂窑,但第3個字符是“a”,并不是數(shù)字桑腮。但此時并不會直接報匹配失敗蕾域,而是移動到下一位置,即從第4位開始繼續(xù)嘗試匹配,但該字符是b旨巷,也不是數(shù)字巨缘。再移動到下一位,是c仍不行采呐,再移動一位是數(shù)字3若锁,此時匹配到了兩位數(shù)字34。此時斧吐,下一次匹配的位置是d的位置又固,即第8位。

第三次煤率,是從第8位開始匹配仰冠,直到試到最后一位,也沒發(fā)現(xiàn)匹配的蝶糯,因此匹配失敗洋只,返回null。同時設(shè)置lastIndex為0昼捍,即识虚,如要再嘗試匹配的話,需從頭開始妒茬。

從上面可以看出担锤,匹配會出現(xiàn)效率問題,主要出現(xiàn)在上面的第3階段和第4階段乍钻。

因此肛循,主要優(yōu)化手法也是針對這兩階段的。

4.1 使用具體型字符組來代替通配符银择,來消除回溯

而在第三階段育拨,最大的問題就是回溯。

例如欢摄,匹配雙引用號之間的字符。如笋粟,匹配字符串123”abc”456中的”abc”怀挠。

如果正則用的是:/".*"/,害捕,會在第3階段產(chǎn)生4次回溯(粉色表示.*匹配的內(nèi)容):

img
img

如果正則用的是:/".*?"/绿淋,會產(chǎn)生2次回溯(粉色表示.*?匹配的內(nèi)容):

img
img

因為回溯的存在,需要引擎保存多種可能中未嘗試過的狀態(tài)尝盼,以便后續(xù)回溯時使用吞滞。注定要占用一定的內(nèi)存。

此時要使用具體化的字符組,來代替通配符.裁赠,以便消除不必要的字符殿漠,此時使用正則/"[^"]*"/,即可佩捞。

4.2 使用非捕獲型分組

因為括號的作用之一是绞幌,可以捕獲分組和分支里的數(shù)據(jù)。那么就需要內(nèi)存來保存它們一忱。

當我們不需要使用分組引用和反向引用時莲蜘,此時可以使用非捕獲分組。例如:

/^[+-]?(\d+\.\d+|\d+|\.\d+)$/

可以修改成:

/^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/

4.3 獨立出確定字符

例如/a+/帘营,可以修改成/aa*/票渠。

因為后者能比前者多確定了字符a。這樣會在第四步中芬迄,加快判斷是否匹配失敗问顷,進而加快移位的速度。

4.4 提取分支公共部分

比如/^abc|^def/薯鼠,修改成/^(?:abc|def)/择诈。

又比如/this|that/,修改成/th(?:is|at)/出皇。

這樣做羞芍,可以減少匹配過程中可消除的重復。

4.5 減少分支的數(shù)量郊艘,縮小它們的范圍

/red|read/荷科,可以修改成/rea?d/。此時分支和量詞產(chǎn)生的回溯的成本是不一樣的纱注。但這樣優(yōu)化后畏浆,可讀性會降低的。

小結(jié)

本章涉及的內(nèi)容并不多狞贱。

一般情況下刻获,針對某問題能寫出一個滿足需求的正則,基本上就可以了瞎嬉。

至于準確性和效率方面的追求蝎毡,純屬看個人要求了。我覺得夠用就行了氧枣。

關(guān)于準確性沐兵,本章關(guān)心的是最常用的解決思路:

針對每種情形,分別寫出正則便监,然用分支把他們合并在一起扎谎,再提取分支公共部分碳想,就能得到準確的正則。

至于優(yōu)化毁靶,本章沒有為了湊數(shù)胧奔,去寫一大堆。了解了匹配原理老充,常見的優(yōu)化手法也就這么幾種葡盗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市啡浊,隨后出現(xiàn)的幾起案子觅够,更是在濱河造成了極大的恐慌,老刑警劉巖巷嚣,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喘先,死亡現(xiàn)場離奇詭異,居然都是意外死亡廷粒,警方通過查閱死者的電腦和手機窘拯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坝茎,“玉大人涤姊,你說我怎么就攤上這事∴头牛” “怎么了思喊?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長次酌。 經(jīng)常有香客問我恨课,道長,這世上最難降的妖魔是什么岳服? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任剂公,我火速辦了婚禮,結(jié)果婚禮上吊宋,老公的妹妹穿的比我還像新娘纲辽。我一直安慰自己,他們只是感情好璃搜,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布拖吼。 她就那樣靜靜地躺著,像睡著了一般腺劣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上因块,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天橘原,我揣著相機與錄音,去河邊找鬼。 笑死趾断,一個胖子當著我的面吹牛拒名,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芋酌,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼增显,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了脐帝?” 一聲冷哼從身側(cè)響起同云,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎堵腹,沒想到半個月后炸站,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡疚顷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年旱易,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腿堤。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡阀坏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笆檀,到底是詐尸還是另有隱情忌堂,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布误债,位于F島的核電站浸船,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寝蹈。R本人自食惡果不足惜李命,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望箫老。 院中可真熱鬧封字,春花似錦、人聲如沸耍鬓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牲蜀。三九已至笆制,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涣达,已是汗流浹背在辆。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工证薇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人匆篓。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓浑度,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鸦概。 傳聞我的和親對象是個殘疾皇子箩张,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 初衷:看了很多視頻、文章窗市,最后卻通通忘記了先慷,別人的知識依舊是別人的,自己卻什么都沒獲得谨设。此系列文章旨在加深自己的印...
    DCbryant閱讀 4,014評論 0 20
  • Python中的正則表達式(re) import rere.match #從開始位置開始匹配熟掂,如果開頭沒有則無re...
    BigJeffWang閱讀 7,085評論 0 99
  • 一、正則表達式的用途(搜索和替換) 1.1.正則表達式(regular expression,簡稱regex)是一...
    IIronMan閱讀 10,118評論 0 14
  • 本文譯自 制作正則引擎的作者 Jan Goyvaerts 為工具 RegexBuddy 寫的教程版權(quán)歸原作者所有注...
    極客圈閱讀 3,290評論 0 25
  • 你不必等同伴。 你不需擔心出錢她少你多二蓝。 你不用焦躁旅行中二人的不愉快誉券。 乃至,你很自由刊愚,想去哪就去哪踊跟。 今天巴黎...
    皮皮呀閱讀 8,888評論 201 290