正則表達(dá)式(二)

`>本文是 Jan Goyvaerts 為 RegexBuddy 寫(xiě)的教程的譯文,版權(quán)歸原作者所有

在本文中講述了正則表達(dá)式中的:

向后引用
先前向后查看
條件測(cè)試
單詞邊界
選擇符
等表達(dá)式及例子,并分析了正則引擎在執(zhí)行匹配時(shí)的內(nèi)部機(jī)理。

單詞邊界

元字符\b是一種對(duì)位置進(jìn)行匹配的“錨”。這種匹配是 0 長(zhǎng)度匹配。

有 4 種位置被認(rèn)為是“單詞邊界”:

  1. 如果字符串的第一個(gè)字符是一個(gè)“單詞字符”,在字符串的第一個(gè)字符前的位置
  2. 如果字符串的最后一個(gè)字符是一個(gè)“單詞字符”,在字符串的最后一個(gè)字符后的位置
  3. “非單詞字符”緊跟在“單詞字符”之后時(shí),在“單詞字符”和“非單詞字符”之間
  4. “單詞字符”緊跟在“非單詞字符”之后時(shí),在“非單詞字符”和“單詞字符”之間

“單詞字符” 用“\w”匹配的字符
“非單詞字符” 用“\W”匹配的字符

在大多數(shù)的正則表達(dá)式實(shí)現(xiàn)中,“單詞字符”通常包括[a-zA-Z0-9_]


正則表達(dá)式\b4\b
能匹配單個(gè)的 4 ,而不是一個(gè)更大數(shù)的一部分(不會(huì)匹 配44中的 4)
即幾乎可以說(shuō)\b匹配一個(gè)“字母數(shù)字序列”的開(kāi)始和結(jié)束的位置。

“單詞邊界”的取反集為\B
他要匹配的位置是兩個(gè)“單詞字符”之間或者兩個(gè)“非單詞字符”之間的位置。

深入正則表達(dá)式引擎內(nèi)部


正則表達(dá)式\bis\b
字符串This island is beautiful

引擎 先處理符號(hào)\b
因?yàn)閈b 是 0 長(zhǎng)度 ,所以第一個(gè)字符 T 前面的位置會(huì)被檢查。
T 是一個(gè)“單詞字符” 且它之前的字符是一個(gè)空字符(void),這是一個(gè)單詞邊界,\b匹配成功
但正則表達(dá)式中的i和第1個(gè)字符T匹配失敗,回溯~

單詞邊界\b繼續(xù)匹配,第5個(gè)空格符和第4個(gè)字符s之間是一個(gè)單詞邊界,\b匹配成功
但正則表達(dá)式中的i和第5個(gè)空格符匹配失敗,回溯~

單詞邊界\b繼續(xù)匹配,第5個(gè)空格字符和第6個(gè)字符i之間是一個(gè)單詞邊界,\b匹配成功
正則表達(dá)式is和第6個(gè)第7個(gè)字符匹配成功
但第8個(gè)字符l不被單詞邊界\b匹配,匹配失敗,回溯~

單詞邊界\b繼續(xù)匹配,到了第 13 個(gè)字符i和前面一個(gè)空格符形成“單詞邊界”,同時(shí)isis匹配。正則表達(dá)式中第二個(gè)\b開(kāi)始匹配,
單詞s和他之后的空格符是一個(gè)單詞邊界,\b匹配成功。
正則表達(dá)式結(jié)束。
引擎“急著”返回成功匹配的結(jié)果。

選擇符

正則表達(dá)式中“|”表示選擇。你可以用選擇符匹配多個(gè)可能的正則表達(dá)式中的一個(gè)。
如果你想匹配文字“cat”或“dog”
正則表達(dá)式cat|dog
如果想多匹配就加入即可cat|dog|mouse|fish

選擇符在正則表達(dá)式中具有最低的優(yōu)先級(jí),即它告訴引擎,要么匹配選擇符左邊的所有表達(dá)式,要么匹配右邊的所有表達(dá)式。

你也可以用圓括號(hào)來(lái)限制選擇符的作用范圍。

\b(cat|dog)\b這樣告訴正則引擎把(cat|dog)當(dāng)成一個(gè)正則表達(dá)式單位來(lái)處理。

正則引擎是急切的:當(dāng)它找到一個(gè)有效的匹配時(shí),停止搜索。
因此在一定條件下,選擇符兩邊的表達(dá)式的順序?qū)Y(jié)果會(huì)有影響。


用正則表達(dá)式搜索一個(gè)編程語(yǔ)言的函數(shù)列表
Get 或 GetValue 或 Set 或 SetValue
一個(gè)明顯的解決方案是正則表達(dá)式Get|GetValue|Set|SetValue
結(jié)果
因?yàn)檎齽t表達(dá)式GetGetValue都失敗了,而Set匹配成功。因?yàn)檎齽t導(dǎo)向的引擎都是“急切”的,所以它會(huì)返回第一個(gè)成功的匹配,文本Set,而不去繼續(xù)搜索是否有其他更好的匹配。
和我們期望的相反,正則表達(dá)式并沒(méi)有匹配整個(gè)字符串。有幾種可能的解決辦法。
1.改變選項(xiàng)的順序,例如我們使用正則表達(dá)式GetValue|Get|SetValue|Set這樣我們就可以優(yōu)先搜索最長(zhǎng)的匹配。
2.把四個(gè)選項(xiàng)結(jié)合起來(lái)成兩個(gè)選項(xiàng)Get(Value)?|Set(Value)?
因?yàn)閱?wèn)號(hào)重復(fù)符是貪婪的, 所以 SetValue 總會(huì)在 Set 之前被匹配。
3.更好的方案是. 使用單詞邊界
\b(Get|GetValue|Set|SetValue)\b\b(Get(Value)?|Set(Value)?\b
既然所有的選擇都有相同的結(jié)尾,正則表達(dá)式可優(yōu)化為\b(Get|Set)(Value)?\b

組與向后引用

把正則表達(dá)式的一部分放在圓括號(hào)內(nèi),你可以將它們形成組。
然后你可以對(duì)整個(gè)組使用一些正則操作,例如重復(fù)操作符。

注意區(qū)別
()圓括號(hào)用于形成正則表達(dá)式組
[]用于定義字符集
{}用于定義重復(fù)操作

當(dāng)用()定義了一個(gè)正則表達(dá)式組后,正則引擎則會(huì)把被匹配的組按照順序編號(hào),存入緩存。
當(dāng)對(duì)被匹配的組進(jìn)行向后引用的時(shí)候,可以用“\數(shù)字”的方式進(jìn)行引用。
正則表達(dá)式\1引用第1個(gè)匹配的后向引用組,\2引用第2組,以此類推,\n引用第 n 個(gè)組。
\0則引用整個(gè)正則表達(dá)式本身。

假設(shè)你想匹配一個(gè) HTML 標(biāo)簽的開(kāi)始標(biāo)簽和結(jié)束標(biāo)簽,以及標(biāo)簽中間的文本。

要匹配<B>和</B>以及中間的文字。
文本<B>This is a test</B>
正則表達(dá)式<([A-Z][A-Z0-9]*)[^>]*>.*?</\1>

首先,正則表達(dá)式<將會(huì)匹配第一個(gè)文本字符<
然后,正則表達(dá)式[A-Z]匹配文本B
[A-Z0-9]*匹配 0 到多次字母數(shù)字,后面緊接著 非“>”的字符 0個(gè)到多個(gè)。
最后,正則表達(dá)式的>將會(huì)匹配文本<B>

接下來(lái)正則引擎將對(duì)結(jié)束標(biāo)簽之前的字符進(jìn)行惰性匹配(急于表功,為了找到最短的文本,每次.*?匹配成功后都試圖進(jìn)行正則表達(dá)式</的匹配。
然后正則表達(dá)式中的“\1”表示對(duì)前面匹配的組([A-Z][A-Z0-9]*)進(jìn)行引用,在本例中,被引用的是標(biāo)簽名 即 文本字符B,所以需要被匹配的結(jié)尾標(biāo)簽為</B>

可以多次引用相同的后向引用組
正則表達(dá)式([a-c])x\1x\1
會(huì)匹配文本
axaxa
bxbxb
cxcxc

如果用數(shù)字形式引用的組沒(méi)有有效的匹配,則引用到的內(nèi)容簡(jiǎn)單的為空。
一個(gè)后向引用不能用于它自身。

錯(cuò)誤正則表達(dá)式([abc]\1)
不能將\0用于一個(gè)正則表達(dá)式匹配本身,它只能用于替換操作中。

后向引用不能用于字符集內(nèi)部。
[]包含的字符集內(nèi)部 \1被解釋為八進(jìn)制形式的轉(zhuǎn)碼。
所以像正則表達(dá)式 (a)[\1b] 其中的\1并不表示后向引用。

向后引用會(huì)降低引擎的速度,因?yàn)樗枰鎯?chǔ)匹配的組。
如果你不需要向后引用,你可以告訴引擎對(duì)某個(gè)組不存儲(chǔ)。例如Get(?:Value)
其中(后面緊跟的?:會(huì)告訴引擎對(duì)于組(Value)不存儲(chǔ)匹配的值以供后向引用。

重復(fù)操作與后向引用

當(dāng)對(duì)組使用重復(fù)操作符時(shí),緩存里后向引用內(nèi)容會(huì)被不斷刷新,只保留最后匹配的內(nèi)容。


正則表達(dá)式([abc]+)=\1
可以匹配文本cab=cab

但正則表達(dá)式([abc])+=\1不會(huì)匹配文本cab=cab

因?yàn)?br> ([abc])第一次匹配文本c時(shí),\1已經(jīng)代表的是c
([abc])繼續(xù)匹配到了文本a \1已經(jīng)代表的是a
([abc])繼續(xù)匹配到了文本b 最后\1已經(jīng)代表的是b
所以正則表達(dá)式([abc])+=\1只會(huì)匹配到文本cab=b

應(yīng)用:檢查重復(fù)單詞
當(dāng)編輯文字時(shí),很容易就會(huì)輸入重復(fù)單詞如the the
使用 \b(\w+)\s+\1\b可以檢測(cè)到這些重復(fù)單詞。
要?jiǎng)h除第二個(gè)單詞,只要簡(jiǎn)單的利用替換功能替換掉“\1”即可

組的命名和引用

在 PHP,Python 中,可以用(?P<name>group)來(lái)對(duì)組進(jìn)行命名。
本例中?P<name>就是把組(group)命名為name
可以用 (?P=name)進(jìn)行引用

.NET 的命名組
.NET framework 也支持命名組。不幸的是,微軟的程序員們決定發(fā)明他們自己的語(yǔ)法, 而不是沿用 Perl、Python 的規(guī)則。目前為止,還沒(méi)有任何其他的正則表達(dá)式實(shí)現(xiàn)支持微軟發(fā)明的語(yǔ)法。
.NET 例
(?<first>group)(?’second’group)
正如你所看到的,.NET 供兩種詞法來(lái)創(chuàng)建命名組:
用尖括號(hào)<> 在字符串中使用更方便
用單引號(hào). 在 ASP 代碼中更有用 因?yàn)锳SP代碼中<>被用作 HTML 標(biāo)簽。

引用命名組
\k<name>\k’name’
當(dāng)進(jìn)行搜索替換時(shí),用${name}來(lái)引用一個(gè)命名組。

正則表達(dá)式的匹配模式

正則表達(dá)式引擎都支持三種匹配模式
/i使正則表達(dá)式對(duì)大小寫(xiě)不敏感
/s開(kāi)啟“單行模式”,即點(diǎn)號(hào).匹配換行符(nweline)
/m開(kāi)啟“多行模式”,即^$匹配換行符(nweline)的前面和后面的位置。

在正則表達(dá)式內(nèi)部打開(kāi)或關(guān)閉模式
如果你在正則表達(dá)式內(nèi)部插入修飾符(?ism)
則該修飾符只對(duì)其右邊的正則表達(dá)式起作用。 (?-i)是關(guān)閉大小寫(xiě)不敏感。你可以很快的進(jìn)行測(cè)試。
(?i)te(?-i)st應(yīng)該匹配 TEst,但不能匹配 teST 或 TEST

原子組與防止回溯

一些特殊情況下回溯會(huì)使得引擎的效率極其低下。

要匹配這樣的字串,字串中的每個(gè)字段間用逗號(hào)做分隔符,第 12 個(gè)字段由P開(kāi)頭。
容易想到這樣的正則表達(dá)式^(.*?,){11}P
這個(gè)正則表達(dá)式在正常情況下工作的很好。
但如果第 12 個(gè)字段不是由 P 開(kāi)頭,則會(huì)發(fā)生災(zāi)難性的回溯。
如文本
1,2,3,4,5,6,7,8,9,10,11,12,13

首先,正則表達(dá)式一直成功匹配直到第 12 個(gè)字符。這時(shí),前面的正則表達(dá)式消耗的字串為1,2,3,4,5,6,7,8,9,10,11,
正則表達(dá)式中的P并不匹配12 引擎進(jìn)行回溯,這時(shí)正則表達(dá)式消耗的字串為 1,2,3,4,5,6,7,8,9,10,11
繼續(xù)下一次匹配過(guò)程,下一個(gè)正則符號(hào)為點(diǎn)號(hào). 能匹配下一個(gè)逗號(hào),
,并不匹配字符12中的1 匹配失敗,繼續(xù)回溯。
... 這樣的回溯組合是個(gè)非常大的數(shù)量 可能會(huì)造成引擎崩潰

用于阻止這樣巨大的回溯有方案:
1.簡(jiǎn)單的方案 盡可能的使匹配精確
用取反字符集代替點(diǎn)號(hào)。例如我們用如下正則表達(dá) 式^([^,\r\n]*,){11}P
這樣可以使失敗回溯的次數(shù)下降到 11 次。

2.使用原子組
原子組的目的是使正則引擎失敗的更快一點(diǎn)。因此可以有效的阻止海量回溯。原子組的語(yǔ)法 是(?>正則表達(dá)式)
位于(?>)之間的所有正則表達(dá)式都會(huì)被認(rèn)為是一個(gè)單一的正則符號(hào)。 一旦匹配失敗,引擎將會(huì)回溯到原子組前面的正則表達(dá)式部分。前面的例子用原子組可以表達(dá)成^(?>(.*?,){11})P一旦第十二個(gè)字段匹配失敗,引擎回溯到原子組前面的^

向前查看與向后查看

Perl 5 引入了兩個(gè)強(qiáng)大的正則語(yǔ)法:“向前查看”和“向后查看”
他們也被稱作“零長(zhǎng)度斷言”。他們和錨定一樣都是 零長(zhǎng)度的(即該正則表達(dá)式不消耗被匹配的字符串)
不同之處在于“前后查看”會(huì)實(shí)際匹配字符,只是他們會(huì)拋棄匹配只返回匹配結(jié)果:匹配或不匹配。這就是為什么他們被稱作“斷言”。他們并不實(shí)際消耗字符串中的字符,而只是斷言一個(gè)匹配是否可能。
注意:Javascript 只支持向前查看,不支持向后查看。

肯定和否定式的向前查看

前面的例子
要查找一個(gè) q,后面沒(méi)有緊跟一個(gè) u
即 要么 q 后面沒(méi)有字符,要么后面的字符不是 u
采用否定式向前查看后的一個(gè)解決方案為q(?!u)
否定式向前查看的語(yǔ)法是(?!查看的內(nèi)容)

肯定式向前查看和否定式向前查看很類似:?=查看的內(nèi)容)

如果在“查看的內(nèi)容”部分有組,也會(huì)產(chǎn)生一個(gè)向后引用。但是向前查看本身并不會(huì)產(chǎn)生向后引用,也不會(huì)被計(jì)入向后引用的編號(hào)中。這是因?yàn)橄蚯安榭幢旧硎菚?huì)被拋棄掉的,只保留匹配與否的判斷結(jié)果。如果你想保留匹配的結(jié)果作為向后引用,你可以用(?=(regex))來(lái)產(chǎn)生一個(gè)向后引用。

肯定和否定式的先后查看

向后查看和向前查看有相同的效果,只是方向相反 否定式向后查看的語(yǔ)法是:<<(?<!查看內(nèi)容)>> 肯定式向后查看的語(yǔ)法是:<<(?<=查看內(nèi)容)>> 我們可以看到,和向前查看相比,多了一個(gè)表示方向的左尖括號(hào)。 例:<<(?<!a)b>>將會(huì)匹配一個(gè)沒(méi)有“a”作前導(dǎo)字符的“b”。 值得注意的是:向前查看從當(dāng)前字符串位置開(kāi)始對(duì)“查看”正則表達(dá)式進(jìn)行匹配;向后查
看則從當(dāng)前字符串位置開(kāi)始先后回溯一個(gè)字符,然后再開(kāi)始對(duì)“查看”正則表達(dá)式進(jìn)行匹配。

深入正則表達(dá)式引擎內(nèi)部

簡(jiǎn)單例子
把正則表達(dá)式q(?!u)應(yīng)用到字符串Iraq
正則表達(dá)式的第一個(gè)符號(hào)是q
開(kāi)始匹配,當(dāng)?shù)谒膫€(gè)字符q被匹配后, q后面是空字符(void)
而下一個(gè)正則符號(hào)是向前查看。引擎注意到已經(jīng)進(jìn)入了一個(gè)向前查看正則表達(dá)式部分。下一個(gè)正則符號(hào)u和空字符不匹配,從而導(dǎo)致向前查看里的正則表達(dá)式匹配失敗。因?yàn)槭且粋€(gè)否定式的向前查看,意味著整個(gè)向前查看結(jié)果是成功的。于是匹配 結(jié)果q被返回了。

我們?cè)诎严嗤恼齽t表達(dá)式應(yīng)用到文本quit
正則表達(dá)式q匹配了q 下一個(gè)正則符號(hào)是向前查看部分的正則表達(dá)式u
它匹配了字符串中的第二個(gè)字符i 引擎繼續(xù)走到下個(gè)字符i
引擎這時(shí)注意到向前查看部分已經(jīng)處理完了,并且向前查看已經(jīng)成功。于是引擎拋棄被匹配的字符串部分,這將導(dǎo)致引擎回退到字符u

因?yàn)橄蚯安榭词欠穸ㄊ降模馕吨榭床糠值某晒ζヅ鋵?dǎo)致了整個(gè)向前查看的失敗,因此 引擎不得不進(jìn)行回溯。最后因?yàn)樵贈(zèng)]有其他的文本q和正則表達(dá)式q匹配,所以整個(gè)匹配失敗了。

為了確保你能清楚地理解向前查看的實(shí)現(xiàn),讓我們把正則表達(dá)式q(?=u)i應(yīng)用到文本quit
正則表達(dá)式q首先匹配q
然后向前查看成功匹配u 匹配的部分被拋棄,只返回可以匹配的判斷結(jié)果。引擎從字符i回退到u

由于向前查看成功了,引擎繼續(xù)處理下一個(gè)正則符號(hào)<<i>>。 結(jié)果發(fā)現(xiàn)<<i>>和“u”不匹配。因此匹配失敗了。由于后面沒(méi)有其他的“q”,整個(gè)正則表達(dá) 式的匹配失敗了。
更進(jìn)一步理解正則表達(dá)式引擎內(nèi)部機(jī)制
讓我們把<<(?<=a)b>>應(yīng)用到“thingamabob”。引擎開(kāi)始處理向后查看部分的正則 符號(hào)和字符串中的第一個(gè)字符。在這個(gè)例子中,向后查看告訴正則表達(dá)式引擎回退一個(gè)字符,然 后查看是否有一個(gè)“a”被匹配。因?yàn)樵凇皌”前面沒(méi)有字符,所以引擎不能回退。因此向后查看 失敗了。引擎繼續(xù)走到下一個(gè)字符“h”。再一次,引擎暫時(shí)回退一個(gè)字符并檢查是否有個(gè)“a” 被匹配。結(jié)果發(fā)現(xiàn)了一個(gè)“t”。向后查看又失敗了。
向后查看繼續(xù)失敗,直到正則表達(dá)式到達(dá)了字符串中的“m”,于是肯定式的向后查看被 匹配了。因?yàn)樗橇汩L(zhǎng)度的,字符串的當(dāng)前位置仍然是“m”。下一個(gè)正則符號(hào)是<<b>>,和 “m”匹配失敗。下一個(gè)字符是字符串中的第二個(gè)“a”。引擎向后暫時(shí)回退一個(gè)字符,并且發(fā) 現(xiàn)<<a>>不匹配“m”。
在下一個(gè)字符是字符串中的第一個(gè)“b”。引擎暫時(shí)性的向后退一個(gè)字符發(fā)現(xiàn)向后查看被滿 足了,同時(shí)<<b>>匹配了“b”。因此整個(gè)正則表達(dá)式被匹配了。作為結(jié)果,正則表達(dá)式返回 字符串中的第一個(gè)“b”。
向前向后查看的應(yīng)用
我們來(lái)看這樣一個(gè)例子:查找一個(gè)具有 6 位字符的,含有“cat”的單詞。 首先,我們可以不用向前向后查看來(lái)解決問(wèn)題,例如:
<< cat\w{3}|\wcat\w{2}|\w{2}cat\w|\w{3}cat>> 足夠簡(jiǎn)單吧!但是當(dāng)需求變成查找一個(gè)具有 6-12 位字符,含有“cat”,“dog”或“mouse”
的單詞時(shí),這種方法就變得有些笨拙了。
我們來(lái)看看使用向前查看的方案。在這個(gè)例子中,我們有兩個(gè)基本需求要滿足:一是我們
需要一個(gè) 6 位的字符,二是單詞含有“cat”。 滿足第一個(gè)需求的正則表達(dá)式為<<\b\w{6}\b>>。滿足第二個(gè)需求的正則表達(dá)式為
<<\b\wcat\w\b>>。
把兩者結(jié)合起來(lái),我們可以得到如下的正則表達(dá)式:
<<(?=\b\w{6}\b)\b\wcat\w\b>>
具體的匹配過(guò)程留給讀者。但是要注意的一點(diǎn)是,向前查看是不消耗字符的,因此當(dāng)判斷 單詞滿足具有 6 個(gè)字符的條件后,引擎會(huì)從開(kāi)始判斷前的位置繼續(xù)對(duì)后面的正則表達(dá)式進(jìn)行匹 配。
最后作些優(yōu)化,可以得到下面的正則表達(dá)式:
<<\b(?=\w{6}\b)\w{0,3}cat\w*>>

  1. 正則表達(dá)式中的條件測(cè)試 條件測(cè)試的語(yǔ)法為<<(?ifthen|else)>>。“if”部分可以是向前向后查看表達(dá)式。如果用
    向前查看,則語(yǔ)法變?yōu)?<<(?(?=regex)then|else)>>,其中 else 部分是可選的。
    如果 if 部分為 true,則正則引擎會(huì)試圖匹配 then 部分,否則引擎會(huì)試圖匹配 else 部分。 需要記住的是,向前先后查看并不實(shí)際消耗任何字符,因此后面的 then 與 else 部分的匹
    配時(shí)從 if 測(cè)試前的部分開(kāi)始進(jìn)行嘗試。
  2. 為正則表達(dá)式添加注釋 在正則表達(dá)式中添加注釋的語(yǔ)法是:<<(?#comment)>> 例:為用于匹配有效日期的正則表達(dá)式添加注釋:
    (?#year)(19|20)\d\d- /.(0[1-9]|1[012])- /.(0[1-9]|[12][0-9]|3[01])
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容