`>本文是 Jan Goyvaerts 為 RegexBuddy 寫的教程的譯文盈电,版權(quán)歸原作者所有
在本文中講述了正則表達式中的:
組
向后引用
先前向后查看
條件測試
單詞邊界
選擇符
等表達式及例子,并分析了正則引擎在執(zhí)行匹配時的內(nèi)部機理冶匹。
單詞邊界
元字符\b是一種對位置進行匹配的“錨”陷虎。這種匹配是 0 長度匹配皆警。
有 4 種位置被認為是“單詞邊界”:
- 如果字符串的第一個字符是一個“單詞字符”凰浮,在字符串的第一個字符前的位置
- 如果字符串的最后一個字符是一個“單詞字符”膜廊,在字符串的最后一個字符后的位置
- “非單詞字符”緊跟在“單詞字符”之后時,在“單詞字符”和“非單詞字符”之間
- “單詞字符”緊跟在“非單詞字符”之后時冲粤,在“非單詞字符”和“單詞字符”之間
“單詞字符” 用“\w”匹配的字符
“非單詞字符” 用“\W”匹配的字符
在大多數(shù)的正則表達式實現(xiàn)中,“單詞字符”通常包括[a-zA-Z0-9_]
例
正則表達式\b4\b
能匹配單個的 4 ,而不是一個更大數(shù)的一部分(不會匹 配44中的 4)
即幾乎可以說\b匹配一個“字母數(shù)字序列”的開始和結(jié)束的位置页眯。
“單詞邊界”的取反集為\B
他要匹配的位置是兩個“單詞字符”之間或者兩個“非單詞字符”之間的位置梯捕。
深入正則表達式引擎內(nèi)部
例
正則表達式\bis\b
字符串This island is beautiful
引擎 先處理符號\b
因為\b 是 0 長度 ,所以第一個字符 T 前面的位置會被檢查窝撵。
T 是一個“單詞字符” 且它之前的字符是一個空字符(void)傀顾,這是一個單詞邊界,\b
匹配成功
但正則表達式中的i
和第1個字符T匹配失敗碌奉,回溯~
單詞邊界\b
繼續(xù)匹配短曾,第5個空格符和第4個字符s之間是一個單詞邊界,\b
匹配成功
但正則表達式中的i
和第5個空格符匹配失敗赐劣,回溯~
單詞邊界\b
繼續(xù)匹配嫉拐,第5個空格字符和第6個字符i之間是一個單詞邊界,\b
匹配成功
正則表達式is
和第6個第7個字符匹配成功
但第8個字符l不被單詞邊界\b
匹配魁兼,匹配失敗婉徘,回溯~
單詞邊界\b
繼續(xù)匹配,到了第 13 個字符i和前面一個空格符形成“單詞邊界”,同時is
和is
匹配盖呼。正則表達式中第二個\b
開始匹配儒鹿,
單詞s和他之后的空格符是一個單詞邊界,\b
匹配成功几晤。
正則表達式結(jié)束约炎。
引擎“急著”返回成功匹配的結(jié)果。
選擇符
正則表達式中“|”表示選擇蟹瘾。你可以用選擇符匹配多個可能的正則表達式中的一個圾浅。
如果你想匹配文字“cat”或“dog”
正則表達式cat|dog
如果想多匹配就加入即可cat|dog|mouse|fish
選擇符在正則表達式中具有最低的優(yōu)先級,即它告訴引擎热芹,要么匹配選擇符左邊的所有表達式贱傀,要么匹配右邊的所有表達式。
你也可以用圓括號來限制選擇符的作用范圍伊脓。
例
\b(cat|dog)\b
這樣告訴正則引擎把(cat|dog)當成一個正則表達式單位來處理府寒。
正則引擎是急切的:當它找到一個有效的匹配時,停止搜索报腔。
因此在一定條件下株搔,選擇符兩邊的表達式的順序?qū)Y(jié)果會有影響。
例
用正則表達式搜索一個編程語言的函數(shù)列表
Get 或 GetValue 或 Set 或 SetValue
一個明顯的解決方案是正則表達式Get|GetValue|Set|SetValue
結(jié)果
因為正則表達式Get
和GetValue
都失敗了纯蛾,而Set
匹配成功纤房。因為正則導向的引擎都是“急切”的,所以它會返回第一個成功的匹配翻诉,文本Set炮姨,而不去繼續(xù)搜索是否有其他更好的匹配。
和我們期望的相反碰煌,正則表達式并沒有匹配整個字符串舒岸。有幾種可能的解決辦法。
1.改變選項的順序芦圾,例如我們使用正則表達式GetValue|Get|SetValue|Set
這樣我們就可以優(yōu)先搜索最長的匹配蛾派。
2.把四個選項結(jié)合起來成兩個選項Get(Value)?|Set(Value)?
因為問號重復符是貪婪的, 所以 SetValue 總會在 Set 之前被匹配个少。
3.更好的方案是. 使用單詞邊界
\b(Get|GetValue|Set|SetValue)\b
或 \b(Get(Value)?|Set(Value)?\b
既然所有的選擇都有相同的結(jié)尾洪乍,正則表達式可優(yōu)化為\b(Get|Set)(Value)?\b
組與向后引用
把正則表達式的一部分放在圓括號內(nèi),你可以將它們形成組夜焦。
然后你可以對整個組使用一些正則操作壳澳,例如重復操作符。
注意區(qū)別
()圓括號用于形成正則表達式組
[]用于定義字符集
{}用于定義重復操作
當用()定義了一個正則表達式組后糊探,正則引擎則會把被匹配的組按照順序編號钾埂,存入緩存河闰。
當對被匹配的組進行向后引用的時候,可以用“\數(shù)字”的方式進行引用褥紫。
正則表達式\1
引用第1個匹配的后向引用組姜性,\2
引用第2組,以此類推髓考,\n
引用第 n 個組部念。
而\0
則引用整個正則表達式本身。
假設你想匹配一個 HTML 標簽的開始標簽和結(jié)束標簽氨菇,以及標簽中間的文本儡炼。
例
要匹配<B>和</B>以及中間的文字。
文本<B>This is a test</B>
正則表達式<([A-Z][A-Z0-9]*)[^>]*>.*?</\1>
首先查蓉,正則表達式<將會匹配第一個文本字符<
然后乌询,正則表達式[A-Z]匹配文本B
[A-Z0-9]*匹配 0 到多次字母數(shù)字,后面緊接著 非“>”的字符 0個到多個豌研。
最后妹田,正則表達式的>
將會匹配文本<B>
接下來正則引擎將對結(jié)束標簽之前的字符進行惰性匹配(急于表功,為了找到最短的文本鹃共,每次.*?匹配成功后都試圖進行正則表達式</
的匹配鬼佣。
然后正則表達式中的“\1”表示對前面匹配的組([A-Z][A-Z0-9]*)
進行引用,在本例中霜浴,被引用的是標簽名 即 文本字符B
,所以需要被匹配的結(jié)尾標簽為</B>
可以多次引用相同的后向引用組
正則表達式([a-c])x\1x\1
會匹配文本
axaxa
bxbxb
cxcxc
如果用數(shù)字形式引用的組沒有有效的匹配晶衷,則引用到的內(nèi)容簡單的為空。
一個后向引用不能用于它自身阴孟。
錯誤正則表達式([abc]\1)
不能將\0
用于一個正則表達式匹配本身晌纫,它只能用于替換操作中。
后向引用不能用于字符集內(nèi)部永丝。
在[]
包含的字符集內(nèi)部 \1
被解釋為八進制形式的轉(zhuǎn)碼缸匪。
所以像正則表達式 (a)[\1b]
其中的\1
并不表示后向引用。
向后引用會降低引擎的速度类溢,因為它需要存儲匹配的組。
如果你不需要向后引用露懒,你可以告訴引擎對某個組不存儲闯冷。例如Get(?:Value)
其中(
后面緊跟的?:
會告訴引擎對于組(Value)
不存儲匹配的值以供后向引用。
重復操作與后向引用
當對組使用重復操作符時懈词,緩存里后向引用內(nèi)容會被不斷刷新蛇耀,只保留最后匹配的內(nèi)容。
例
正則表達式([abc]+)=\1
可以匹配文本cab=cab
但正則表達式([abc])+=\1
不會匹配文本cab=cab
因為
([abc])
第一次匹配文本c
時坎弯,\1
已經(jīng)代表的是c
([abc])
繼續(xù)匹配到了文本a
\1
已經(jīng)代表的是a
([abc])
繼續(xù)匹配到了文本b
最后\1
已經(jīng)代表的是b
所以正則表達式([abc])+=\1
只會匹配到文本cab=b
應用:檢查重復單詞
當編輯文字時纺涤,很容易就會輸入重復單詞如the the
使用 \b(\w+)\s+\1\b
可以檢測到這些重復單詞译暂。
要刪除第二個單詞,只要簡單的利用替換功能替換掉“\1”即可
組的命名和引用
在 PHP撩炊,Python 中外永,可以用(?P<name>group)
來對組進行命名。
本例中?P<name>
就是把組(group)
命名為name
可以用 (?P=name)進行引用
.NET 的命名組
.NET framework 也支持命名組拧咳。不幸的是伯顶,微軟的程序員們決定發(fā)明他們自己的語法, 而不是沿用 Perl骆膝、Python 的規(guī)則祭衩。目前為止,還沒有任何其他的正則表達式實現(xiàn)支持微軟發(fā)明的語法阅签。
.NET 例
(?<first>group)(?’second’group)
正如你所看到的掐暮,.NET 供兩種詞法來創(chuàng)建命名組:
用尖括號<>
在字符串中使用更方便
用單引號. 在 ASP 代碼中更有用 因為ASP代碼中<>
被用作 HTML 標簽。
引用命名組
\k<name>
或 \k’name’
當進行搜索替換時政钟,用${name}
來引用一個命名組路克。
正則表達式的匹配模式
正則表達式引擎都支持三種匹配模式
/i
使正則表達式對大小寫不敏感
/s
開啟“單行模式”,即點號.
匹配換行符(nweline)
/m
開啟“多行模式”锥涕,即^
和$
匹配換行符(nweline)的前面和后面的位置衷戈。
在正則表達式內(nèi)部打開或關(guān)閉模式
如果你在正則表達式內(nèi)部插入修飾符(?ism)
則該修飾符只對其右邊的正則表達式起作用。 (?-i)是關(guān)閉大小寫不敏感层坠。你可以很快的進行測試殖妇。
(?i)te(?-i)st
應該匹配 TEst,但不能匹配 teST 或 TEST
原子組與防止回溯
一些特殊情況下回溯會使得引擎的效率極其低下破花。
例
要匹配這樣的字串谦趣,字串中的每個字段間用逗號做分隔符,第 12 個字段由P開頭座每。
容易想到這樣的正則表達式^(.*?,){11}P
這個正則表達式在正常情況下工作的很好前鹅。
但如果第 12 個字段不是由 P 開頭,則會發(fā)生災難性的回溯峭梳。
如文本
1,2,3,4,5,6,7,8,9,10,11,12,13
首先舰绘,正則表達式一直成功匹配直到第 12 個字符。這時葱椭,前面的正則表達式消耗的字串為1,2,3,4,5,6,7,8,9,10,11,
正則表達式中的P
并不匹配12
引擎進行回溯捂寿,這時正則表達式消耗的字串為 1,2,3,4,5,6,7,8,9,10,11
繼續(xù)下一次匹配過程,下一個正則符號為點號.
能匹配下一個逗號,
但,
并不匹配字符12
中的1
匹配失敗孵运,繼續(xù)回溯秦陋。
... 這樣的回溯組合是個非常大的數(shù)量 可能會造成引擎崩潰
用于阻止這樣巨大的回溯有方案:
1.簡單的方案 盡可能的使匹配精確
用取反字符集代替點號。例如我們用如下正則表達 式^([^,\r\n]*,){11}P
這樣可以使失敗回溯的次數(shù)下降到 11 次治笨。
2.使用原子組
原子組的目的是使正則引擎失敗的更快一點驳概。因此可以有效的阻止海量回溯赤嚼。原子組的語法 是(?>正則表達式)
位于(?>)之間的所有正則表達式都會被認為是一個單一的正則符號。 一旦匹配失敗顺又,引擎將會回溯到原子組前面的正則表達式部分更卒。前面的例子用原子組可以表達成^(?>(.*?,){11})P
一旦第十二個字段匹配失敗,引擎回溯到原子組前面的^
向前查看與向后查看
Perl 5 引入了兩個強大的正則語法:“向前查看”和“向后查看”
他們也被稱作“零長度斷言”待榔。他們和錨定一樣都是 零長度的(即該正則表達式不消耗被匹配的字符串)
不同之處在于“前后查看”會實際匹配字符逞壁,只是他們會拋棄匹配只返回匹配結(jié)果:匹配或不匹配。這就是為什么他們被稱作“斷言”锐锣。他們并不實際消耗字符串中的字符腌闯,而只是斷言一個匹配是否可能。
注意:Javascript 只支持向前查看雕憔,不支持向后查看姿骏。
肯定和否定式的向前查看
前面的例子
要查找一個 q,后面沒有緊跟一個 u
即 要么 q 后面沒有字符斤彼,要么后面的字符不是 u
采用否定式向前查看后的一個解決方案為q(?!u)
否定式向前查看的語法是(?!查看的內(nèi)容)
肯定式向前查看和否定式向前查看很類似:?=查看的內(nèi)容)
如果在“查看的內(nèi)容”部分有組分瘦,也會產(chǎn)生一個向后引用。但是向前查看本身并不會產(chǎn)生向后引用琉苇,也不會被計入向后引用的編號中嘲玫。這是因為向前查看本身是會被拋棄掉的,只保留匹配與否的判斷結(jié)果并扇。如果你想保留匹配的結(jié)果作為向后引用去团,你可以用(?=(regex))
來產(chǎn)生一個向后引用。
肯定和否定式的先后查看
向后查看和向前查看有相同的效果穷蛹,只是方向相反 否定式向后查看的語法是:<<(?<!查看內(nèi)容)>> 肯定式向后查看的語法是:<<(?<=查看內(nèi)容)>> 我們可以看到土陪,和向前查看相比,多了一個表示方向的左尖括號肴熏。 例:<<(?<!a)b>>將會匹配一個沒有“a”作前導字符的“b”鬼雀。 值得注意的是:向前查看從當前字符串位置開始對“查看”正則表達式進行匹配;向后查
看則從當前字符串位置開始先后回溯一個字符,然后再開始對“查看”正則表達式進行匹配蛙吏。
深入正則表達式引擎內(nèi)部
簡單例子
把正則表達式q(?!u)
應用到字符串Iraq
正則表達式的第一個符號是q
開始匹配源哩,當?shù)谒膫€字符q
被匹配后, q
后面是空字符(void)
而下一個正則符號是向前查看鸦做。引擎注意到已經(jīng)進入了一個向前查看正則表達式部分璧疗。下一個正則符號u
和空字符不匹配,從而導致向前查看里的正則表達式匹配失敗馁龟。因為是一個否定式的向前查看,意味著整個向前查看結(jié)果是成功的漆魔。于是匹配 結(jié)果q
被返回了坷檩。
我們在把相同的正則表達式應用到文本quit
正則表達式q
匹配了q
下一個正則符號是向前查看部分的正則表達式u
它匹配了字符串中的第二個字符i
引擎繼續(xù)走到下個字符i
引擎這時注意到向前查看部分已經(jīng)處理完了却音,并且向前查看已經(jīng)成功。于是引擎拋棄被匹配的字符串部分矢炼,這將導致引擎回退到字符u
因為向前查看是否定式的系瓢,意味著查看部分的成功匹配導致了整個向前查看的失敗,因此 引擎不得不進行回溯句灌。最后因為再沒有其他的文本q
和正則表達式q
匹配夷陋,所以整個匹配失敗了。
為了確保你能清楚地理解向前查看的實現(xiàn)胰锌,讓我們把正則表達式q(?=u)i
應用到文本quit
正則表達式q
首先匹配q
然后向前查看成功匹配u
匹配的部分被拋棄骗绕,只返回可以匹配的判斷結(jié)果。引擎從字符i
回退到u
由于向前查看成功了资昧,引擎繼續(xù)處理下一個正則符號<<i>>酬土。 結(jié)果發(fā)現(xiàn)<<i>>和“u”不匹配。因此匹配失敗了格带。由于后面沒有其他的“q”撤缴,整個正則表達 式的匹配失敗了。
更進一步理解正則表達式引擎內(nèi)部機制
讓我們把<<(?<=a)b>>應用到“thingamabob”叽唱。引擎開始處理向后查看部分的正則 符號和字符串中的第一個字符屈呕。在這個例子中,向后查看告訴正則表達式引擎回退一個字符棺亭,然 后查看是否有一個“a”被匹配虎眨。因為在“t”前面沒有字符,所以引擎不能回退侦铜。因此向后查看 失敗了专甩。引擎繼續(xù)走到下一個字符“h”。再一次钉稍,引擎暫時回退一個字符并檢查是否有個“a” 被匹配涤躲。結(jié)果發(fā)現(xiàn)了一個“t”。向后查看又失敗了贡未。
向后查看繼續(xù)失敗种樱,直到正則表達式到達了字符串中的“m”壳坪,于是肯定式的向后查看被 匹配了陡叠。因為它是零長度的,字符串的當前位置仍然是“m”绘沉。下一個正則符號是<<b>>消恍,和 “m”匹配失敗岂昭。下一個字符是字符串中的第二個“a”。引擎向后暫時回退一個字符狠怨,并且發(fā) 現(xiàn)<<a>>不匹配“m”约啊。
在下一個字符是字符串中的第一個“b”邑遏。引擎暫時性的向后退一個字符發(fā)現(xiàn)向后查看被滿 足了,同時<<b>>匹配了“b”恰矩。因此整個正則表達式被匹配了记盒。作為結(jié)果,正則表達式返回 字符串中的第一個“b”外傅。
向前向后查看的應用
我們來看這樣一個例子:查找一個具有 6 位字符的纪吮,含有“cat”的單詞。 首先萎胰,我們可以不用向前向后查看來解決問題碾盟,例如:
<< cat\w{3}|\wcat\w{2}|\w{2}cat\w|\w{3}cat>> 足夠簡單吧!但是當需求變成查找一個具有 6-12 位字符,含有“cat”奥洼,“dog”或“mouse”
的單詞時巷疼,這種方法就變得有些笨拙了。
我們來看看使用向前查看的方案灵奖。在這個例子中嚼沿,我們有兩個基本需求要滿足:一是我們
需要一個 6 位的字符,二是單詞含有“cat”瓷患。 滿足第一個需求的正則表達式為<<\b\w{6}\b>>骡尽。滿足第二個需求的正則表達式為
<<\b\wcat\w\b>>。
把兩者結(jié)合起來擅编,我們可以得到如下的正則表達式:
<<(?=\b\w{6}\b)\b\wcat\w\b>>
具體的匹配過程留給讀者攀细。但是要注意的一點是,向前查看是不消耗字符的爱态,因此當判斷 單詞滿足具有 6 個字符的條件后谭贪,引擎會從開始判斷前的位置繼續(xù)對后面的正則表達式進行匹 配。
最后作些優(yōu)化锦担,可以得到下面的正則表達式:
<<\b(?=\w{6}\b)\w{0,3}cat\w*>>
- 正則表達式中的條件測試 條件測試的語法為<<(?ifthen|else)>>俭识。“if”部分可以是向前向后查看表達式洞渔。如果用
向前查看套媚,則語法變?yōu)?<<(?(?=regex)then|else)>>,其中 else 部分是可選的磁椒。
如果 if 部分為 true堤瘤,則正則引擎會試圖匹配 then 部分,否則引擎會試圖匹配 else 部分浆熔。 需要記住的是本辐,向前先后查看并不實際消耗任何字符,因此后面的 then 與 else 部分的匹
配時從 if 測試前的部分開始進行嘗試。 - 為正則表達式添加注釋 在正則表達式中添加注釋的語法是:<<(?#comment)>> 例:為用于匹配有效日期的正則表達式添加注釋:
(?#year)(19|20)\d\d- /.(0[1-9]|1[012])- /.(0[1-9]|[12][0-9]|3[01])