從零寬斷言說起到用python匹配html標簽內容
版權聲明:本文為博主原創文章,轉載請附帶原文網址http://www.cnblogs.com/wbchanblog/p/7411750.html ,謝謝!
提示:本文主要是講解零寬斷言,所以閱讀本文需要有一定的正則表達式基礎。
概念
我們知道元字符“\b”、“^”、“$”匹配的是一個位置,而且這個位置需要滿足一定的條件(比如“\b”表示單詞的邊界),我們把這個條件稱為斷言或零寬度斷言。這裏有很重要的兩個信息:一是斷言實際上是某種條件;二是它不占字符寬度,只是一個位置,並不匹配任何字符。
零寬斷言一共分為正向和反向兩類,每類又分為預測先行和回顧後發兩種:
§零寬度正預測先行斷言,簡稱正向先行斷言,語法是(?=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>),看形式,跟斷言語法中的(?=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)得到目標字符串。
從零寬斷言說起到用python匹配html標簽內容