正則表達式學習筆記

寫在前面:(一點題外話,點我跳過>>正則表達式

正如摘要裏面所說的,正則表達式是一個龐大的知識體系,不是簡單的一張元字符表,也不是幾句話能說清楚的算法

有人這麼評論,「...若是說在計算機發展至今的歷史上,出現過一些偉大的東西的話,正則表達式(Regular Expression)算一個,而Web,Lisp,哈希算法,UNIX,關係模型,面向對象這些東西也在此列,但這樣的東西絕對不超過20項...」編程

這麼說或許仍然不足以引發你的重視,由於雖然你也據說過正則,對着元字符表也能看懂現成的表達式,但在具體開發中卻不多用到正則...編程語言

的確是這樣的,那麼,正則還活着嗎?它去哪裏了?ide

答案是正則已經滲入了咱們的編程語言,操做系統,及相關應用中,舉個例子,不少高級語言都會提供相似於String.find()這樣的方法,不少操做系統也會提供文件內容檢索命令(如Linux的grep命令),這些都與正則表達式有關。工具

那麼,既然正則已經「消失」(滲入)了,咱們還有必要學習它嗎?固然有,正則表達式是一種技術,理解一種技術的意義要遠大於掌握一種工具。學習

-------flex

目錄結構優化

1.正則表達式工做原理this

2.正則引擎

3.正則環視

4.回溯

5.正則表達式的優化

6.如何寫出高效的真正個表達式?

7.幾個易錯點

8.總結

9.附表【元字符表】【模式控制符表】【特殊元字符表】

-------

一.正則表達式工做原理

一個正則表達式應用於目標字符串的具體過程以下:

1.正則表達式編譯

檢查正則表達式的語法正確性,若是正確,就將其編譯爲內部形式

2.傳動開始

傳動裝置將正則引擎「定位」到目標字符串的起始位置

P.S.簡單解釋一下「傳動」,就是正則引擎內部的一種機制,例如,將[abc]應用到串family上,首先嚐試首位的f,失敗,接着到第二位的a,成功,匹配結束。注意,這個過程當中是誰在控制這種「按位」處理(先第一位,失敗後嘗試第二位...)?沒錯,正是所謂的傳動裝置

3.元素檢測

正則引擎開始嘗試匹配正則表達式和文本,不只有按位向前進行,還有回溯過程(回溯是一個重點,會在後面詳細解釋)

4.得出匹配結果

肯定匹配結果,成功或者失敗,其具體過程與正則引擎的類型有關,例如找到第一個徹底匹配的串就返回成功結果,或者找到第一個合格的串後繼續尋找,返回最長的合格串

5.驅動過程

若是在當前位置沒有找到合適的匹配,那麼傳動裝置會驅動引擎,從當前位置的下一個字符處開始新的一輪嘗試

6.匹配完全失敗

若是傳動裝置驅動引擎到指定串尾,仍然沒有找到合適的匹配,那麼匹配宣告失敗(簡單點說就是,從頭至尾都沒匹配上的話就算失敗,這裏之因此描述的那麼艱澀,是爲了更貼近其內部原理)

二.正則引擎

所謂的正則引擎類型實際上是一種分類,前面說過了,正則是一種技術,全部人均可以運用它來解決問題,而你們解決問題的思路都不一樣,換言之就是正則表達式的具體實現都不一樣,規則各不相同。因而通過長期的發展,最終造成了一些流派,各個流派推行的規則不一樣。

常見的流派(正則引擎類型)有如下幾種:

1.NFA(中文是「非肯定型有窮自動機」,不用理會這奇怪的名字...)

2.DFA

3.POSIX NFA

4.DFA,NFA混合型

咱們沒必要知道各個引擎的分類標準是什麼,只須要明白相互之間的區別以及咱們經常使用的工具所屬分類就行了,很是簡單:

1.NFA

此類工具:Java,GUN Emacs,grep,dotNet,PHP,Python,Ruby等等

區別:NFA,咱們能夠稱之爲正則表達式主導型引擎,由於其匹配效率與正則表達式密切相關(例如表達式中多選分支的順序)

2.DFA

此類工具:awk,egrep,flex,lex,MySQL,Procmail等等

區別:DFA,咱們稱之爲文本主導的引擎,其匹配效率之與文本(目標串)有關(等價但不一樣形式的表達式效率相同,例如[a-d]與[abcd],注意,在NFA中這二者效率是不一樣的,通常來講前者更好一些)

3.POSIX NFA

此類工具:mawk,Mortice Kern System's utilities等等

區別:不管匹配成功與否,都要嘗試全部可能,試圖找出可以匹配的最長串

4.DFA,NFA混合型

此類工具:GUN awk,GUN grep/egrep,Tcl

區別:此類引擎應該說是最好最成熟的,引擎內部優化作的相對完善,集DFA與NFA兩者的優勢與一身,但目前應用此類引擎的工具不多

-------

說了這麼多,其實咱們要知道的是:

使用一個支持Regex的工具以前,首先要知道它的引擎所屬類型,這是極其重要的,由於不一樣的引擎具體工做機制不一樣,好比,PHP的三套正則庫都屬於NFA型,其匹配與表達式密切相關,因此我應該對錶達式進行合理優化,以提升效率。

三.正則環視(lookaround)

[其實這個東西沒有必要單獨列出來,由於它只是正則表達式很小的一部份內容,但鑑於一部分人不知道「環視」,也有一部分人聽過,但不瞭解,以爲這東西很高深...因此仍是單獨拿出來討論一下(絕對不難)]

1.什麼是「環視」?

單純理解漢字,「環視」就是向四周觀望,正則環視其實也就是這個道理——驅動到一個位置,先向左右看看這個位置是否是咱們要找的位置

舉個例子,用(this|that)來匹配there is a boy lying under that tree.

很明顯,這個表達式在NFA引擎下效率很低,它是這樣工做的:

首先,遇到第一位t,按位檢查this,發現i與e不匹配,就按位檢查that,發現a與e不匹配;

驅動前進一位,到h,按位檢查this...按位檢查that...;

驅動前進一位,到e,.......

。。。

作了不少無用功,那麼要怎麼優化?

能夠把前綴提取出來(經常使用的優化方式之一,後面有總結),變成th(is|at)

固然,咱們在這裏討論的是環視,就用環視來解決,變成(?=th)(this|that),哎呀,前面的(?=)看不懂怎麼辦?

不要緊,這個就是確定順序環視,表示的意思是:我從開頭向後走,遇到th就停下來,比對(?=th)後面的表達式部分——(this|that)【注意,反之就是說若是沒遇到th就不停,直接向後繼續走...效率是否是有點變化呢?】

優化後比較的次數明顯下降,固然這裏用環視彷佛有些小題大做了,咱們只是舉個應用環視的簡單例子而已,沒必要較真

2.正則環視的種類極其做用

類型 正則表達式 匹配成功的條件
確定順序環視 (?=...) 子表達式可以匹配右側文本
確定逆序環視 (?<=...) ......................左.........
否認順序環視 (?!...) 子表達式不能匹配右側文本
否認逆序環視 (?<!...) ......................左.........

 

 

 

 

 

P.S.上面的左右側指的是匹配進行的當前位置的左右側,這與通常的匹配不一樣,舉個例子:

用確定順序環視(?=a)abc匹配串family,初始位置是f的前面而不是f所在位置,爲何會這樣?

由於【環視結構不匹配任何字符,只匹配文本中的特定位置】,若是當前位置是f與a之間的話,確定順序環視匹配成功,開始按位檢測abc。

咱們發現:確定順序環視可以限制真正開始比較的位置,從而減小嚐試次數

3.環視的應用

環視多用於表達式的優化,與其餘一些特殊的場合(不用環視不行的場合,固然,通常來講,環視均可以用其餘複雜一些的結構來代替)

例如,要匹配the land blongs to these animals中的單詞the,如何避免匹配到these中的the?

咱們很容易想到單詞分界符(若是引擎支持的話),用\bthe\b進行全局匹配就能夠了

其實針對此例,咱們還能夠用the(?!\w)來完成目標,前面的the即使匹配了these中的the也沒關係,後面的否認順序環視(?!\w)會將these排除(這裏的否認順序環視限定了e的後面不能是單詞的字母,具體的說\w等價於[a-zA-Z0-9],在這裏或許不是很合適,但勉強能說明問題)

四.回溯(在說起優化以前,回溯是絕對是一個重點問題)

簡單的說,回溯就是倒退到何嘗試過的分支(或者說是回到備用狀態,固然,對不熟悉正則的人來講第一種說法更容易理解,而第二種說法則更確切一些)

舉個簡單的例子,用.*!來匹配串"An idel youth, a needy age!", an old saying said.

首先,*修飾.能夠匹配任意多個任意字符(點號表示任意字符,*表示任意數量),並且*是匹配優先的(就是*會盡量長的匹配串)

因此.*匹配了整個串(從A到.),這時檢測發現!沒法匹配了,怎麼辦?

.*匹配的串必須交還一部分來讓!有機會匹配,交還了句末的點號,!仍是沒法匹配

繼續交還,此次是d,沒法匹配

。。。

到age後面的!被交還,匹配成功

整個過程當中從.*佔有整個串到被迫交還!的時間裏,進行的動做就是回溯(簡單的說就是引擎的驅動在往回走)

-------

相似這樣的回溯顯然是毫無心義並且浪費時間的,咱們要作的優化很大一部分工做就是減小回溯次數。

從另外一個角度看,減小回溯的做用是提升了匹配的效率,或者說是縮短了引擎從開始工做到反饋匹配結果(成功/失敗)的時間,這不正是優化嗎?

五.正則表達式的優化

1.效率指標

考察一個正則表達式的效率,參考指標主要有兩個:嘗試(比較)次數與回溯次數

在保證表達式正確性的基礎上,嘗試次數與回溯次數越少越好,次數少意味着可以更快速的找到合適的匹配(或者更快速的反饋匹配失敗)

2.優化操做

優化操做有兩個方向:

a.加快某些操做

這須要結合具體的引擎內部實現來考慮,例如,通常來講,在NFA引擎下,[\d]要比[0-9]快,[0-9]要比[0123456789]快

b.避免冗餘操做

也就是精確限制,好比上面提到的正則環視的例子,對匹配開始位置加以限制,就能大大提升效率

固然,作此類優化時須要權衡,若是花費了很大一部分時間用來限定位置,而匹配的效率卻降低了,那麼這樣的優化是不可取的

要不要優化?優化到什麼程度?這都須要咱們結合具體應用場景來權衡

3.經常使用優化方法

優化方法很是多,這裏只列舉出最經常使用的一些優化方法(有興趣的能夠參考相關書籍)

a.消除沒必要要的括號

在不少場合,添加()只是爲了限定兩次的做用範圍,而不是爲了捕獲匹配文本,這時應該用非捕獲型括號(?:)代替捕獲型括號(),不只能減小內存開銷,還能大大提升效率

b.消除不須要的字符組

有的人習慣用[.]這樣的字符組來表示單個特殊字符,其實能夠用\.來替換,相似的有[*] -> \*等等

c.避免反覆編譯

這一點是說在其它工具中應用正則時須要注意的,好比,用Java來將一個正則表達式應用到一串文本上,首先須要對正則表達式進行編譯,不一樣的正則表達式只須要編譯一次,因此編譯的部分不該該放在循環內部,以此避免反覆編譯,節省額外的時間

d.使用起始錨點

這是應當養成的一個良好習慣,例如,大多數以.*開頭的正則表達式均可以在前面加上^或者\A來表示行或者段落的開頭,這樣作有什麼好處?

在一些落後的引擎中,這樣的優化效果很是明顯,設想一下,若是.*對目標串進行一輪嘗試後發現沒有合適的匹配,那麼若是表達式前面沒有^或者\A,那麼引擎要作的工做就是從目標串的第二個字符位置開始進行一輪新的嘗試...固然,很明顯這樣作沒有意義(咱們很清楚地一輪匹配結束後匹配結果就出來了,根本不須要第2輪甚至第n輪)

而一些發展比較成熟的引擎能夠對這樣的表達式作自動優化,若是檢測到.*開頭的表達式前面沒有^或者\A,引擎會自動爲表達式加上起始位置標誌,避免無心義的嘗試

對於咱們而言,在.*前面加上起始標誌應當成爲一個習慣

e.將文字文本獨立出來

例如[xx*]比[x+]更快,x{3, 5}沒有xxxx{0, 2}快,th(?:is|at)比(?:this|that)快

六.如何寫出高效的正則表達式?

寫正則表達式應當遵循如下步驟:

1.匹配指望文本

2.排除不指望的文本

3.易於控制和理解

4.保證效率,儘快得出結果(匹配成功/匹配失敗)

前兩點保證了表達式的正確性,後兩點須要在效率與易用性之間作出恰當的取捨,這就是寫正則表達式的原則

這裏有一句很是經典的話,基本能夠說明通常原則——不要把孩子連同洗澡水一塊兒倒掉

七.幾個易錯點

1.[-./]與[.-/]與[./-]的區別

乍看好像沒什麼區別,其實第一個和第三個是等價的,表示當前位置上的字符必須是中劃線,點號或者斜槓

第二個表達式是錯誤的,表示當前位置上的字符必須是從點號到斜槓之間全部字符中的任意一個(簡單的說就是這裏的-表示範圍,相似於[a-z]),但明顯點號到斜槓之間存在什麼字符與字符集環境有關,若是是Unicode字符集,則會出現不少奇怪的字符,與咱們的原意不符

因此在字符組中使用-時,必須仔細查看-所處的位置,避免此類錯誤

2.^在[]內外的區別

^在外面表示行的開頭,$表示行的末尾,^在裏面表示「非」([^...]即所謂的排除型字符組)或者普通字符([...^])

3.[ab]*與(a*|b*)的區別

兩者看似等價,其實存在一種特殊狀況:前者可以匹配aba然後者不能,除此以外,前者的效率要更高一些

4.使用量詞修飾符(?+*)時的易錯點

當存在嵌套使用的量詞時,應當仔細揣摩語義,避免形成循環(無限回溯),例如用"(\\.|[^\\"]+)*"來匹配文本中的連續雙引號部分,引號中的部分能夠包括用反斜槓轉義的雙引號,這個表達式就會形成循環,幾乎永遠得不到匹配結果

而存在量詞嵌套並不必定致使循環,總之,表達式中出現量詞嵌套時應當很是謹慎的對待

八.總結

我的對正則表達式的見解是:

若是對正則理解的不是很透徹,那麼儘可能不要嘗試用正則去解決複雜的問題(或者說是嘗試應用很長的正則表達式),由於其中存在的一些陷阱會讓你百思不得其解,構造一個完美的正則表達式須要至關縝密的思惟,而在通常應用中,咱們用程序進行串的匹配要更易於控制一些。

固然,也不是說盡可能不要用正則(不能因噎廢食),不得不認可在某些場合,正則有着不可替代的神奇做用(例如從文本中提取URL...)

並且,即使本身不用,也應該充分理解正則表達式,由於別人會用,因此咱們總會遇到

九.附表【元字符表】【模式控制符表】【特殊元字符表】

1.元字符表(此處提供大多數工具共同支持的元字符)

元字符 名稱 含義
^ 脫字符 表示行開始位置
$ 美圓符 表示行結束位置
. 點號 表示任意字符(通常不能表示行尾的\n)
[] 字符組 表示括號中字符的任意一個(必需要匹配一個字符)
[^] 排除型字符組 表示除括號中字符外的任意一個字符(必需要匹配一個字符)
\char 轉義字符 表示char的另外一種含義,例如\^表示普通字符^而再也不表示行開始位置
() (捕獲型)括號 表示量詞的做用範圍或者捕獲匹配的文本(能夠在反向引用中獲取捕獲到的文本)
(?:) 非捕獲型括號 與括號功能相同,但不捕獲文本
? 問號 量詞,表示左邊的部分無關緊要
* 星號 量詞,表示左邊的部分能夠有任意多個(固然,也能夠一個都沒有)
+ 加號 量詞,表示左邊的部分至少出現一次,至多不限
{min, max} 區間 量詞,表示左邊的部分至少出現min次,至多出現max次
{num} 特殊區間 量詞,表示左邊的部分必須出現num次
| 豎線 表示或者,用來實現多選結構
\< 單詞分界符 表示單詞開始位置
\> 單詞分界符 表示單詞結束位置
\num 反向引用 表示第num個捕獲型括號捕獲的文本(括號計數是按照左括號出現的順序算的,注意嵌套括號)

2.模式控制符表(此處提供一些模式控制符例子,在具體的工具中可能不一樣)

控制符 含義
i 匹配忽略大小寫
g 全局匹配,找出目標文本中全部可以匹配的部分,默認只找出第一個
x 寬鬆排列,正則表達式能夠分散到多行而且能夠包含註釋
m 加強的行錨點模式,把段落分割成邏輯行,使得^和$能夠匹配每一行的相應位置,而不是整個串的開始和結束位置
s 點號通配模式,在此模式下,點號能夠匹配任意字符(默認點號只能匹配除換行符外的任意字符)

3.特殊元字符表(此處提供某些工具支持的特殊元字符)

元字符 含義
\d 數字,等價於[0-9]
\D 非數字字符,等價與[^0-9]
\w 數字及字母,等價於[a-zA-Z0-9]
\W 非數字和字母,等價於[^a-zA-Z0-9]
\s 空白字符,例如空格符,製表符,進紙符,回車符,換行符等等
\S 非空白字符
\b 單詞分界符,表示單詞的開始或者結束位置
(?>...) 固化分組,不交還任何與之匹配的字符,例如(?>\w+!)不能匹配Hi!
??與+?與*?與{min, max}? 忽略優先量詞,儘量少的匹配內容(在可以匹配的狀況下只匹配最短的內容)
?+與++與*+與{min, max}+ 佔有優先量詞,語義同固化分組

-------

聲明,上面的全部內容來自筆者對參考書籍內容的理解

參考書籍:《精通正則表達式》(Jeffrey E.F Friedl著)

書評:這本書在章節進度安排,內容穿插強調,甚至排版方面都很不錯(特殊的排版方式:書中提出的全部思考問題,都必須翻一頁才能看到答案),對於深刻理解正則頗有幫助,有興趣的朋友能夠參閱

相關文章
相關標籤/搜索