零寬度斷言


從零寬斷言說起到用python匹配html標簽內容

 

提示:本文主要是講解零寬斷言,所以閱讀本文需要有一定的正則表達式基礎。

概念

  我們知道元字符“\b”、“^”、“$”匹配的是一個位置,而且這個位置需要滿足一定的條件(比如“\b”表示單詞的邊界),我們把這個條件稱為斷言或零寬度斷言。這里有很重要的兩個信息:一是斷言實際上是某種條件;二是它不占字符寬度,只是一個位置,並不匹配任何字符。

  零寬斷言一共分為正向反向兩類,每類又分為預測先行回顧后發兩種:

  §零寬度正預測先行斷言,簡稱正向先行斷言,語法是(?=exp),它斷言此位置的后面能匹配表達式exp。

  §零寬度正回顧后發斷言,簡稱正向后發斷言,語法是(?<=exp),它斷言此位置的前面能匹配表達式exp。

  §零寬度負預測先行斷言,簡稱反向先行斷言,語法是(?!exp),它斷言此位置的后面不能匹配表達式exp。

  §零寬度負回顧后發斷言,簡稱反向后發斷言,語法是(?<!exp),它斷言此位置的前面不能匹配表達式exp。

  好了,說到這里你一定感覺雲里霧里,講道理我剛看到這官方定義也是一臉懵逼,下面就結合例子來幫助理解一下什么是斷言。做過python爬蟲的朋友一定做過提取html標簽內容的工作吧,比如有<div>hello world</div>,我們要把div標簽里面的‘hello world’提取出來,用斷言就是如下這樣:

正則表達式:(?<=<div>).*(?=</div>) 
匹配字符串:<div>hello world</div>
匹配結果: hello world

  我們結合這段表達式來看,我們前后用了(?<=<div>)(?=</div>)兩個斷言。

  先來看第一個斷言(?<=<div>),看形式,是不是跟斷言語法中的(?<=exp)一樣,沒錯,這個就是正向后發斷言,這里的exp就是<div>,它斷言此位置的前面能匹配表達式<div>,這樣說其實很不好理解,關鍵在於此位置這三個字不知道代表什么,實際上,這個此位置可以替換成目標字符串,也就是我們需要提取出來的內容,替換之后就變成了:它斷言目標字符串的前面能匹配表達式<div>,換個更形象的說法:我斷言,我所要提取的目標字符串,它前面的內容一定要匹配表達式<div>。單靠這個條件,去匹配<div>hello world</div>,可以得到結果hello world</div>

  再來看第二個斷言(?=</div>),看形式,跟斷言語法中的(?=exp)一樣,那么這個就是正向先行斷言,這里的exp就是</div>,它就代表:我斷言,我所要提取的目標字符串,它后面的內容一定要匹配表達式</div>。根據這個條件,結合上一段得到的hello world</div>,我們可以得到匹配結果hello world

  這里安利一個叫Regex Match Tracer的軟件,可以幫助我們學習正則表達式:

編寫含斷言的正則表達式思路

  根據以上所說,當我們需要提取字符串的時候,可以用斷言,就比如上述字符串<div>hello world</div>,想得到div標簽里面的內容時,我們可以按照以下思路寫正則表達式:

  首先,目標字符串是hello world,那么它可以歸納為 .* 

  其次,目標字符串前面有<div>,既然是前面有,那么根據四種斷言的含義,容易得出用正向后發斷言(?<=exp),將它放在目標字符串前面,得到(?<=<div>).*,進一步可以將div歸納為[a-zA-Z]+,從而得到(?<=<[a-zA-Z]+>).*

  最后,目標字符串后面有</div>,既然是后面有,那么根據四種斷言的含義,容易得出用正向先行斷言(?=exp),將它放在目標字符串后面,從而得到(?<=<[a-zA-Z]+>).*(?=</[a-zA-Z]+>)

  進一步的,我們發現前后兩個斷言中都有[a-zA-Z]+,可以使用分組來避免書寫重復的內容:(?<=<([a-zA-Z]+)>).*(?=</\1>),當然也可以使用命名分組,這里就不展開了。

  說到這里,我歸納出了幾句書寫斷言的口訣:

    前面有,正向后發(?<=exp),放前面;

    后面有,正向先行(?=exp),放后面;

    前面無,反向后發(?<!exp),放前面;

    后面無,反向先行(?!exp),放后面。

  請記住,這個前面和后面是針對目標字符串,也就是你要提取出來的字符串而言的。

 

Python中斷言的應用

  前面說了這么多, 都是就正則表達式本身而言的,我們知道不同編程語言都有自己對正則表達式的擴展,python也不例外。來看下面一段代碼:

import re
pattern = re.compile(r'(?<=<([a-zA-Z]+>)).*(?=</\1>)')
s = '<html>hello world</html>'
ret = re.search(pattern, s)
print(ret.group())

#得到結果:
#Traceback (most recent call last):
#   raise error("look-behind requires fixed-width pattern")
#sre_constants.error: look-behind requires fixed-width pattern

  我們看到python解釋器報錯了,怎么回事?別急,接着看:

import re
pattern = re.compile(r'(?<=<([a-zA-Z]+>)).*')
s = '<html>hello world</html>'
ret = re.search(pattern, s)
print(ret.group())

#得到結果:
#Traceback (most recent call last):
#   raise error("look-behind requires fixed-width pattern")
#sre_constants.error: look-behind requires fixed-width pattern
import re
pattern = re.compile(r'.*(?=</[a-zA-Z]+>)')
s = '<html>hello world</html>'
ret = re.search(pattern, s)
print(ret.group())

#得到結果:
#<html>hello world

  看明白了嗎?將上面第二第三段分別跟第一段代碼對比,我們看到第二段相對於第一段的正則表達式去掉了正向先行斷言,仍然報錯;第三段相對於第一段的正則表達式去掉了正向后發斷言(當然用到分組的地方已經手動補全了),卻匹配到了結果。再結合錯誤信息“sre_constants.error: look-behind requires fixed-width pattern”,我們可以得出python的re模塊並不支持變長的后發斷言,只支持定長的后發斷言。

  那咋辦?難不成就不能提取html標簽里的內容了?別急,請看下面代碼:

import re
pattern = re.compile(r'<([a-zA-Z]+)>(.*)</\1>')
s = '<html>hello world</html>'
ret = re.search(pattern, s)
print('re.group()→', ret.group())
print('re.group(2)→', ret.group(2))

#運行結果
#re.group()→ <html>hello world</html>
#re.group(2)→ hello world

  我們可以用分組來提取特定的字符串,上面代碼給了.*增加了一個分組,按從左到右是第二個分組,這樣我們可以在匹配結果中用.group(2)得到目標字符串。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM