淺析正則表達式—(原理篇)


前言:

  其實這篇文章很久之前就應該發出來,由於種種原因沒有發出來,如果這篇文章中有錯誤,還請大家指出,小弟並改正之,沒有學不會的東西,只有不想學的東西,只要功夫深,鐵杵磨成針,我的至理名言:吾生也有涯而知也無涯,以有涯隨無涯,殆矣。我們只要堅持將其看完,相信大家的正則表達式會有一個提升空間!本文屬於.NET正則表達式里面的內容,由於不同語言正則表達式有所不同。

首先先講解下正則表達式的基礎知識:

  1.字符串的組成

  對於字符串”123“而言,包括三個字符四個位置。如下圖所示:

  2.占有字符和零寬度

  正則表達式匹配過程中,如果子表達式匹配到東西,而並非是一個位置,並最終保存到匹配的結果當中。這樣的就稱為占有字符,而只匹配一個位置,或者是匹配的內容並不保存到匹配結果中,這種就稱作零寬度,后續會講到的零寬度斷言等。占有字符是互斥的,零寬度是非互斥的。也就是一個字符,同一時間只能由一個子表達式匹配,而一個位置,卻可以同時由多個零寬度的子表達式匹配。

  3.控制權和傳動

  正則表達式由左到右依次進行匹配,通常情況下是由一個表達式取得控制權,從字符串的的某個位置進行匹配,一個子表達式開始嘗試匹配的位置,是從前一子表達匹配成功的結束位置開始的(例如:(表達式一)(表達式二)意思就是表達式一匹配完成后才能匹配表達式二,而匹配表達式二的位置是從表達式一的位置匹配結束后的位置開始)。如果表達式一是零寬度,那表達式一匹配完成后,表達式二匹配的位置還是原來表達式以匹配的位置。也就是說它匹配開始和結束的位置是同一個。

  

  舉一個簡單的例子進行說明:正則表達式:123

  源數據:123

  講解:首先正則表達式是從最左側開始進行匹配,也就是位置0處進行匹配,首先得到控制權的是正則表達式中的“1”,而不是源數據中的“1”,匹配源數據中的“1”,匹配成功,將源數據的“1”進行保存到匹配的結果當中,這就表明它占有了一個字符,接下來就將控制權傳給正則表達式中的“2”,匹配的位置變成了位置1,匹配源數據中的“2”,匹配成功,將控制權又傳動給了正則表達式的“3”,這時候匹配的位置變成了位置2,這時候就會將源數據中的“3”進行匹配。又有正則表達式“3”進行傳動控制權,發現已經到了正則表達式的末尾,正則表達式結束。

 

一、元字符

限定符 描述 模式
.

匹配出換行符以外的任意字符

\d*\.\d

\w

匹配字母數字或下划線或者漢字或者下划線

"be+"
\s

匹配任意空白符

  
"rai?n"
\d

匹配數字

",\d{3}"
\b

匹配單詞開始或結束,它只是匹配一個位置

"\d{2,}"

^

匹配字符串開始

"\d{3,5}"

$

匹配字符串結束

"\d{3,5}"

 

二、轉義字符

如果你想要得到元字符本身的話需要使用“\”來取消這些元字符的特殊意義

 

三、字符類

  首先字符類使用“[]”包起來的,例如以下這個例子:(大小寫要區分)

  ①[aeiou]則表示匹配任意一個英文元音字母(這個僅僅是匹配一個,也就是說你如果匹配了a這個整個正則表達式就已經結束了,這里面的邏輯表示的是“或”的意思),再看這個例子[.!?]表示匹配.或者?或者!

  ②[a-zA-Z0-9]這個正則表達式表示的是匹配a到z的任意一個小寫字母,或者是A到Z的任意一個字母,或者是數字0到9任意一個.

四.重復(MSDN上稱作是限定符)

代碼/語法

說明

*

重復0次或多次

+

重復一次或多次

?

重復零次或1次

{n}

重復n次

{n,}

重復至少n次

{n,m}

重復至少n次,但不多於m次

 

五.分支條件

  其實正則表達式中的分支條件,就指的是有幾種規則:用“|”把不同的規則分開

  來看下例子:

  ①0\d{2}-\d{8}|0\d{3}-\d{7}:匹配兩種以連字號分隔的電話號碼;一種是三位區號8位本地號(例如:010-12345678),另外一種規則則是4位區號7位本地號(例   如:0315-8834524)

  ②\d{5}-\d{4}|\d{5}:需要注意的是使用分支條件是一定要注意分支條件的順序,如果改成\d{5}|\d{5}-\d{4}這個樣子的話,那么只會匹配五位數字而不會匹配后面的四位數字(例如:我們利用第二個匹配12345-1234,它只會匹配12345,原因是:正則表達式是從左到右依次匹配,如果滿足了某個分支的話它就不會再管其他分支了)

 

六.分組

  你可以使用小括號()來指定字表達式

  ①(\d{1,3}){3}\d{3}:這個正則表達式的意思就是把我們分組的小括號里面的東西重復三次,也就是說我們至少匹配3個最多匹配9個數字,后面再加上三個數字

 

  我們可以看圖,最后一個是1234567891 123也就是說前面是十個數字按照我們的常理來分析的話就應該匹配應該最多的是9個所以匹配之后的數到2就匹配成功了。

  OK我們講到分組不知道你們對上面這幅圖有沒有什么想法?對,沒錯就是為啥還有0,1之分呢?想知道答案跟我繼續看下去,保證你有意外收獲哦!

  也許大家會問為什么這里的寫的1里面匹配的是這些數字,我們稍后我們會為你解析這是為什么會是這些數字!

七.反義字符

代碼/語法

說明

\W

匹配任意一個不是字母或數字下划線或漢字的字符

\S

匹配任意一個不是空白符的字符

\D

匹配不是數字的字符

\B

匹配不是單詞開頭或者結尾的位置

[^X]

匹配除了X以外的任意字符

[^aeiou]

匹配除了aeiou這幾個字母以外的任意字符

 

八.反向引用

  使用小括號指定一個子表達式后,匹配這個子表達式的文本(也就是此分組捕獲的內容)可以在表達式或其它程序中作進一步的處理。默認情況下,每個分組會自動擁有一個組號,規則是:從左向右,以分組的左括號為標志,第一個出現的分組的組號為1,第二個為2,以此類推。但是其實分組號不是這么簡單:

  •分組0對應整個正則表達式

  •實際上組號分配過程是要從左向右掃描兩遍的:第一遍只給未命名組分配,第二遍只給命名組分配--因此所有命名組的組號都大於未命名的組號

  •你可以使用(?:exp)這樣的語法來剝奪一個分組對組號分配的參與權.

  通過上面三條講述我們可以清楚地知道分組的方式是怎樣的,其實意思就是首先我們先對沒有為組進行命名的組進行分配組號(從左到右依次次分配),然后再對分配組號的組進行分配組號(使用(?<組名>)方式顯示分配組名稱),如果你想剝奪某一個組的組號可以采用(?:exp)這種方式進行剝奪,也就是不給他分配組號,可以理解為跳過此組。看一下例子:

  正則表達式:(?<work>3)(1)(2)(?<SmallDing>565)

  匹配文本:312565

  匹配結果表明首先0號組的是匹配的整個表達式,匹配1號組名的則是1,匹配2號組的是2,匹配3號組的就是命名為work組名的3,匹配4號組的則是匹配命名為SmallDing組名的565,顯然可以看到分配組號就是按照以上的規則來分配。

說到了反向引用我們來看一下反向引用是什么概念,我們前面已經詳細講解了組號的分配,那么反向引用則用於重復搜索前面某個分組匹配的文本,例如\1代表分組1匹配的文本

請看下面的例子:

  正則表達式:(1)(2)(3)\2則表示匹配123且在此匹配組號為2的內容也就是再次匹配2

  匹配文本:1232

  匹配結果如下圖所示:

  而至於想知道怎樣取消分組號那就跟着我的腳步走,來看看下面的內容吧!

  正則表達式:(?<work>333)(?<smallDing>222)(?:\d{3})該正則表達式代表的是顯示為匹配333的組分配組名為work,顯示為匹配222結果的組分配組名為222,但是如果匹配3位數字這個組已經取消了組號,所以該組號是沒有的,也就是整個正則表達式是第一個組號為0,首先將所有未命名的組進行分配組號,而只有一個(?:\d{3})這個沒有分配組名,但是它卻將組號進行取消了,所以組號不會給它分配。

  源文本為:333222123

  匹配結果為:如下圖所示:

那現在我們就來講一下零寬斷言和負零寬斷言

常見的幾種分組方法

分類

代表/語法

說明

 

 

捕獲

 

 

(exp)

匹配exp,並捕獲文本到自動命名的組里

(?<name>exp)

匹配exp,並捕獲文本到名稱為name的組里,也可以寫成(?’name’exp)

(?:exp)

匹配exp,不捕獲匹配文本,也不給分組分配組號

 

 

 斷言

 

 

 

(?=exp)

匹配exp前面位置,但是不匹配exp

(?<exp)

匹配exp后面位置,但是不匹配exp

(?!exp)

匹配后面的不是exp的位置,但是不匹配exp

(?<!exp)

匹配前面不是exp的位置,但是不匹配exp

注釋

(?#comment)

注釋

  零寬度斷言

  1.(?=exp):也叫零寬度正預測先行斷言,它匹配自身出現的位置后面能匹配表達式exp

  例如:\b\w+(?=ing\b)則這個正則表達式就是匹配一ing結尾的單詞,但是不包含ing,這個零寬度正預測先行斷言可以這樣理解,我們就以上面的正則表達式作為例來進行講解,首先我們肯定是匹配源文本為doing它會先匹配d的時候它會瞻仰一下后面跟的是不是ing,如果不是就會繼續往下走,匹配到第二個字符o它會預測(或瞻仰)下后面是不是ing如果是整個表達式就結束了,並且不匹配ing。而這個可以總結一句話就是匹配exp前面的東西

  2.(?<=exp):也叫零寬度正回顧斷言,它匹配自身出現位置的前面匹配表達式exp,這句話聽着很繞口,其實零寬度正回顧斷言中解釋說是自身出現位置這個自身出現位置是表示它匹配的文本,就比如說(?<=Ding)\d{3}這個正則表達式,這里的自身出現的位置僅僅是從開始匹配文本的時候也就是\d{3}也就是主動權在這個\d{3}的時候才是自身匹配的位置。舉例說明源文本,比如匹配Din123,按照我們的常理理解的是數字123是自身匹配的位置,但是前面不是Ding所以匹配不成功,我們可以講這個表達式理解為就是以exp為開始的正則表達式但是不包含exp,意思就是匹配exp后面的東西。

  負向零寬斷言:(可以和上面的進行對比來學哦!這個表達式的是否定的)

  1.(?!exp):也叫零寬度負預測先行斷言,斷言此位置的后面跟的不能匹配表達式exp,

  例如:\d{3}(?!123):正則表達式的含義表達了前面匹配的是三個數字,匹配的位置就是當前匹配的這三個數字后面跟的不能是123。

  2.(?<!exp):零寬度負回顧斷言,斷言此位置前面跟的不是exp的位置。

  九.平衡組

  接下來我來講一下平衡租的原理,在上面我們做下了鋪墊,也就是說我們在第六節的時候提出來了一系列問題,是不是感覺一頭霧水,沒關系的,到了這一節終於守得雲開見月明了,聽過本章節的學習我相信你們會對上面的問題進行一個詳細合理的回答!OK,Come On Baby!懂你們迫不及待心情,一定會說你咋這多廢話呢,好,閑話少說,繼續....

說到平衡組有些人就會想到分組,沒錯他們之間是有聯系的,也就是我們前面所講的分配組號的問題,那下面呢我們先引出語法,詳細見下表

語法

說明

(?’group’)

把捕獲的內容命名為group,並壓入堆棧

(?’-group’)

從堆棧上彈出最后壓入堆棧名為group的捕獲內容,如果堆棧為空則本組匹配失敗

(?(group)yes|no)

如果堆棧上存在名為group的捕獲內容的話,繼續匹配yes部分的表達式,否則匹配no的表達式

(?!)

零寬度負先行斷言,由於沒有后綴表達式,試圖匹配總是失敗

  也許大家看到這些語法都不知道是什么概念,也不知道這個平衡組到底用在什么地方合適,接下來我們我們就來說一個場景分析它用在什么位置比較合適,有時我們需要匹配像( 100 * ( 50 + 15 ) )這樣的可嵌套的層次性結構,這時簡單地使用\(.+\)則只會匹配到最左邊的左括號和最右邊的右括號之間的內容(這里我們討論的是貪婪模式,懶惰模式也有下面的問題)。假如原來的字符串里的左括號和右括號出現的次數不相等,比如( 5 / ( 3 + 2 ) ) ),那我們的匹配結果里兩者的個數也不會相等。有沒有辦法在這樣的字符串里匹配到最長的,配對的括號之間的內容呢?為了避免(和\(把你的大腦徹底搞糊塗,我們還是用尖括號代替圓括號吧。現在我們的問題變成了如何把xx <aa <bbb> <bbb> aa> yy這樣的字符串里,最長的配對的尖括號內的內容捕獲出來?

  接下來我們對這些語法進行分析,怎么樣一個平衡法,大家都見過第一個語法,語法的內容講解的就是為一個組分配組名,這里我們為什么還強調一下分配組名的問題么?前面不是提到過這些問題了么!那現在讓我們解析一下平衡法以及用這些語法去構建一個平衡。

  我們先以一個例子開始,正則表達式:(?'Group'123)(?'Group'456)看這個正則表達式,你會發現一些問題,恩?怎么給兩個組分配了一個組名,這樣返回的Group組名獲取的到底是個什么東東呢?大家來猜一下(匹配文本:123456)會是個什么結果?

先看一下測試結果:

我們可以看到0組當人不讓的是整個表達式的,而Group組里面獲取的是456,而不是123,這是為什么呢?那么我們就來分析一下他的原理,一張圖搞懂原理

  OK,我們來講一下組其實內部是一個堆棧,也就是我們分別往組名為Group的堆棧中放入了兩個內容,第一個壓入棧的是123,而第二壓入棧的是456,Group組獲取的文本是堆棧的top,也就是棧頂的數據,所以Group獲取的數據是456,而不是123,那么有些人說了我不想要456,我就想要123怎么實現?這樣也好辦啊!我們就彈出棧頂數據不就行了么!

  看下面的實例:(?'Group'123)(?'Group'456)(?’-Group’)這里的表達式(?’-Group’)就是壓出堆棧棧頂的數據也就是如下圖所示的:

 

  那么現在棧頂的數據就是123了,那么我們就來看一下匹配的結果是不是我們想的這樣:

  

  那么我們就可以想到分組名的是這樣沒有分組名的組也是這樣的匹配原理那么我們回到第六章就可以將答案找出來,為什么這個組里的數據會是這個了!剩下還有(?(group)yes|no)深入講解下這個表達式是什么意思,我們前面已經講到了分組是一個堆棧,可以壓入和彈出,但是再彈出的時候我們不知道它有沒有彈完用什么辦法來可以檢測它是不是已經到了棧底了呢?那么用這個正則表達式就可以檢測到!它說的意思就是如果我們已經將數據全部都彈出去了就會執行一個表達式在No的位置,“|”表示分割兩種不同情況,如果還存咱數據就說明還沒有到棧底,就會執行yes的表達式。那么我們就開始舉例說明:正則表達式:(?’group’123)(?’group’456)(?’-group’)(?(group)1|2):這個表達式含義就是如果堆棧中還有數據就匹配1,否則就匹配2,看下面測試結果表明堆棧中還有數據。

 

十、貪婪與非貪婪

  首先先說一下關於貪婪匹配和非貪婪匹配的一些基本概念,貪婪與非貪婪模式影響的是被量詞修飾的子表達式的匹配行為,貪婪模式在整個表達式匹配成功的前提下,盡可能多的匹配,而非貪婪模式在整個表達式匹配成功的前提下,盡可能少的匹配。

  下面是一些限定符(限定符指定在輸入字符串中必須存在上一個元素(可以是字符、組或字符類)的多少個實例才能出現匹配項)

  貪婪匹配的限定符如下表所示:

限定符 描述 模式 匹配
* 匹配上一個元素零次或多次 \d*\.\d ".0","19.9"和"219.9"
+ 匹配上一個元素一次或多次 "be+" "been"和"bee","bent"和"be"
匹配前面的元素零次或一次   "rai?n" "ran"和"rain"
{n} 匹配上一個元素恰好n次 ",\d{3}" "1.043.6"中的.043
{n,} 匹配上一個元素至少n次 "\d{2,}" "166","29"和"1930"
{n,m} 匹配上一個元素至少n次,但不多於m次 "\d{3,5}" "166","16546","132654"中的13265

  非貪婪是在貪婪限定符后面多加一個“?”,如下表所示:

  

限定符 描述 模式 匹配
*? 匹配上一個元素零次或多次,但次數盡可能少 \d*?\.\d ".0","19.9"和"219.9"
+? 匹配上一個元素一次或多次,但次數盡可能少 "be+?" "been中的"be",bent"中的"be"
?? 匹配上一個元素零次或一次,但次數盡可能少 "rai??n" "ran"和"rain"
{n}? 匹配前導元素恰好 n 次 ",\d{3}?" "1.043.6"中的.043
{n,}? 匹配上一個元素至少 n 次,但次數盡可能少 "\d{2,}?" "166","29"和"1930"
{n,m}? 匹配上一個元素的次數介於 n 和 m 之間,但次數盡可能少 "\d{3,5}?" "166","16546","132654"中的"132","654"

 

十一、貪婪匹配和非貪婪匹配原理

  這是最后一章節,也是最難理解的一章節了,希望大家跟進腳步學習下!其實這節貪婪匹配與懶惰匹配應該放在重復后面講,因為這個和重復有關系,那么下面詳細介紹什么是貪婪匹配什么是非貪婪匹配,貪婪與非貪婪模式影響的是被量詞修飾的子表達式的匹配行為,貪婪模式在整個表達式匹配成功的前提下,盡可能多的匹配,而非貪婪模式在整個表達式匹配成功的前提下,盡可能少的匹配。非貪婪模式只被部分NFA引擎所支持。

  從原理角度分析一下貪婪匹配與懶惰匹配,接下來我們將以一個例子分析

  匹配兩個正則表達式,正則表達式一為:.*

  正則表達式二是:.*?

  源文本是:“Regex”

  (1).貪婪

  

  注:為了能夠看清晰匹配過程,上面的空隙留得較大,實際源字符串為“"Regex"”,下同。

  來看一下匹配過程。首先將控制權交給“"”,由它來匹配第一個字符"匹配成功,將控制權轉交給“.*”,這時候控制權掌握在了“.*”的手上,由於“*”是優先詞量,在可匹配與不可匹配的情況下,優先嘗試匹配,他就會嘗試匹配第一字符R,匹配成功就會繼續往下匹配,匹配第二字符e,匹配成功,繼續向右匹配,直到匹配到結尾的“"”,匹配成功,再向后匹配時發現已經到結尾了,“.*”結束匹配將控制權轉交給""","""發現已經到了源字符串的結尾,看有沒有可供回溯的狀態,將控制權給了“.*”,“.*”還回一個字符“x”,然后將控制權轉交給“"”,來匹配后面的字符“"”,匹配成功正則表達式結束。這句表達式只進行了一次回溯。

  (2).懶惰

  源字符串:"Regex" 

  正則表達式:".*?" 

  看一下非貪婪模式的匹配過程。首先由第一個“"”取得控制權,匹配位置0位的“"”,匹配成功,控制權交給“.*?”。“.*?”取得控制權后,由於“*?”是忽略優先量詞,在可匹配可不匹配的情況下,優先嘗試不匹配,由於“*”等價於“{0,}”,所以在忽略優先的情況下,可以不匹配任何內容。從位置1處嘗試忽略匹配,也就是不匹配任何內容,將控制權交給正則表達式最后的“””。 

  “"”取得控制權后,從位置1處嘗試匹配,由“"”匹配位置1處的“R”,匹配失敗,向前查找可供回溯的狀態,控制權交給“.*?”,由“.*?”吃進一個字符,匹配位置1處的“R”,再把控制權交給正則表達式最后的“"”。

  “"”取得控制權后,從位置2處嘗試匹配,由“"”匹配位置1處的“e”,匹配失敗,向前查找可供回溯的狀態,重復以上過程,直到由“.*?”匹配到“x”為止,再把控制權交給正則表達式最后的“"”。 

  “"”取得控制權后,從位置6處嘗試匹配,由“"”匹配字符串最后的“"”,匹配成功。 

  此時整個正則表達式匹配成功,其中“.*?”匹配的內容為“Regex”,匹配過程中進行了五次回溯。 

 

  寫的很認真但是難免會有錯誤,希望大家多多包涵,多多指出,時刻保持學習的身段,正所謂三人行必有我師焉。人外有人天外有天,只有保持不斷學習的精神,才能達到我們的目標。我會將其更新並且改正。

  這篇文章為了方便大家傳閱將其寫成word文檔,可以進行下載,下載地址如下:

  百度網盤:http://pan.baidu.com/s/1kTKB3Zx 提取密碼:l6q4

  其中有我在公司的技術分享視頻,由於是第一次錄制可能有些細節沒有講得很清楚。也在上面百度網盤中。

  參考文章:

  圖片是從下面文章中找到的:

    http://www.jb51.net/article/31491.htm

  http://www.cnblogs.com/deerchao/archive/2006/08/24/zhengzhe30fengzhongjiaocheng.html#mission

  測試工具也在里面:

  http://deerchao.net/tutorials/regex/common.htm

  謝謝各位的提的意見,從你們的意見中將這篇文章盡自己最大努力完善,這篇文章寫得不完美,需要大家的努力。

   接下篇對令寬度進行詳細講解:

   淺析正則表達式——柳暗花明又一村篇

  如果有了基礎之后可以進行下面的文章的學習:

  淺析正則表達式-應用篇

 

  轉載請注明出處,版權歸本人所屬!

 

 


免責聲明!

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



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