正則表達式(二)

`>本文是 Jan Goyvaerts 為 RegexBuddy 寫的教程的譯文,版權歸原作者所有

在本文中講述了正則表達式中的:

向后引用
先前向后查看
條件測試
單詞邊界
選擇符
等表達式及例子,并分析了正則引擎在執行匹配時的內部機理。

單詞邊界

元字符\b是一種對位置進行匹配的“錨”。這種匹配是 0 長度匹配。

有 4 種位置被認為是“單詞邊界”:

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

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

在大多數的正則表達式實現中,“單詞字符”通常包括[a-zA-Z0-9_]


正則表達式\b4\b
能匹配單個的 4 ,而不是一個更大數的一部分(不會匹 配44中的 4)
即幾乎可以說\b匹配一個“字母數字序列”的開始和結束的位置。

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

深入正則表達式引擎內部


正則表達式\bis\b
字符串This island is beautiful

引擎 先處理符號\b
因為\b 是 0 長度 ,所以第一個字符 T 前面的位置會被檢查。
T 是一個“單詞字符” 且它之前的字符是一個空字符(void),這是一個單詞邊界,\b匹配成功
但正則表達式中的i和第1個字符T匹配失敗,回溯~

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

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

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

選擇符

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

選擇符在正則表達式中具有最低的優先級,即它告訴引擎,要么匹配選擇符左邊的所有表達式,要么匹配右邊的所有表達式。

你也可以用圓括號來限制選擇符的作用范圍。

\b(cat|dog)\b這樣告訴正則引擎把(cat|dog)當成一個正則表達式單位來處理。

正則引擎是急切的:當它找到一個有效的匹配時,停止搜索。
因此在一定條件下,選擇符兩邊的表達式的順序對結果會有影響。


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

組與向后引用

把正則表達式的一部分放在圓括號內,你可以將它們形成組。
然后你可以對整個組使用一些正則操作,例如重復操作符。

注意區別
()圓括號用于形成正則表達式組
[]用于定義字符集
{}用于定義重復操作

當用()定義了一個正則表達式組后,正則引擎則會把被匹配的組按照順序編號,存入緩存。
當對被匹配的組進行向后引用的時候,可以用“\數字”的方式進行引用。
正則表達式\1引用第1個匹配的后向引用組,\2引用第2組,以此類推,\n引用第 n 個組。
\0則引用整個正則表達式本身。

假設你想匹配一個 HTML 標簽的開始標簽和結束標簽,以及標簽中間的文本。

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

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

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

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

如果用數字形式引用的組沒有有效的匹配,則引用到的內容簡單的為空。
一個后向引用不能用于它自身。

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

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

向后引用會降低引擎的速度,因為它需要存儲匹配的組。
如果你不需要向后引用,你可以告訴引擎對某個組不存儲。例如Get(?:Value)
其中(后面緊跟的?:會告訴引擎對于組(Value)不存儲匹配的值以供后向引用。

重復操作與后向引用

當對組使用重復操作符時,緩存里后向引用內容會被不斷刷新,只保留最后匹配的內容。


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

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

因為
([abc])第一次匹配文本c時,\1已經代表的是c
([abc])繼續匹配到了文本a \1已經代表的是a
([abc])繼續匹配到了文本b 最后\1已經代表的是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 也支持命名組。不幸的是,微軟的程序員們決定發明他們自己的語法, 而不是沿用 Perl、Python 的規則。目前為止,還沒有任何其他的正則表達式實現支持微軟發明的語法。
.NET 例
(?<first>group)(?’second’group)
正如你所看到的,.NET 供兩種詞法來創建命名組:
用尖括號<> 在字符串中使用更方便
用單引號. 在 ASP 代碼中更有用 因為ASP代碼中<>被用作 HTML 標簽。

引用命名組
\k<name>\k’name’
當進行搜索替換時,用${name}來引用一個命名組。

正則表達式的匹配模式

正則表達式引擎都支持三種匹配模式
/i使正則表達式對大小寫不敏感
/s開啟“單行模式”,即點號.匹配換行符(nweline)
/m開啟“多行模式”,即^$匹配換行符(nweline)的前面和后面的位置。

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

原子組與防止回溯

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

要匹配這樣的字串,字串中的每個字段間用逗號做分隔符,第 12 個字段由P開頭。
容易想到這樣的正則表達式^(.*?,){11}P
這個正則表達式在正常情況下工作的很好。
但如果第 12 個字段不是由 P 開頭,則會發生災難性的回溯。
如文本
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
繼續下一次匹配過程,下一個正則符號為點號. 能匹配下一個逗號,
,并不匹配字符12中的1 匹配失敗,繼續回溯。
... 這樣的回溯組合是個非常大的數量 可能會造成引擎崩潰

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

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

向前查看與向后查看

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

肯定和否定式的向前查看

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

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

如果在“查看的內容”部分有組,也會產生一個向后引用。但是向前查看本身并不會產生向后引用,也不會被計入向后引用的編號中。這是因為向前查看本身是會被拋棄掉的,只保留匹配與否的判斷結果。如果你想保留匹配的結果作為向后引用,你可以用(?=(regex))來產生一個向后引用。

肯定和否定式的先后查看

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

深入正則表達式引擎內部

簡單例子
把正則表達式q(?!u)應用到字符串Iraq
正則表達式的第一個符號是q
開始匹配,當第四個字符q被匹配后, q后面是空字符(void)
而下一個正則符號是向前查看。引擎注意到已經進入了一個向前查看正則表達式部分。下一個正則符號u和空字符不匹配,從而導致向前查看里的正則表達式匹配失敗。因為是一個否定式的向前查看,意味著整個向前查看結果是成功的。于是匹配 結果q被返回了。

我們在把相同的正則表達式應用到文本quit
正則表達式q匹配了q 下一個正則符號是向前查看部分的正則表達式u
它匹配了字符串中的第二個字符i 引擎繼續走到下個字符i
引擎這時注意到向前查看部分已經處理完了,并且向前查看已經成功。于是引擎拋棄被匹配的字符串部分,這將導致引擎回退到字符u

因為向前查看是否定式的,意味著查看部分的成功匹配導致了整個向前查看的失敗,因此 引擎不得不進行回溯。最后因為再沒有其他的文本q和正則表達式q匹配,所以整個匹配失敗了。

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

由于向前查看成功了,引擎繼續處理下一個正則符號<<i>>。 結果發現<<i>>和“u”不匹配。因此匹配失敗了。由于后面沒有其他的“q”,整個正則表達 式的匹配失敗了。
更進一步理解正則表達式引擎內部機制
讓我們把<<(?<=a)b>>應用到“thingamabob”。引擎開始處理向后查看部分的正則 符號和字符串中的第一個字符。在這個例子中,向后查看告訴正則表達式引擎回退一個字符,然 后查看是否有一個“a”被匹配。因為在“t”前面沒有字符,所以引擎不能回退。因此向后查看 失敗了。引擎繼續走到下一個字符“h”。再一次,引擎暫時回退一個字符并檢查是否有個“a” 被匹配。結果發現了一個“t”。向后查看又失敗了。
向后查看繼續失敗,直到正則表達式到達了字符串中的“m”,于是肯定式的向后查看被 匹配了。因為它是零長度的,字符串的當前位置仍然是“m”。下一個正則符號是<<b>>,和 “m”匹配失敗。下一個字符是字符串中的第二個“a”。引擎向后暫時回退一個字符,并且發 現<<a>>不匹配“m”。
在下一個字符是字符串中的第一個“b”。引擎暫時性的向后退一個字符發現向后查看被滿 足了,同時<<b>>匹配了“b”。因此整個正則表達式被匹配了。作為結果,正則表達式返回 字符串中的第一個“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>>。
把兩者結合起來,我們可以得到如下的正則表達式:
<<(?=\b\w{6}\b)\b\wcat\w\b>>
具體的匹配過程留給讀者。但是要注意的一點是,向前查看是不消耗字符的,因此當判斷 單詞滿足具有 6 個字符的條件后,引擎會從開始判斷前的位置繼續對后面的正則表達式進行匹 配。
最后作些優化,可以得到下面的正則表達式:
<<\b(?=\w{6}\b)\w{0,3}cat\w*>>

  1. 正則表達式中的條件測試 條件測試的語法為<<(?ifthen|else)>>。“if”部分可以是向前向后查看表達式。如果用
    向前查看,則語法變為:<<(?(?=regex)then|else)>>,其中 else 部分是可選的。
    如果 if 部分為 true,則正則引擎會試圖匹配 then 部分,否則引擎會試圖匹配 else 部分。 需要記住的是,向前先后查看并不實際消耗任何字符,因此后面的 then 與 else 部分的匹
    配時從 if 測試前的部分開始進行嘗試。
  2. 為正則表達式添加注釋 在正則表達式中添加注釋的語法是:<<(?#comment)>> 例:為用于匹配有效日期的正則表達式添加注釋:
    (?#year)(19|20)\d\d- /.(0[1-9]|1[012])- /.(0[1-9]|[12][0-9]|3[01])
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容