1. 程式人生 > >從零寬斷言說起到用python匹配html標簽內容

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

idt inpu 重要 python 感覺 出了 5% htm 轉載

版權聲明:本文為博主原創文章,轉載請附帶原文網址http://www.cnblogs.com/wbchanblog/p/7411750.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)得到目標字符串。

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