行的開始:^
^abc 以a開頭,接下來是b 再接下來是c的文本 (只有用在表達式的開頭時才表示行的開始)
行的結(jié)束:$
abc$ 以c結(jié)尾前面是b 再前面是a的文本
字符組:[]
[abc] 表示 a 或 b 或 c
連接字符: -
用于字符組內(nèi),表示一個范圍 [1-5] 等同于 [12345]
^ 用在組內(nèi)第一個字符時表示非
^[^1-6] 表示不以1-6的數(shù)字開頭的文本
任意符:.
. 匹配任意字符
小括號:()
界定范圍,其實還有個重要的作用,捕捉文本分組
多選分支:|
a(b|d)c 匹配abc或adc
多選分支和字符組都有或的意思,
但是字符組只是匹配字符組中包含字符的其中之一,
但是多選分支可能本身就是個正則表達式 比如a(a[bd]c|1[0-5]3)b
注意事項 ^abc|123: 和 ^(abc|123): 是不同的,
前面表示匹配 abc或123:開始的文本
后面表示匹配 abc:或123:開始的文本 因此為了明確語境需要配合()來完成
單詞分界符:\< 和 \> (只有在少部分支持的系統(tǒng)中才能使用)
\<ab 以ab開頭的單詞 比如able
和行開始符表示的意思不同 ^ob.*表示的是以ob開始的文本,
比如'obc my good'
\<ob.* 匹配的是以ob開頭的單詞開頭的文本,
因為obc不是個單詞,所以不能匹配
如果換成 'object my good' 就可以匹配,因為object是英文單詞
問號:?
goo?d ?表示可選元素不作為匹配的必然條件 示例可匹配god,good 配合括號可作出更多的操作 go(od)?則可以匹配go,good
除了?外還有兩個量詞 + 和 * ,和一個組合量詞{}
?表示可選,即匹配一次或0次
+ 表示至少一次
* 表示任意次 0次或多次
{} 表示范圍
abc? 匹配ab abc
abc+ 匹配abcc abccccc...
abc* 匹配ab abccccc...
捕獲命名 (?P<name>...)
Python支持為捕獲的內(nèi)容進行重命名
(?P<myName>good) 表示捕獲 good,可以通過obj.group(1) 來獲取good 但是也可以通過自定義名稱 obj.group("myName") 來獲取good
大括號 {}
大括號代表區(qū)間個數(shù) [a-z]{8} 表示 連續(xù)的八個字母 [a-z]{3,8} 連續(xù)的 3 到 8 個字母
反向引用
()有捕捉文本的作用,利用這個特性可做一些反向引用的操作,比如查找重復(fù)單詞即連續(xù)出現(xiàn)兩次相同的單詞 my name is is xie,這里is是重復(fù)的如果用正則表達式找出所有的這種重復(fù)單詞要怎么做,首先要匹配的是單詞,所以要用到單詞分界符 \<is\s+is\> 這樣只可以匹配 is is 為了更通用 \<([a-zA-Z]+)\s+\1\> 這里有兩個關(guān)鍵點1.小括號,2.\1 因為括號內(nèi)的內(nèi)容是會被捕獲的,所以我們可以取出其捕獲的內(nèi)容 取出方法便是用\加一個數(shù)字 \1 表示取出第一個括號捕獲的內(nèi)容 因此該表達式表示 先匹配一個單詞并捕獲,在至少一個空格后再取出其捕獲的內(nèi)容進行匹配,這樣前后兩個匹配就是相同的\<\>又保障了這兩個是單詞,所以滿足需求。類似的有(0-9)(a-z)\1\2 用于匹配連續(xù)兩次相同的數(shù)字加字母2a2a \1 取第一個()捕獲的內(nèi)容 \2取第二個()捕獲的內(nèi)容
如果不是在表達式中而是在表達式匹配完成時取得這些分組中的值,
不同語言有不同的方式,
Perl中用 $1 $2 去取分組\1 和 \2 的內(nèi)容,
Python中用group(1) group(2) 比如:
Perl中:以下表達式用來匹配前兩個數(shù)字相同后面跟多個字母的字符串 類似99abc , 22kkk
if($inputStr =~ m/^([0-9])\1([a-zA-Z]*)$/) {
$value = $1; //$1 用來獲取第一個括號內(nèi)捕獲的內(nèi)容
$string = $2; //$2 用來獲取第二個括號內(nèi)捕獲的內(nèi)容
print "value = $value, string = $string";
}
當(dāng)()嵌套時其對應(yīng)的序號按照左邊半括號(的順序,
比如:(abc(123(.CF)))
abc(123(.CF)) 在分組\1內(nèi) 123(.CF)在分組\2內(nèi) .CF在分組\3內(nèi)
這里有個()的反例,如果只想界定范圍而不捕捉內(nèi)容怎么做,答案是:(?:) 這是一個組合元字符 里面的 ? 和之前的 可選? 沒有任何關(guān)系
把上面的例子改為 (abc(?:123(.CF))) 則分組順序變成了 abc(123(.CF)) 在分組\1內(nèi) 由于第二個( 放棄了捕捉內(nèi)容因此123(.CF)不會被捕捉,也不占用分組,所以.CF 變成了在分組\2內(nèi) 這個元字符在這個例子里也許并看不出有什么實際意義,但是在一些實際的開發(fā)場景里非常有用,舉個例子:
你在開發(fā)中寫了這么一個正則 ([0-9]+)\s*(K?m) 用來捕獲距離,\1 捕獲數(shù)值 \2 捕獲單位,而且此時程序已經(jīng)寫完了,這時需求擴展了,需要能匹配小數(shù),所以你把正則改為了 ([0-9]+(.[0-9])?)\s*(K?m) 這里 (.[0-9])?的括號原本只是用來給?界定邊界的 但是其依然有捕捉作用,如果你這么寫 因為插入了一個分組 原本用來捕捉單位的分組則變成了\3 因為此時程序已經(jīng)寫完了,內(nèi)部都是按照\2判斷的,所以要想程序正常就必須把之前里面對\2的判斷改為\3 這當(dāng)然是可行的,但是還有一種更優(yōu)雅的方法就是讓不必捕捉的括號放棄捕捉,正則表達式改為([0-9]+(?:.[0-9])?)\s*(K?m) 這里只是多了個?: 由于第二個(內(nèi)放棄了捕捉,不占用分組,所以原來的分組順序沒有任何改變就能完成這個功能的擴展
反斜杠 \ 轉(zhuǎn)義
前面有多次出現(xiàn)過反斜杠的地方 比如 \< \> \1 \2 這些都被成為組合元字符 他們組合在一起有特殊意義 其實反斜杠還有個非常重要的作用 轉(zhuǎn)義,比如我們想要匹配一個 \< 的字符串要怎么做,因為\<本身代表的是單詞開頭 所以要把元字符當(dāng)成一個普通字符進行匹配就需要用到轉(zhuǎn)義 在元字符前加一個\ 所以可以用\\< 匹配\<
正則表達式一些簡記字符(Python支持)
\a 警報符
\f 進止符
\b 單詞分界符(當(dāng)出現(xiàn)在字符組內(nèi)時為退格符[\b])
\v 垂直制表符
\t 水平制表符
\n 換行符
\r 回車符
\s 任何'空白'字符(空格,制表符,進止符)*很常用
\S 除了 \s 之外的所有字符
\w [0-9a-zA-Z] (經(jīng)測試在Python3中 \w 表示[0-9a-zA-Z_]) 除了數(shù)字和字母外還會包括下劃線
\W 除了 \w 之外的所有字符即[^0-9a-zA-Z]
\d [0-9]
\D [^0-9]
環(huán)視
環(huán)視的中心思想是用來找出一個位置而不是字符,所以其只用來校驗不用來捕獲,也不占用匹配字符
肯定環(huán)視:
順序環(huán)視 (?=...) 從左往右校驗 當(dāng)遇到 滿足 條件的字段時,那么該字段的左邊位置便是匹配到的位置
逆序環(huán)視 (?<=...) 從右往左校驗 當(dāng)遇到 滿足 條件的字段時,那么該字段的右邊位置便是匹配到的位置
比如 (?<=goo)(?=d) 這個表達式用來找到一個 在goo右邊
在 d 左邊的一個位置,如果這個位置存在則為true否則為false
(?=good)(goo) 這個表達式的作用是是找到字符串good左邊的一個位置并捕獲
這個位置 后的goo 即只匹配good 單詞中的goo 不會匹配 goof中的goo
(ood)(?<=good) 和上面一樣,ood后面的位置必須是good的右邊,
因此只匹配good中的ood 不會匹配bood中的ood
(?=good)(ood) 該表達式永遠為false
因為 good 左邊位置的下一個字符是 g 而不是 o,
因此永遠為false
同理:(goo)(?<=good) 也永遠為false
因為(goo)的右邊緊鄰的是goo的右邊
而環(huán)視要求是good的右邊,這兩個條件矛盾,永不成立
否定環(huán)視:
否定順序環(huán)視 (?!...) 從左往右校驗 當(dāng)遇到 不滿足 條件的字段時,那么該字段的左邊位置便是匹配到的位置
否定逆序環(huán)視 (?<!...) 從右往左校驗 當(dāng)遇到 不滿足 條件的字段時,那么該字段的右邊位置便是匹配到的位置
同樣的例子:
(?!good)(goo) 先找到不是good字段的左邊的一個位置
并捕獲 這個位置 后的goo 即不匹配good中的goo 可以匹配 goof 中的goo
否定逆序同理
關(guān)于環(huán)視的主意點:
在Python中逆序環(huán)視必須是有明確長度的,
順序環(huán)視則沒有限制 比如 (?<=[a-z]+) 是不合法的,
因為[a-z]+的長度是不固定的 但在順序中可以 (?=[a-z]+) 是合法的
所以應(yīng)當(dāng)特別主意逆序中只能使用(?<=[a-z]) (?<=good)這種有明確長度的表示
判斷條件 (?... ...|...) 相當(dāng)于 (?if then | else)
?后面緊跟判斷條件,條件成立則匹配 | 左邊 否則匹配其右邊
(?(?:good)[A-Z]|[a-z]) good后面應(yīng)該是一個大寫字母 不是good則后面應(yīng)該是個小寫字母
匹配優(yōu)先量詞
?,*,+,{}
匹配優(yōu)先的匹配過程是先滿足前面匹配條件,
并且盡可能多的匹配:
比如 .*: 去匹配abc:abc: 匹配到的內(nèi)容不是 abc: 而是abc:abc:
原因就是.* 是匹配優(yōu)先的,
會直接匹配所有字符即 abc:abc: 當(dāng)匹配進行到':'時.*已經(jīng)把所有字符匹配完了,
但為了滿足整個表達式,所以.*匹配的內(nèi)容要進行回溯,回溯是后入先出的,即倒序的,
因此從第二個:開始,當(dāng)回溯到第二個:時正好滿足表達式
忽略優(yōu)先量詞
*?,??,+?,{}?
忽略優(yōu)先的匹配過程是:
先跳過略優(yōu)先的匹配進行下面的匹配,
如果下面的匹配不符合就回溯到忽略匹配,
比如把上面的例子改為 .*?: 去匹配 abc:abc:
此時其匹配的結(jié)果便是 abc: 而不是 abc:abc:
原因是 *?是忽略優(yōu)先的,因此在該匹配過程中會跳過.的匹配 先匹配 ':'
第一個a不能滿足 : 就回溯到a并用.去匹配,
因為.可以匹配a所以接著用:進行下面的匹配b 因為b也滿足不了:
所以回溯到b并用忽略條件去匹配 依次類推,
當(dāng)匹配到第一個:時此時正則表達式已滿足,匹配完成
占有優(yōu)先量詞
*+,?+,++,{}+
占有優(yōu)先和匹配優(yōu)先一樣會先盡可能多的滿足前面的匹配條件,
而且一旦內(nèi)容被匹配就不會再交出來,即不允許匹配到的內(nèi)容進行回溯
還是上面的例子 .*+: 去匹配 abc:abc: 此時匹配會失敗
原因是 .*+ 會直接匹配到所有的內(nèi)容 abc:abc:
當(dāng)匹配到表達式的 : 時因為已經(jīng)沒有內(nèi)容了,
正常情況下需要對前面匹配到的內(nèi)容進行回溯,
但前面的內(nèi)容是占有優(yōu)先的,不允許回溯,因此:無法匹配到內(nèi)容,所以匹配失敗
固化分組
(?>...)
固化分組的作用和占有優(yōu)先相同,即防止匹配的內(nèi)容回溯
比如 (?>.*): 匹配 abc:abc: 同樣會失敗,原因和占有優(yōu)先相同
回溯的補充:
關(guān)于回溯,其實回溯能夠?qū)崿F(xiàn)的原因是表達式引擎在匹配過程中的每一步都會保留一個備用狀態(tài),當(dāng)后面的匹配失敗時就會跳轉(zhuǎn)到上一個備用狀態(tài)即完成回溯
占有優(yōu)先和固化分組能夠避免回溯的原因是,占有優(yōu)先和固化分組的條件都不會保留備用狀態(tài),因此無法回溯
固化分組和占有優(yōu)先的意義在于可以優(yōu)化表達式,避免非必要的回溯,比如 .*+: 去匹配 abcdefg 當(dāng)程序匹配到g時表達式立即給出失敗,因為:無法匹配,又無法回溯,
如果改為匹配優(yōu)先 .*: 當(dāng)匹配到g時此時表達式開始匹配: 但是內(nèi)容已經(jīng)被.* 全部匹配了,所以要進行回溯到g此時仍不匹配繼續(xù)回溯到f依次一直回溯到a發(fā)現(xiàn)表達式確實無法滿足,此時才會報告失敗
匹配優(yōu)化
因為DFA是文本引導(dǎo)型,優(yōu)化表達式并無作用。而NFA則是表達式引導(dǎo)型,不同的表達式效率差別會非常大,但由于POSIX NFA效率低下和用到的很少,所以以下優(yōu)化沒有特殊說明則只針對傳統(tǒng)NFA引擎。
對于多選結(jié)構(gòu)來說一般把更加通用的匹配條件放到前面,會減少回溯次數(shù)
例如用(^"|")* 來匹配 abcdefghigk"lmn 就比 ("|^")* 要快 因為對于傳統(tǒng)NFA來說 多選結(jié)構(gòu)只有第一個條件匹配失敗才會回溯去匹配下一個條件 對于第一個表達式(^")* 第一個條件可以一直匹配到k 直到第一個"才會匹配失敗,此時回溯用(")來匹配 接下來繼續(xù)用(^")匹配到最后 因此第一個表達式只進行了一次回溯。而第二個表達式("|^")* 需要先用(")* 來匹配,因為一直到"之前都不滿足因此a-k每次匹配都要回溯后用(^")* 來匹配 這樣整個表達式匹配完成需要14次回溯
對于上面的例子還可以進一步優(yōu)化,(^"|")* 雖然可以減少回溯次數(shù),但仍需要15次迭代即匹配的次數(shù),為了減少迭代可以改寫成(^"+|") 這樣 由于+是匹配優(yōu)先的 所以(^"+)會一次性匹配 a-k,然后回溯一次之后再一次性匹配 l-n 這樣整個表達式 只進行了一次回溯 和 兩次迭代
這里要注意只有在傳統(tǒng)NFA引擎下才可以用(^"+|")*來優(yōu)化,
對于POSIX NFA來說這會引起超線性匹配,
實際上對于任何量詞的嵌套POSIX NFA 都會產(chǎn)生超線性匹配
比如([a-z]+)* 來匹配 abcdefghigklmn
因為POSIX NFA 下表達式并不會在第一次匹配成功后就停止,
而是會遍歷所有的可能 這樣由于+和*都是不定長量詞,
因此表達式會嘗試(a)bcdefghigklmn 也會嘗試 (ab)cdefghigklmn
也會嘗試 a(bc)defghigklmn 依次類推,
這個匹配數(shù)量是冪增長的 當(dāng)字符串長度超過40時將是一個天文數(shù)字對于
對于相似單詞的匹配多選分支的范圍盡量要小
比如匹配this 或 that 表達式 th(is|at) 要比 (this|that) 好
單字符多選分支可采用字符集來替代 對于指定字符可采用簡記符來代替 即 \d > [0-9] > (0|1|2|3|4|5|6|7|8|9)
另一種優(yōu)化
對于正則引擎,每次匹配都需要編譯表達式進行格式差錯校驗,校驗完成才會調(diào)動傳動引擎進行匹配,對于不含變量的表達式這樣重復(fù)校驗完全是沒必要的,可以進行預(yù)編譯并緩存,python 對預(yù)編譯的支持是通過 compile()來實現(xiàn)的
未完待續(xù)...
上面內(nèi)容均來自《正則表達式》一書,在接下來的學(xué)習(xí)中本人任然會把一些心得和總結(jié)在該文章中進行分享,希望對大家有些許的幫助