正則表達式學習筆記


寫在前面:(一點題外話,點我跳過>>

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

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

這么說或許仍然不足以引起你的重視,因為雖然你也聽說過正則,對着元字符表也能看懂現成的表達式,但在具體開發中卻很少用到正則...

的確是這樣的,那么,正則還活着嗎?它去哪里了?

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

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

-------

目錄結構

1.正則表達式工作原理

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著)

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM