正則表達式的先行斷言和後行斷言一共有 4 種形式:

  • (?=pattern) 零寬正向先行斷言(zero-width positive lookahead assertion)
  • (?!pattern) 零寬負向先行斷言(zero-width negative lookahead assertion)
  • (?<=pattern) 零寬正向後行斷言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零寬負向後行斷言(zero-width negative lookbehind assertion)

這裏麵的 pattern 是一個正則表達式。

如同 ^ 代表開頭,$ 代表結尾,\b 代表單詞邊界一樣,先行斷言和後行斷言也有類似的作用,它們隻匹配某些位置,在匹配過程中,不占用字符,所以被稱為"零寬"。所謂位置,是指字符串中(每行)第一個字符的左邊、最後一個字符的右邊以及相鄰字符的中間(假設文字方向是頭左尾右)。

下麵分別舉例來說明這 4 種斷言的含義。

(?=pattern) 正向先行斷言

代表字符串中的一個位置,緊接該位置之後的字符序列能夠匹配 pattern。

例如對 "a regular expression" 這個字符串,要想匹配 regular 中的 re,但不能匹配 expression 中的 re,可以用 re(?=gular),該表達式限定了 re 右邊的位置,這個位置之後是 gular,但並不消耗 gular 這些字符。


將表達式改為 re(?=gular).,將會匹配 reg,元字符 . 匹配了 g,括號這一砣匹配了 eg 之間的位置。


(?!pattern) 負向先行斷言

代表字符串中的一個位置,緊接該位置之後的字符序列不能匹配 pattern。

例如對 "regex represents regular expression" 這個字符串,要想匹配除 regex 和 regular 之外的 re,可以用 re(?!g),該表達式限定了 re 右邊的位置,這個位置後麵不是字符 g

負向和正向的區別,就在於該位置之後的字符能否匹配括號中的表達式。

(?<=pattern) 正向後行斷言

代表字符串中的一個位置,緊接該位置之前的字符序列能夠匹配 pattern。

例如對 regex represents regular expression 這個字符串,有 4 個單詞,要想匹配單詞內部的 re,但不匹配單詞開頭的 re,可以用 (?<=\w)re,單詞內部的 re,在 re 前麵應該是一個單詞字符。

之所以叫後行斷言,是因為正則表達式引擎在匹配字符串和表達式時,是從前向後逐個掃描字符串中的字符,並判斷是否與表達式符合,當在表達式中遇到該斷言時,正則表達式引擎需要往字符串前端檢測已掃描過的字符,相對於掃描方向是向後的。

(?<!pattern) 負向後行斷言

代表字符串中的一個位置,緊接該位置之前的字符序列不能匹配 pattern。

例如對 "regex represents regular expression" 這個字符串,要想匹配單詞開頭的 re,可以用 (?<!\w)re。單詞開頭的 re,在本例中,也就是指不在單詞內部的 re,即 re 前麵不是單詞字符。當然也可以用 \bre 來匹配。

對於這 4 個斷言的理解,可以從兩個方麵入手:

  • 1、關於先行(lookahead)和後行(lookbehind):正則表達式引擎在執行字符串和表達式匹配時,會從頭到尾(從前到後)連續掃描字符串中的字符,設想有一個掃描指針指向字符邊界處並隨匹配過程移動。先行斷言,是當掃描指針位於某處時,引擎會嚐試匹配指針還未掃過的字符,先於指針到達該字符,故稱為先行。後行斷言,引擎會嚐試匹配指針已掃過的字符,後於指針到達該字符,故稱為後行。

  • 2、關於正向(positive)和負向(negative):正向就表示匹配括號中的表達式,負向表示不匹配。

對這 4 個斷言形式的記憶:

  • 1、先行和後行:後行斷言 (?<=pattern)、(?<!pattern) 中,有個小於號,同時也是箭頭,對於自左至右的文本方向,這個箭頭是指向後的,這也比較符合我們的習慣。把小於號去掉,就是先行斷言。

  • 2、正向和負向:不等於 (!=)、邏輯非 (!) 都是用 !號來表示,所以有 ! 號的形式表示不匹配、負向;將 ! 號換成 = 號,就表示匹配、正向。

我們經常用正則表達式來檢測一個字符串中包含某個子串,要表示一個字符串中不包含某個字符或某些字符也很容易,用 [^...] 形式就可以了。要表示一個字符串中不包含某個子串(由字符序列構成)呢?

[^...] 這種形式就不行了,這時就要用到(負向)先行斷言或後行斷言、或同時使用。

例如判斷一句話中包含 this,但不包含 that

包含 this 比較好辦,一句話中不包含 that,可以認為這句話中每個字符的前麵都不是 that 或每個字符的後麵都不是 that。正則表達式如下:

^((?<!that).)*this((?<!that).)*$
或 
^(.(?!that))*this(.(?!that))*$

對於 this is runoob test 這句話,兩個表達式都能夠匹配成功,而 this and that is runoob test 都匹配失敗。

在一般情況下,這兩個表達式基本上都能夠滿足要求了。考慮極端情況,如一句話以 that 開頭、以 that 結尾、thatthis 連在一起時,上述表達式就可能不勝任了。 如 runoob thatthis is the case 或者 this is the case, not that 等。

隻要靈活運用這幾個斷言,就很容易解決:

^(.(?<!that))*this(.(?<!that))*$
^(.(?<!that))*this((?!that).)*$
^((?!that).)*this(.(?<!that))*$
^((?!that).)*this((?!that).)*$

這 4 個正則表達式測試上述的幾句話,結果都能夠滿足要求。

上述 4 種斷言,括號裏的 pattern 本身是一個正則表達式。但對 2 種後行斷言有所限製,在 Perl 和 Python 中,這個表達式必須是定長(fixed length)的,即不能使用 *、+、? 等元字符,如 (?<=abc) 沒有問題,但 (?<=a*bc) 是不被支持的,特別是當表達式中含有|連接的分支時,各個分支的長度必須相同。之所以不支持變長表達式,是因為當引擎檢查後行斷言時,無法確定要回溯多少步。Java 支持 ?、{m}、{n,m} 等符號,但同樣不支持 *、+ 字符。Javascript 幹脆不支持後行斷言,不過一般來說,這不是太大的問題。

先行斷言和後行斷言某種程度上就好比使用 if 語句對匹配的字符前後做判斷驗證。

以下列出 ?=、?<=、?!、?<!= 的使用

exp1(?=exp2):查找 exp2 前麵的 exp1。

(?<=exp2)exp1:查找 exp2 後麵的 exp1。

exp1(?!exp2):查找後麵不是 exp2 的 exp1。

(?<!=exp2)exp1:查找前麵不是 exp2 的 exp1。

參考鏈接:https://blog.51cto.com/cnn237111/749047