正則表達式前端使用手冊

導讀

你有沒有在搜索文本的時候絞盡腦汁, 試了一個又一個表達式, 仍是不行.php

你有沒有在表單驗證的時候, 只是作作樣子(只要不爲空就好), 而後燒香拜佛, 虔誠祈禱, 千萬不要出錯.html

你有沒有在使用sed 和 grep 命令的時候, 感受莫名其妙, 明明應該支持的元字符, 卻就是匹配不到.前端

甚至, 你壓根沒遇到過上述狀況, 你只是一遍又一遍的調用 replace 而已 (把非搜索文本所有替換爲空, 而後就只剩搜索文本了), 面對別人家的簡潔高效的語句, 你只能在心中吶喊, replace 大法好.java

爲何要學正則表達式. 有位網友這麼說: 江湖傳說裏, 程序員的正則表達式和醫生的處方, 道士的鬼符齊名, 曰: 普通人看不懂的三件神器. 這個傳說至少向咱們透露了兩點信息: 一是正則表達式很牛, 能和醫生的處方, 道士的鬼符齊名, 並被你們提起, 可見其江湖地位. 二是正則表達式很難, 這也從側面說明了, 若是你能夠熟練的掌握並應用它, 在裝逼的路上, 你將如日中天 (別問我中天是誰……) !python

顯然, 有關正則表達的介紹, 無須我多言. 這裏就藉助 Jeffrey Friedl 的《精通正則表達式》一書的序言正式拋個磚.linux

​ "若是羅列計算機軟件領域的偉大發明, 我相信絕對不會超過二十項, 在這個名單當中, 固然應該包括分組交換網絡, Web, Lisp, 哈希算法, UNIX, 編譯技術, 關係模型, 面向對象, XML這些大名鼎鼎的傢伙, 而正則表達式也絕對不該該被漏掉. git

​ 對不少實際工做而言, 正則表達式簡直是靈丹妙藥, 可以成百倍的提升開發效率和程序質量, 正則表達式在生物信息學和人類基因圖譜的研究中所發揮的關鍵做用, 更是被傳爲佳話. CSDN的創始人蔣濤先生在早年開發專業軟件產品時, 就曾經體驗過這一工具的巨大威力, 而且一直印象深入."程序員

所以, 咱們沒有理由不去了解正則表達式, 甚至是熟練掌握並運用它.github

本文以正則基礎語法開篇, 結合具體實例, 逐步講解正則表達式匹配原理. 代碼實例使用語言包括 js, php, python, java(因有些匹配模式, js並未支持, 須要藉助其餘語言講解). 內容包括初階技能和高階技能, 適合新手學習和進階. 本文力求簡單通俗易懂, 同時爲求全面, 涉及知識較多, 共計12k字, 篇幅較長, 請耐心閱讀, 若有閱讀障礙請及時聯繫我.ajax

回顧歷史

要論正則表達式的淵源, 最先能夠追溯至對人類神經系統如何工做的早期研究. Warren McCulloch 和 Walter Pitts 這兩位神經大咖 (神經生理學家) 研究出一種數學方式來描述這些神經網絡.

1956 年, 一位叫 Stephen Kleene 的數學家在 McCulloch 和 Pitts 早期工做的基礎上, 發表了一篇標題爲"神經網事件的表示法"的論文, 引入了正則表達式的概念.

隨後, 發現能夠將這一工做應用於使用 Ken Thompson 的計算搜索算法的一些早期研究中. 而 Ken Thompson 又是 Unix 的主要發明人. 所以半個世紀之前的Unix 中的 qed 編輯器(1966 qed編輯器問世) 成了第一個使用正則表達式的應用程序.

至此以後, 正則表達式成爲家喻戶曉的文本處理工具, 幾乎各大編程語言都以支持正則表達式做爲賣點, 固然 JavaScript 也不例外.

正則表達式的定義

正則表達式是由普通字符和特殊字符(也叫元字符或限定符)組成的文字模板. 以下即是簡單的匹配連續數字的正則表達式:

/[0-9]+/
/\d+/

"d" 就是元字符, 而 "+" 則是限定符.

元字符

元字符 描述
. 匹配除換行符之外的任意字符
\d 匹配數字, 等價於字符組[0-9]
\w 匹配字母, 數字, 下劃線或漢字
\s 匹配任意的空白符(包括製表符,空格,換行等)
\b 匹配單詞開始或結束的位置
^ 匹配行首
$ 匹配行尾

反義元字符

元字符 描述
\D 匹配非數字的任意字符, 等價於[^0-9]
\W 匹配除字母,數字,下劃線或漢字以外的任意字符
\S 匹配非空白的任意字符
\B 匹配非單詞開始或結束的位置
[^x] 匹配除x之外的任意字符

能夠看出正則表達式嚴格區分大小寫.

重複限定符

限定符共有6個, 假設重複次數爲x次, 那麼將有以下規則:

限定符 描述
* x>=0
+ x>=1
? x=0 or x=1
{n} x=n
{n,} x>=n
{n,m} n<=x<=m

字符組

[...] 匹配中括號內字符之一. 如: [xyz] 匹配字符 x, y 或 z. 若是中括號中包含元字符, 則元字符降級爲普通字符, 再也不具備元字符的功能, 如 [+.?] 匹配 加號, 點號或問號.

排除性字符組

[^…] 匹配任何未列出的字符,. 如: [^x] 匹配除x之外的任意字符.

多選結構

| 就是或的意思, 表示二者中的一個. 如: a|b 匹配a或者b字符.

括號

括號 經常使用來界定重複限定符的範圍, 以及將字符分組. 如: (ab)+ 能夠匹配abab..等, 其中 ab 即是一個分組.

轉義字符

即轉義字符, 一般 * + ? | { [ ( ) ] }^ $ . # 和 空白 這些字符都須要轉義.

操做符的運算優先級

  1. 轉義符

  2. (), (?:), (?=), [] 圓括號或方括號

  3. *, +, ?, {n}, {n,}, {n,m} 限定符

  4. ^, $ 位置

  5. | "或" 操做

測試

咱們來測試下上面的知識點, 寫一個匹配手機號碼的正則表達式, 以下:

(\+86)?1\d{10}

① "\+86" 匹配文本 "+86", 後面接元字符問號, 表示可匹配1次或0次, 合起來表示 "(\+86)?" 匹配 "+86" 或者 "".

② 普通字符"1" 匹配文本 "1".

③ 元字符 "\d" 匹配數字0到9, 區間量詞 "{10}" 表示匹配 10 次, 合起來表示 "\d{10}" 匹配連續的10個數字.

以上, 匹配結果以下:

修飾符

javaScript中正則表達式默認有以下五種修飾符:

  • g (全文查找), 如上述截圖, 實際上就開啓了全文查找模式.

  • i (忽略大小寫查找)

  • m (多行查找)

  • y (ES6新增的粘連修飾符)

  • u (ES6新增)

經常使用的正則表達式

  1. 漢字: ^[u4e00-u9fa5]{0,}$

  2. Email: ^w+([-+.]w+)*@w+([-.]w+)*\.w+([-.]w+)*$

  3. URL: ^https?://([w-]+.)+[w-]+(/[w-./?%&=]*)?$

  4. 手機號碼: ^1d{10}$

  5. 身份證號: ^(d{15}|d{17}(d|X))$

  6. 中國郵政編碼: [1-9]d{5}(?!d) (郵政編碼爲6位數字)

密碼驗證

密碼驗證是常見的需求, 通常來講, 常規密碼大體會知足規律: 6-16位, 數字, 字母, 字符至少包含兩種, 同時不能包含中文和空格. 以下即是常規密碼驗證的正則描述:

var reg = /(?!^[0-9]+$)(?!^[A-z]+$)(?!^[^A-z0-9]+$)^[^\s\u4e00-\u9fa5]{6,16}$/;

正則的幾你們族

正則表達式分類

在 linux 和 osx 下, 常見的正則表達式, 至少有如下三種:

  • 基本的正則表達式( Basic Regular Expression 又叫 Basic RegEx 簡稱 BREs )

  • 擴展的正則表達式( Extended Regular Expression 又叫 Extended RegEx 簡稱 EREs )

  • Perl 的正則表達式( Perl Regular Expression 又叫 Perl RegEx 簡稱 PREs )

正則表達式比較

字符 說明 Basic RegEx Extended RegEx python RegEx Perl regEx
轉義
^ 匹配行首,例如'^dog'匹配以字符串dog開頭的行(注意:awk 指令中,'^'則是匹配字符串的開始) ^ ^ ^ ^
$ 匹配行尾,例如:'^、dog&dollar;' 匹配以字符串 dog 爲結尾的行(注意:awk 指令中,'$'則是匹配字符串的結尾) $ $ $ $
^$ 匹配空行 ^$ ^$ ^$ ^$
^string$ 匹配行,例如:'^dog$'匹配只含一個字符串 dog 的行 ^string$ ^string$ ^string$ ^string$
< 匹配單詞,例如:'<frog' (等價於'bfrog'),匹配以 frog 開頭的單詞 < < 不支持 不支持(但可使用b來匹配單詞,例如:'bfrog')
> 匹配單詞,例如:'frog>'(等價於'frogb '),匹配以 frog 結尾的單詞 > > 不支持 不支持(但可使用b來匹配單詞,例如:'frogb')
<x> 匹配一個單詞或者一個特定字符,例如:'<frog>'(等價於'bfrogb')、'<G>' <x> <x> 不支持 不支持(但可使用b來匹配單詞,例如:'bfrogb'
() 匹配表達式,例如:不支持'(frog)' 不支持(但可使用,如:dog () () ()
匹配表達式,例如:不支持'(frog)' 不支持(同()) 不支持(同()) 不支持(同())
匹配前面的子表達式 0 次或 1 次(等價於{0,1}),例如:where(is)?能匹配"where" 以及"whereis" 不支持(同?)
? 匹配前面的子表達式 0 次或 1 次(等價於'{0,1}'),例如:'whereis? '能匹配 "where"以及"whereis" ? 不支持(同?) 不支持(同?) 不支持(同?)
? 當該字符緊跟在任何一個其餘限制符(*, +, ?, {n},{n,}, {n,m}) 後面時,匹配模式是非貪婪的。非貪婪模式儘量少的匹配所搜索的字符串,而默認的貪婪模式則儘量多的匹配所搜索的字符串。例如,對於字符串 "oooo",'o+?' 將匹配單個"o",而 'o+' 將匹配全部 'o' 不支持 不支持 不支持 不支持
. 匹配除換行符('n')以外的任意單個字符(注意:awk 指令中的句點能匹配換行符) . .(若是要匹配包括「n」在內的任何一個字符,請使用: [sS] . .(若是要匹配包括「n」在內的任何一個字符,請使用:' [.n] '
* 匹配前面的子表達式 0 次或屢次(等價於{0, }),例如:zo* 能匹配 "z"以及 "zoo" * * * *
+ 匹配前面的子表達式 1 次或屢次(等價於'{1, }'),例如:'whereis+ '能匹配 "whereis"以及"whereisis" + 不支持(同+) 不支持(同+) 不支持(同+)
+ 匹配前面的子表達式 1 次或屢次(等價於{1, }),例如:zo+能匹配 "zo"以及 "zoo",但不能匹配 "z" 不支持(同\+) + + +
{n} n 必須是一個 0 或者正整數,匹配子表達式 n 次,例如:zo{2}能匹配 不支持(同\{n\}) {n} {n} {n}
{n,} "zooz",但不能匹配 "Bob"n 必須是一個 0 或者正整數,匹配子表達式大於等於 n次,例如:go{2,} 不支持(同\{n,\}) {n,} {n,} {n,}
{n,m} 能匹配 "good",但不能匹配 godm 和 n 均爲非負整數,其中 n <= m,最少匹配 n 次且最多匹配 m 次 ,例如:o{1,3}將配"fooooood" 中的前三個 o(請注意在逗號和兩個數之間不能有空格) 不支持(同\{n,m\}) {n,m} {n,m} {n,m}
x l y 匹配 x 或 y 不支持(同x l y x l y x l y x l y
[0-9] 匹配從 0 到 9 中的任意一個數字字符(注意:要寫成遞增) [0-9] [0-9] [0-9] [0-9]
[xyz] 字符集合,匹配所包含的任意一個字符,例如:'[abc]'能夠匹配"lay" 中的 'a'(注意:若是元字符,例如:. *等,它們被放在[ ]中,那麼它們將變成一個普通字符) [xyz] [xyz] [xyz] [xyz]
[^xyz] 負值字符集合,匹配未包含的任意一個字符(注意:不包括換行符),例如:'[^abc]' 能夠匹配 "Lay" 中的'L'(注意:[^xyz]在awk 指令中則是匹配未包含的任意一個字符+換行符) [^xyz] [^xyz] [^xyz] [^xyz]
[A-Za-z] 匹配大寫字母或者小寫字母中的任意一個字符(注意:要寫成遞增) [A-Za-z] [A-Za-z] [A-Za-z] [A-Za-z]
[^A-Za-z] 匹配除了大寫與小寫字母以外的任意一個字符(注意:寫成遞增) [^A-Za-z] [^A-Za-z] [^A-Za-z] [^A-Za-z]
\d 匹配從 0 到 9 中的任意一個數字字符(等價於 [0-9]) 不支持 不支持 \d \d
\D 匹配非數字字符(等價於 1 不支持 不支持 \D \D
\S 匹配任何非空白字符(等價於2 不支持 不支持 \S \S
\s 匹配任何空白字符,包括空格、製表符、換頁符等等(等價於[ fnrtv]) 不支持 不支持 \s \s
\W 匹配任何非單詞字符 (等價於3) \W \W \W \W
\w 匹配包括下劃線的任何單詞字符(等價於[A-Za-z0-9_]) \w \w \w \w
\B 匹配非單詞邊界,例如:'erB' 能匹配 "verb" 中的'er',但不能匹配"never" 中的'er' \B \B \B \B
\b 匹配一個單詞邊界,也就是指單詞和空格間的位置,例如: 'erb' 能夠匹配"never" 中的 'er',但不能匹配 "verb" 中的'er' \b \b \b \b
\t 匹配一個橫向製表符(等價於 x09和 cI) 不支持 不支持 \t \t
\v 匹配一個垂直製表符(等價於 x0b和 cK) 不支持 不支持 \v \v
\n 匹配一個換行符(等價於 x0a 和cJ) 不支持 不支持 \n \n
\f 匹配一個換頁符(等價於x0c 和cL) 不支持 不支持 \f \f
\r 匹配一個回車符(等價於 x0d 和cM) 不支持 不支持 \r \r
\ 匹配轉義字符自己"" \ \ \ \
cx 匹配由 x 指明的控制字符,例如:cM匹配一個Control-M 或回車符,x 的值必須爲A-Z 或 a-z 之一,不然,將 c 視爲一個原義的 'c' 字符 不支持 不支持 cx
xn 匹配 n,其中 n 爲十六進制轉義值。十六進制轉義值必須爲肯定的兩個數字長,例如:'x41' 匹配 "A"。'x041' 則等價於'x04' & "1"。正則表達式中可使用 ASCII 編碼 不支持 不支持 xn
num 匹配 num,其中 num是一個正整數。表示對所獲取的匹配的引用 不支持 num num
[:alnum:] 匹配任何一個字母或數字([A-Za-z0-9]),例如:'[[:alnum:]] ' [:alnum:] [:alnum:] [:alnum:] [:alnum:]
[:alpha:] 匹配任何一個字母([A-Za-z]), 例如:' [[:alpha:]] ' [:alpha:] [:alpha:] [:alpha:] [:alpha:]
[:digit:] 匹配任何一個數字([0-9]),例如:'[[:digit:]] ' [:digit:] [:digit:] [:digit:] [:digit:]
[:lower:] 匹配任何一個小寫字母([a-z]), 例如:' [[:lower:]] ' [:lower:] [:lower:] [:lower:] [:lower:]
[:upper:] 匹配任何一個大寫字母([A-Z]) [:upper:] [:upper:] [:upper:] [:upper:]
[:space:] 任何一個空白字符: 支持製表符、空格,例如:' [[:space:]] ' [:space:] [:space:] [:space:] [:space:]
[:blank:] 空格和製表符(橫向和縱向),例如:'[[:blank:]]'ó'[stv]' [:blank:] [:blank:] [:blank:] [:blank:]
[:graph:] 任何一個能夠看得見的且能夠打印的字符(注意:不包括空格和換行符等),例如:'[[:graph:]] ' [:graph:] [:graph:] [:graph:] [:graph:]
[:print:] 任何一個能夠打印的字符(注意:不包括:[:cntrl:]、字符串結束符'0'、EOF 文件結束符(-1), 但包括空格符號),例如:'[[:print:]] ' [:print:] [:print:] [:print:] [:print:]
[:cntrl:] 任何一個控制字符(ASCII 字符集中的前 32 個字符,即:用十進制表示爲從 0 到31,例如:換行符、製表符等等),例如:' [[:cntrl:]]' [:cntrl:] [:cntrl:] [:cntrl:] [:cntrl:]
[:punct:] 任何一個標點符號(不包括:[:alnum:]、[:cntrl:]、[:space:]這些字符集) [:punct:] [:punct:] [:punct:] [:punct:]
[:xdigit:] 任何一個十六進制數(即:0-9,a-f,A-F) [:xdigit:] [:xdigit:] [:xdigit:] [:xdigit:]

注意

  • js中支持的是EREs.

  • 當使用 BREs ( 基本正則表達式 ) 時,必須在下列這些符號(?,+,|,{,},(,))前加上轉義字符 .

  • 上述[[:xxxx:]] 形式的正則表達式, 是php中內置的通用字符簇, js中並不支持.

linux/osx下經常使用命令與正則表達式的關係

我曾經嘗試在 grep 和 sed 命令中書寫正則表達式, 常常發現不能使用元字符, 並且有時候須要轉義, 有時候不須要轉義, 始終不能摸清它的規律. 若是剛好你也有一樣的困惑, 那麼請往下看, 相信應該能有所收穫.

grep , egrep , sed , awk 正則表達式特色

grep 支持:BREs、EREs、PREs 正則表達式

  • grep 指令後不跟任何參數, 則表示要使用 "BREs"

  • grep 指令後跟 」-E" 參數, 則表示要使用 "EREs"

  • grep 指令後跟 「-P" 參數, 則表示要使用 "PREs"

egrep 支持:EREs、PREs 正則表達式

  • egrep 指令後不跟任何參數, 則表示要使用 "EREs"

  • egrep 指令後跟 「-P" 參數, 則表示要使用 "PREs"

sed 支持: BREs、EREs

  • sed 指令默認是使用 "BREs"

  • sed 指令後跟 "-r" 參數 , 則表示要使用「EREs"

awk 支持 EREs, 而且默認使用 "EREs"

正則表達式初階技能

貪婪模式與非貪婪模式

默認狀況下, 全部的限定詞都是貪婪模式, 表示儘量多的去捕獲字符; 而在限定詞後增長?, 則是非貪婪模式, 表示儘量少的去捕獲字符. 以下:

var str = "aaab",
    reg1 = /a+/, //貪婪模式
    reg2 = /a+?/;//非貪婪模式
console.log(str.match(reg1)); //["aaa"], 因爲是貪婪模式, 捕獲了全部的a
console.log(str.match(reg2)); //["a"], 因爲是非貪婪模式, 只捕獲到第一個a

實際上, 非貪婪模式很是有效, 特別是當匹配html標籤時. 好比匹配一個配對出現的div, 方案一可能會匹配到不少的div標籤對, 而方案二則只會匹配一個div標籤對.

var str = "<div class='v1'><div class='v2'>test</div><input type='text'/></div>";
var reg1 = /<div.*<\/div>/; //方案一,貪婪匹配
var reg2 = /<div.*?<\/div>/;//方案二,非貪婪匹配
console.log(str.match(reg1));//"<div class='v1'><div class='v2'>test</div><input type='text'/></div>"
console.log(str.match(reg2));//"<div class='v1'><div class='v2'>test</div>"
區間量詞的非貪婪模式

通常狀況下, 非貪婪模式, 咱們使用的是"*?", 或 "+?" 這種形式, 還有一種是 "{n,m}?".

區間量詞"{n,m}" 也是匹配優先, 雖有匹配次數上限, 可是在到達上限以前, 它依然是儘量多的匹配, 而"{n,m}?" 則表示在區間範圍內, 儘量少的匹配.

須要注意的是:

  • 能達到一樣匹配結果的貪婪與非貪婪模式, 一般是貪婪模式的匹配效率較高.

  • 全部的非貪婪模式, 均可以經過修改量詞修飾的子表達式, 轉換爲貪婪模式.

  • 貪婪模式能夠與固化分組(後面會講到)結合,提高匹配效率,而非貪婪模式卻不能夠.

分組

正則的分組主要經過小括號來實現, 括號包裹的子表達式做爲一個分組, 括號後能夠緊跟限定詞表示重複次數. 以下, 小括號內包裹的abc即是一個分組:

/(abc)+/.test("abc123") == true

那麼分組有什麼用呢? 通常來講, 分組是爲了方便的表示重複次數, 除此以外, 還有一個做用就是用於捕獲, 請往下看.

捕獲性分組

捕獲性分組, 一般由一對小括號加上子表達式組成. 捕獲性分組會建立反向引用, 每一個反向引用都由一個編號或名稱來標識, js中主要是經過 $+編號 或者 \+編號 表示法進行引用. 以下即是一個捕獲性分組的例子.

var color = "#808080";
var output = color.replace(/#(\d+)/,"$1"+"~~");//天然也能夠寫成 "$1~~"
console.log(RegExp.$1);//808080
console.log(output);//808080~~

以上, (d+) 表示一個捕獲性分組, "RegExp.&dollar;1" 指向該分組捕獲的內容. $+編號 這種引用一般在正則表達式以外使用. \+編號 這種引用卻能夠在正則表達式中使用, 可用於匹配不一樣位置相同部分的子串.

var url = "www.google.google.com";
var re = /([a-z]+)\.\1/;
console.log(url.replace(re,"$1"));//"www.google.com"

以上, 相同部分的"google"字符串只被替換一次.

非捕獲性分組

非捕獲性分組, 一般由一對括號加上"?:"加上子表達式組成, 非捕獲性分組不會建立反向引用, 就好像沒有括號同樣. 以下:

var color = "#808080";
var output = color.replace(/#(?:\d+)/,"$1"+"~~");
console.log(RegExp.$1);//""
console.log(output);//$1~~

以上, (?:d+) 表示一個非捕獲性分組, 因爲分組不捕獲任何內容, 因此, RegExp.$1 就指向了空字符串.
同時, 因爲$1 的反向引用不存在, 所以最終它被當成了普通字符串進行替換.
實際上, 捕獲性分組和無捕獲性分組在搜索效率方面也沒什麼不一樣, 沒有哪個比另外一個更快.

命名分組

語法: (?<name>...)

命名分組也是捕獲性分組, 它將匹配的字符串捕獲到一個組名稱或編號名稱中, 在得到匹配結果後, 可經過分組名進行獲取. 以下是一個python的命名分組的例子.

import re
data = "#808080"
regExp = r"#(?P<one>\d+)"
replaceString = "\g<one>" + "~~"
print re.sub(regExp,replaceString,data) # 808080~~

python的命名分組表達式與標準格式相比, 在 ? 後多了一大寫的 P 字符, 而且python經過「g<命名>"表示法進行引用. (若是是捕獲性分組, python經過"g<編號>"表示法進行引用)

與python不一樣的是, javaScript 中並不支持命名分組.

固化分組

固化分組, 又叫原子組.

語法: (?>...)

如上所述, 咱們在使用非貪婪模式時, 匹配過程當中可能會進行屢次的回溯, 回溯越多, 正則表達式的運行效率就越低. 而固化分組就是用來減小回溯次數的.

實際上, 固化分組(?>…)的匹配與正常的匹配並沒有分別, 它並不會改變匹配結果. 惟一的不一樣就是: 固化分組匹配結束時, 它匹配到的文本已經固化爲一個單元, 只能做爲總體而保留或放棄, 括號內的子表達式中何嘗試過的備用狀態都會被放棄, 因此回溯永遠也不能選擇其中的狀態(所以不能參與回溯). 下面咱們來經過一個例子更好地理解固化分組.

假如要處理一批數據, 原格式爲 123.456, 由於浮點數顯示問題, 部分數據格式會變爲123.456000000789這種, 現要求只保留小數點後2~3位, 可是最後一位不能爲0, 那麼這個正則怎麼寫呢?

var str = "123.456000000789";
str = str.replace(/(\.\d\d[1-9]?)\d*/,"$1"); //123.456

以上的正則, 對於"123.456" 這種格式的數據, 將白白處理一遍. 爲了提升效率, 咱們將正則最後的一個"*"改成"+". 以下:

var str = "123.456";
str = str.replace(/(\.\d\d[1-9]?)\d+/,"$1"); //123.45

此時, "dd[1-9]?" 子表達式, 匹配是 "45", 而不是 "456", 這是由於正則末尾使用了"+", 表示末尾至少要匹配一個數字, 所以末尾的子表達式"d+" 匹配到了 "6". 顯然 "123.45" 不是咱們指望的匹配結果, 那咱們應該怎麼作呢? 可否讓 "[1-9]?" 一旦匹配成功, 便再也不進行回溯, 這裏就要用到咱們上面說的固化分組.

"(\.dd(?>[1-9]?))d+" 即是上述正則的固化分組形式. 因爲字符串 "123.456" 不知足該固化分組的正則, 因此, 匹配會失敗, 符合咱們指望.

下面咱們來分析下固化分組的正則 (\.dd(?>[1-9]?))d+ 爲何匹配不到字符串"123.456".

很明顯, 對於上述固化分組, 只存在兩種匹配結果.

狀況①: 若 [1-9] 匹配失敗, 正則會返回 ? 留下的備用狀態. 而後匹配脫離固化分組, 繼續前進到[d+]. 當控制權離開固化分組時, 沒有備用狀態須要放棄(因固化分組中根本沒有建立任何備用狀態).

狀況②: 若 [1-9] 匹配成功, 匹配脫離固化分組以後, ? 保存的備用狀態仍然存在, 可是, 因爲它屬於已經結束的固化分組, 因此會被拋棄.

對於字符串 "123.456", 因爲 [1-9] 可以匹配成功, 因此它符合狀況②. 下面咱們來還原狀況②的執行現場.

  1. 匹配所處的狀態: 匹配已經走到了 "6" 的位置, 匹配將繼續前進;==>

  2. 子表達式 d+ 發現沒法匹配, 正則引擎便嘗試回溯;==>

  3. 查看是否存在備用狀態以供回溯?==>

  4. "?" 保存的備用狀態屬於已經結束的固化分組, 因此該備用狀態會被放棄;==>

  5. 此時固化分組匹配到的 "6", 便不能用於正則引擎的回溯;==>

  6. 嘗試回溯失敗;==>

  7. 正則匹配失敗.==>

  8. 文本 "123.456" 沒有被正則表達式匹配上, 符合預期.

相應的流程圖以下:

正則表達式流程圖

遺憾的是, javaScript, java 和 python中並不支持固化分組的語法, 不過, 它在php和.NET中表現良好. 下面提供了一個php版的固化分組形式的正則表達式, 以供嘗試.

$str = "123.456";
echo preg_replace("/(\.\d\d(?>[1-9]?))\d+/","\\1",$str); //固化分組

不只如此, php還提供了佔有量詞優先的語法. 以下:

$str = "123.456";
echo preg_replace("/(\.\d\d[1-9]?+)\d+/","\\1",$str); //佔有量詞優先

雖然java不支持固化分組的語法, 但java也提供了佔有量詞優先的語法, 一樣可以避免正則回溯. 以下:

String str = "123.456";
System.out.println(str.replaceAll("(\\.\\d\\d[1-9]?+)\\d+", "$1"));// 123.456

值得注意的是: java中 replaceAll 方法須要轉義反斜槓.

正則表達式高階技能-零寬斷言

若是說正則分組是寫輪眼, 那麼零寬斷言就是萬花筒寫輪眼終極奧義-須佐能乎(這裏借火影忍術打個比方). 合理地使用零寬斷言, 可以能分組之不能, 極大地加強正則匹配能力, 它甚至能夠幫助你在匹配條件很是模糊的狀況下快速地定位文本.

零寬斷言, 又叫環視. 環視只進行子表達式的匹配, 匹配到的內容不保存到最終的匹配結果, 因爲匹配是零寬度的, 故最終匹配到的只是一個位置.

環視按照方向劃分, 有順序和逆序兩種(也叫前瞻和後瞻), 按照是否匹配有確定和否認兩種, 組合之, 便有4種環視. 4種環視並不複雜, 以下即是它們的描述.

字符 描述 示例
(?:pattern) 非捕獲性分組, 匹配pattern的位置, 但不捕獲匹配結果.也就是說不建立反向引用, 就好像沒有括號同樣. 'abcd(?:e)匹配'abcde
(?=pattern) 順序確定環視, 匹配後面是pattern 的位置, 不捕獲匹配結果. 'Windows (?=2000)'匹配 "Windows2000" 中的 "Windows"; 不匹配 "Windows3.1" 中的 "Windows"
(?!pattern) 順序否認環視, 匹配後面不是 pattern 的位置, 不捕獲匹配結果. 'Windows (?!2000)'匹配 "Windows3.1" 中的 "Windows"; 不匹配 "Windows2000" 中的 "Windows"
(?<=pattern) 逆序確定環視, 匹配前面是 pattern 的位置, 不捕獲匹配結果. '(?<=Office)2000'匹配 " Office2000" 中的 "2000"; 不匹配 "Windows2000" 中的 "2000"
(?<!pattern) 逆序否認環視, 匹配前面不是 pattern 的位置, 不捕獲匹配結果. '(?<!Office)2000'匹配 " Windows2000" 中的 "2000"; 不匹配 " Office2000" 中的 "2000"

非捕獲性分組因爲結構與環視類似, 故列在表中, 以作對比. 以上4種環視中, 目前 javaScript 中只支持前兩種, 也就是隻支持 順序確定環視順序否認環視. 下面咱們經過實例來幫助理解下:

var str = "123abc789",s;
//沒有使用環視,abc直接被替換
s = str.replace(/abc/,456);
console.log(s); //123456789

//使用了順序確定環視,捕獲到了a前面的位置,因此abc沒有被替換,只是將3替換成了3456
s = str.replace(/3(?=abc)/,3456);
console.log(s); //123456abc789

//使用了順序否認環視,因爲3後面跟着abc,不滿意條件,故捕獲失敗,因此原字符串沒有被替換
s = str.replace(/3(?!abc)/,3456);
console.log(s); //123abc789

下面經過python來演示下 逆序確定環視逆序否認環視 的用法.

import re
data = "123abc789"
# 使用了逆序確定環視,替換左邊爲123的連續的小寫英文字母,匹配成功,故abc被替換爲456
regExp = r"(?<=123)[a-z]+"
replaceString = "456"
print re.sub(regExp,replaceString,data) # 123456789

# 使用了逆序否認環視,因爲英文字母左側不能爲123,故子表達式[a-z]+捕獲到bc,最終bc被替換爲456
regExp = r"(?<!123)[a-z]+"
replaceString = "456"
print re.sub(regExp,replaceString,data) # 123a456789

須要注意的是: python 和 perl 語言中的 逆序環視 的子表達式只能使用定長的文本. 好比將上述 "(?<=123)" (逆序確定環視)子表達式寫成 "(?<=[0-9]+)", python解釋器將會報錯: "error: look-behind requires fixed-width pattern".

場景回顧

獲取html片斷

假如如今, js 經過 ajax 獲取到一段 html 代碼以下:

var responseText = "<div data='dev.xxx.txt'></div><img src='dev.xxx.png' />";

現咱們須要替換img標籤的src 屬性中的 "dev"字符串 爲 "test" 字符串.

① 因爲上述 responseText 字符串中包含至少兩個子字符串 "dev", 顯然不能直接 replace 字符串 "dev"爲 "test".

② 同時因爲 js 中不支持逆序環視, 咱們也不能在正則中判斷前綴爲 "src='", 而後再替換"dev".

③ 咱們注意到 img 標籤的 src 屬性以 ".png" 結尾, 基於此, 就可使用順序確定環視. 以下:

var reg = /dev(?=[^']*png)/; //爲了防止匹配到第一個dev, 通配符前面須要排除單引號或者是尖括號
var str = responseText.replace(reg,"test");
console.log(str);//<div data='dev.xxx'></div><img src='test.xxx.png' />

固然, 以上不止順序確定環視一種解法, 捕獲性分組一樣能夠作到. 那麼環視高級在哪裏呢? 環視高級的地方就在於它經過一次捕獲就能夠定位到一個位置, 對於複雜的文本替換場景, 常有奇效, 而分組則須要更多的操做.

千位分割符

千位分隔符, 顧名思義, 就是數字中的逗號. 參考西方的習慣, 數字之中加入一個符號, 避免因數字太長難以直觀的看出它的值. 故而數字之中, 每隔三位添加一個逗號, 即千位分隔符.

那麼怎麼將一串數字轉化爲千位分隔符形式呢?

var str = "1234567890";
(+str).toLocaleString();//"1,234,567,890"

如上, toLocaleString() 返回當前對象的"本地化"字符串形式.

  • 若是該對象是Number類型, 那麼將返回該數值的按照特定符號分割的字符串形式.

  • 若是該對象是Array類型, 那麼先將數組中的每項轉化爲字符串, 而後將這些字符串以指定分隔符鏈接起來並返回.

toLocaleString 方法特殊, 有本地化特性, 對於天朝, 默認的分隔符是英文逗號. 所以使用它剛好能夠將數值轉化爲千位分隔符形式的字符串. 若是考慮到國際化, 以上方法就有可能會失效了.

咱們嘗試使用環視來處理下.

function thousand(str){
  return str.replace(/(?!^)(?=([0-9]{3})+$)/g,',');
}
console.log(thousand(str));//"1,234,567,890"
console.log(thousand("123456"));//"123,456"
console.log(thousand("1234567879876543210"));//"1,234,567,879,876,543,210"

上述使用到的正則分爲兩塊. (?!^)(?=([0-9]{3})+$). 咱們先來看後面的部分, 而後逐步分析之.

  1. "[0-9]{3}" 表示連續3位數字.

  2. "([0-9]{3})+" 表示連續3位數字至少出現一次或更屢次.

  3. "([0-9]{3})+$" 表示連續3的正整數倍的數字, 直到字符串末尾.

  4. 那麼 (?=([0-9]{3})+$) 就表示匹配一個零寬度的位置, 而且從這個位置到字符串末尾, 中間擁有3的正整數倍的數字.

  5. 正則表達式使用全局匹配g, 表示匹配到一個位置後, 它會繼續匹配, 直至匹配不到.

  6. 將這個位置替換爲逗號, 實際上就是每3位數字添加一個逗號.

  7. 固然對於字符串"123456"這種恰好擁有3的正整數倍的數字的, 固然不能在1前面添加逗號. 那麼使用 (?!^) 就指定了這個替換的位置不能爲起始位置.

千位分隔符實例, 展現了環視的強大, 一步到位.

正則表達式在JS中的應用

ES6對正則的擴展

ES6對正則擴展了又兩種修飾符(其餘語言可能不支持):

  • y (粘連sticky修飾符), 與g相似, 也是全局匹配, 而且下一次匹配都是從上一次匹配成功的下一個位置開始, 不一樣之處在於, g修飾符只要剩餘位置中存在匹配便可, 而y修飾符確保匹配必須從剩餘的第一個位置開始.

var s = "abc_ab_a";
var r1 = /[a-z]+/g;
var r2 = /[a-z]+/y;
console.log(r1.exec(s),r1.lastIndex); // ["abc", index: 0, input: "abc_ab_a"] 3
console.log(r2.exec(s),r2.lastIndex); // ["abc", index: 0, input: "abc_ab_a"] 3

console.log(r1.exec(s),r1.lastIndex); // ["ab", index: 4, input: "abc_ab_a"] 6
console.log(r2.exec(s),r2.lastIndex); // null 0

如上, 因爲第二次匹配的開始位置是下標3, 對應的字符串是 "_", 而使用y修飾符的正則對象r2, 須要從剩餘的第一個位置開始, 因此匹配失敗, 返回null.

正則對象的 sticky 屬性, 表示是否設置了y修飾符. 這點將會在後面講到.

  • u 修飾符, 提供了對正則表達式添加4字節碼點的支持. 好比 "?" 字符是一個4字節字符, 直接使用正則匹配將會失敗, 而使用u修飾符後, 將會等到正確的結果.

var s = "?";
console.log(/^.$/.test(s));//false
console.log(/^.$/u.test(s));//true

UCS-2字節碼

有關字節碼點, 稍微提下. javaScript 只能處理UCS-2編碼(js於1995年5月被Brendan Eich花費10天設計出來, 比1996年7月發佈的編碼規範UTF-16早了一年多, 當時只有UCS-2可選). 因爲UCS-2先天不足, 形成了全部字符在js中都是2個字節. 若是是4個字節的字符, 將會默認被看成兩個雙字節字符處理. 所以 js 的字符處理函數都會受到限制, 沒法返回正確結果. 以下:

var s = "?";
console.log(s == "\uD834\uDF06");//true ?至關於UTF-16中的0xD834DF06
console.log(s.length);//2 長度爲2, 表示這是4字節字符

幸運的是, ES6能夠自動識別4字節的字符.所以遍歷字符串能夠直接使用for of循環. 同時, js中若是直接使用碼點表示Unicode字符, 對於4字節字符, ES5裏是沒辦法識別的. 爲此ES6修復了這個問題, 只需將碼點放在大括號內便可.

console.log(s === "\u1D306");//false   ES5沒法識別?
console.log(s === "\u{1D306}");//true  ES6能夠藉助大括號識別?

附: ES6新增的處理4字節碼的函數

  • String.fromCodePoint():從Unicode碼點返回對應字符

  • String.prototype.codePointAt():從字符返回對應的碼點

  • String.prototype.at():返回字符串給定位置的字符

有關js中的unicode字符集, 請參考阮一峯老師的 Unicode與JavaScript詳解 .

以上是ES6對正則的擴展. 另外一個方面, 從方法上看, javaScript 中與正則表達式有關的方法有:

方法名 compile test exec match search replace split
所屬對象 RegExp RegExp RegExp String String String String

由上, 一共有7個與js相關的方法, 這些方法分別來自於 RegExp 與 String 對象. 首先咱們先來看看js中的正則類 RegExp.

RegExp

RegExp 對象表示正則表達式, 主要用於對字符串執行模式匹配.

語法: new RegExp(pattern[, flags])

參數 pattern 是一個字符串, 指定了正則表達式字符串或其餘的正則表達式對象.

參數 flags 是一個可選的字符串, 包含屬性 "g"、"i" 和 "m", 分別用於指定全局匹配、區分大小寫的匹配和多行匹配. 若是pattern 是正則表達式, 而不是字符串, 則必須省略該參數.

var pattern = "[0-9]";
var reg = new RegExp(pattern,"g");
// 上述建立正則表達式對象,能夠用對象字面量形式代替,也推薦下面這種
var reg = /[0-9]/g;

以上, 經過對象字面量和構造函數建立正則表達式, 有個小插曲.

"對於正則表達式的直接量, ECMAscript 3規定在每次它時都會返回同一個RegExp對象, 所以用直接量建立的正則表達式的會共享一個實例. 直到ECMAScript 5才規定每次返回不一樣的實例."

因此, 如今咱們基本不用擔憂這個問題, 只須要注意在低版本的非IE瀏覽器中儘可能使用構造函數建立正則(這點上, IE一直遵照ES5規定, 其餘瀏覽器的低級版本遵循ES3規定).

RegExp 實例對象包含以下屬性:

實例屬性 描述
global 是否包含全局標誌(true/false)
ignoreCase 是否包含區分大小寫標誌(true/false)
multiline 是否包含多行標誌(true/false)
source 返回建立RegExp對象實例時指定的表達式文本字符串形式
lastIndex 表示原字符串中匹配的字符串末尾的後一個位置, 默認爲0
flags(ES6) 返回正則表達式的修飾符
sticky(ES6) 是否設置了y(粘連)修飾符(true/false)

compile

compile 方法用於在執行過程當中改變和從新編譯正則表達式.

語法: compile(pattern[, flags])

參數介紹請參考上述 RegExp 構造器. 用法以下:

var reg = new RegExp("abc", "gi"); 
var reg2 = reg.compile("new abc", "g");
console.log(reg);// /new abc/g
console.log(reg2);// undefined

可見 compile 方法會改變原正則表達式對象, 並從新編譯, 並且它的返回值爲空.

test

test 方法用於檢測一個字符串是否匹配某個正則規則, 只要是字符串中含有與正則規則匹配的文本, 該方法就返回true, 不然返回 false.

語法: test(string), 用法以下:

console.log(/[0-9]+/.test("abc123"));//true
console.log(/[0-9]+/.test("abc"));//false

以上, 字符串"abc123" 包含數字, 故 test 方法返回 true; 而 字符串"abc" 不包含數字, 故返回 false.

若是須要使用 test 方法測試字符串是否完成匹配某個正則規則, 那麼能夠在正則表達式裏增長開始(^)和結束($)元字符. 以下:

console.log(/^[0-9]+$/.test("abc123"));//false

以上, 因爲字符串"abc123" 並不是以數字開始, 也並不是以數字結束, 故 test 方法返回false.

實際上, 若是正則表達式帶有全局標誌(帶有參數g)時, test 方法還受正則對象的lastIndex屬性影響,以下:

var reg = /[a-z]+/;//正則不帶全局標誌
console.log(reg.test("abc"));//true
console.log(reg.test("de"));//true

var reg = /[a-z]+/g;//正則帶有全局標誌g
console.log(reg.test("abc"));//true
console.log(reg.lastIndex);//3, 下次運行test時,將從索引爲3的位置開始查找
console.log(reg.test("de"));//false

該影響將在exec 方法講解中予以分析.

exec

exec 方法用於檢測字符串對正則表達式的匹配, 若是找到了匹配的文本, 則返回一個結果數組, 不然返回null.

語法: exec(string)

exec 方法返回的數組中包含兩個額外的屬性, index 和 input. 而且該數組具備以下特色:

  • 第 0 個項表示正則表達式捕獲的文本

  • 第 1~n 項表示第 1~n 個反向引用, 依次指向第 1~n 個分組捕獲的文本, 可使用RegExp.$ + "編號1~n" 依次獲取分組中的文本

  • index 表示匹配字符串的初始位置

  • input 表示正在檢索的字符串

不管正則表達式有無全局標示"g", exec 的表現都相同. 但正則表達式對象的表現卻有些不一樣. 下面咱們來詳細說明下正則表達式對象的表現都有哪些不一樣.

假設正則表達式對象爲 reg , 檢測的字符爲 string , reg.exec(string) 返回值爲 array.

若 reg 包含全局標示"g", 那麼 reg.lastIndex 屬性表示原字符串中匹配的字符串末尾的後一個位置, 即下次匹配開始的位置, 此時 reg.lastIndex == array.index(匹配開始的位置) + array[0].length(匹配字符串的長度). 以下:

var reg = /([a-z]+)/gi,
    string = "World Internet Conference";
var array = reg.exec(string);
console.log(array);//["World", "World", index: 0, input: "World Internet Conference"]
console.log(RegExp.$1);//World
console.log(reg.lastIndex);//5, 恰好等於 array.index + array[0].length

隨着檢索繼續, array.index 的值將日後遞增, 也就是說, reg.lastIndex 的值也會同步日後遞增. 所以, 咱們也能夠經過反覆調用 exec 方法來遍歷字符串中全部的匹配文本. 直到 exec 方法再也匹配不到文本時, 它將返回 null, 並把 reg.lastIndex 屬性重置爲 0.

接着上述例子, 咱們繼續執行代碼, 看看上面說的對不對, 以下所示:

array = reg.exec(string);
console.log(array);//["Internet", "Internet", index: 6, input: "World Internet Conference"]
console.log(reg.lastIndex);//14

array = reg.exec(string);
console.log(array);//["Conference", "Conference", index: 15, input: "World Internet Conference"]
console.log(reg.lastIndex);//25

array = reg.exec(string);
console.log(array);//null
console.log(reg.lastIndex);//0

以上代碼中, 隨着反覆調用 exec 方法, reg.lastIndex 屬性最終被重置爲 0.

問題回顧

在 test 方法的講解中, 咱們留下了一個問題. 若是正則表達式帶有全局標誌g, 以上 test 方法的執行結果將受 reg.lastIndex影響, 不只如此, exec 方法也同樣. 因爲 reg.lastIndex 的值並不老是爲零, 而且它決定了下次匹配開始的位置, 若是在一個字符串中完成了一次匹配以後要開始檢索新的字符串, 那就必需要手動地把 lastIndex 屬性重置爲 0. 避免出現下面這種錯誤:

var reg = /[0-9]+/g,
    str1 = "123abc",
    str2 = "123456";
reg.exec(str1);
console.log(reg.lastIndex);//3
var array = reg.exec(str2);
console.log(array);//["456", index: 3, input: "123456"]

以上代碼, 正確執行結果應該是 "123456", 所以建議在第二次執行 exec 方法前, 增長一句 "reg.lastIndex = 0;".

若 reg 不包含全局標示"g", 那麼 exec 方法的執行結果(array)將與 string.match(reg) 方法執行結果徹底相同.

String

match, search, replace, split 方法請參考 字符串經常使用方法 中的講解.

以下展現了使用捕獲性分組處理文本模板, 最終生成完整字符串的過程:

var tmp = "An ${a} a ${b} keeps the ${c} away";
var obj = {
  a:"apple",
  b:"day",
  c:"doctor"
};
function tmpl(t,o){
  return t.replace(/\${(.)}/g,function(m,p){
    console.log('m:'+m+' p:'+p);
    return o[p];
  });
}
tmpl(tmp,obj);

上述功能使用ES6可這麼實現:

var obj = {
  a:"apple",
  b:"day",
  c:"doctor"
};
with(obj){
  console.log(`An ${a} a ${b} keeps the ${c} away`);
}

正則表達式在H5中的應用

H5中新增了 pattern 屬性, 規定了用於驗證輸入字段的模式, pattern的模式匹配支持正則表達式的書寫方式. 默認 pattern 屬性是所有匹配, 即不管正則表達式中有無 "^", "$" 元字符, 它都是匹配全部文本.

注: pattern 適用於如下 input 類型:text, search, url, telephone, email 以及 password. 若是須要取消表單驗證, 在form標籤上增長 novalidate 屬性便可.

正則引擎

目前正則引擎有兩種, DFA 和 NFA, NFA又能夠分爲傳統型NFA和POSIX NFA.

  • DFA Deterministic finite automaton 肯定型有窮自動機

  • NFA Non-deterministic finite automaton 非肯定型有窮自動機

  • Traditional NFA

  • POSIX NFA

DFA引擎不支持回溯, 匹配快速, 而且不支持捕獲組, 所以也就不支持反向引用. 上述awk, egrep命令均支持 DFA引擎.

POSIX NFA主要指符合POSIX標準的NFA引擎, 像 javaScript, java, php, python, c#等語言均實現了NFA引擎.

有關正則表達式詳細的匹配原理, 暫時沒在網上看到適合的文章, 建議選讀 Jeffrey Friedl 的 <精通正則表達式>[第三版] 中第4章-表達式的匹配原理(p143-p183), Jeffrey Friedl 對正則表達式有着深入的理解, 相信他可以幫助您更好的學習正則.

有關NFA引擎的簡單實現, 能夠參考文章 基於ε-NFA的正則表達式引擎 - twoon.

總結

在學習正則的初級階段, 重在理解 ①貪婪與非貪婪模式, ②分組, ③捕獲性與非捕獲性分組, ④命名分組, ⑤固化分組, 體會設計的精妙之處. 而高級階段, 主要在於熟練運用⑥零寬斷言(或環視)解決問題, 而且熟悉正則匹配的原理.

實際上, 正則在 javaScript 中的功能不算強大, js 僅僅支持了①貪婪與非貪婪模式, ②分組, ③捕獲性與非捕獲性分組 以及 ⑥零寬斷言中的順序環視. 若是再稍微熟悉些 js 中7種與正則有關的方法(compile, test, exec, match, search, replace, split), 那麼處理文本或字符串將遊刃有餘.

正則表達式, 在文本處理方面天賦異稟, 它的功能十分強大, 不少時候甚至是惟一解決方案. 正則不侷限於js, 當下熱門的編輯器(好比Sublime, Atom) 以及 IDE(好比WebStorm, IntelliJ IDEA) 都支持它. 您甚至能夠在任什麼時候候任何語言中, 嘗試使用正則解決問題, 也許以前不能解決的問題, 如今能夠輕鬆的解決.

附其餘語言正則資料:


本文做者: louis
本文簡介: 本文斷斷續續歷時兩個月而成, 共計12k字, 爲求簡潔全面地還原前端場景中正則的使用規律, 蒐集了大量正則相關資料, 並剔除很多冗餘字句, 碼字不易, 喜歡的請點個贊?或者收藏, 我將持續保持更新.
原文地址: http://louiszhai.github.io/20...

參考文章


  1. 0-9
  2. fnrtv
  3. A-Za-z0-9_
相關文章
相關標籤/搜索