官术网_书友最值得收藏!

  • 正則指引
  • 余晟著
  • 2626字
  • 2019-01-09 16:29:12

第2章 量詞

2.1 一般形式

根據(jù)上一章的介紹,可以用字符組[0-9]或者\d 匹配單個數(shù)字字符。現(xiàn)在用正則表達(dá)式來驗(yàn)證更復(fù)雜的字符串,比如大陸地區(qū)的郵政編碼。

粗略來看,郵政編碼并沒有特殊的規(guī)定,只是6位數(shù)字構(gòu)成的字符串,比如201203100858,所以用正則表達(dá)式來表示就是\d\d\d\d\d\d,如例2-1所示,只有同時滿足“長度是6個字符”和“每個字符都是數(shù)字”兩個條件,匹配才成功(同樣,這里不能忽略^和$)。

例2-1匹配郵政編碼

      re.search(r"^\d\d\d\d\d\d$", "100859") != None        #  => True
      re.search(r"^\d\d\d\d\d\d$", "201203") != None        #  => True
      re.search(r"^\d\d\d\d\d\d$", "20A203") != None        #  => False
      re.search(r"^\d\d\d\d\d\d$", "20103") != None         #  => False
      re.search(r"^\d\d\d\d\d\d$", "2012036") != None       #  => False

雖然這不難理解,但\d重復(fù)了6次,讀寫都不方便。為此,正則表達(dá)式提供了量詞(quantifier),比如上面匹配郵政編碼的表達(dá)式,就可以如例2-2那樣,簡寫為\d{6},它使用阿拉伯?dāng)?shù)字,更簡潔也更直觀。

例2-2使用量詞簡化字符組

      re.search(r"^\d{6}$", "100859") != None      #  => True
      re.search(r"^\d{6}$", "201203") != None      #  => True
      re.search(r"^\d{6}$", "20A203") != None      #  => False
      re.search(r"^\d{6}$", "20103") != None       #  => False
      re.search(r"^\d{6}$", "2012036") != None     #  => False

量詞還可以表示不確定的長度,其通用形式是{m,n},其中mn 是兩個數(shù)字(有些人習(xí)慣在代碼中的逗號之后添加空格,這樣更好看,但是量詞中的逗號之后絕不能有空格),它限定之前的元素在上一章提到,字符組是正則表達(dá)式的基本“結(jié)構(gòu)”之一,而此處提到之前的“元素”,在此做一點(diǎn)解釋。在本書中,“結(jié)構(gòu)”一般指的是正則表達(dá)式所提供功能的記法。比如字符組就是一種結(jié)構(gòu),下一章要提到的括號也是一種結(jié)構(gòu);而“元素”指的是具體的正則表達(dá)式中的某個部分,比如某個具體表達(dá)式中的字符組[a-z],可以算作一個元素,“元素”也叫“子表達(dá)式”(sub-expression)。能夠出現(xiàn)的次數(shù),m是下限,n是上限(均為閉區(qū)間)。比如\d{4,6},就表示這個數(shù)字字符串的長度最短是4個字符(“單個數(shù)字字符”至少出現(xiàn)4次),最長是6個字符。

如果不確定長度的上限,也可以省略,只指定下限,寫成\d{m,},比如\d{4,}表示“數(shù)字字符串的長度必須在4個字符以上”。

量詞限定的出現(xiàn)次數(shù)一般都有明確下限,如果沒有,則默認(rèn)為0。有一些語言(比如Ruby)支持{,n}的記法,這時候并不是“不確定長度的下限”,而是省略了“下限為0”的情況,比如\d{,6}表示“數(shù)字字符串最多可以有6個字符”。不過,這種用法并不是所有語言中都通用的,比如Java就不支持這種寫法,所以必須寫明{0,n}。我推薦的做法是:最好使用{0,n}的記法,因?yàn)樗菑V泛支持的。表2-1集中說明了這幾種形式的量詞,例2-3展示了它們的使用。

表2-1 量詞的一般形式

例2-3表示不確定長度的量詞

      re.search(r"^\d{4,6}$", "123") != None       #  => False
      re.search(r"^\d{4,6}$", "1234") != None      #  => True
      re.search(r"^\d{4,6}$", "123456") != None    #  => True
      re.search(r"^\d{4,6}$", "1234567") != None   #  => False
      re.search(r"^\d{4,}$", "123") != None        #  => False
      re.search(r"^\d{4,}$", "1234") != None       #  => True
      re.search(r"^\d{4,}$", "123456") != None     #  => True
      re.search(r"^\d{0,6}$", "12345") != None     #  => True
      re.search(r"^\d{0,6}$", "123456") != None    #  => True
      re.search(r"^\d{0,6}$", "1234567") != None   #  => False

2.2 常用量詞

{m,n}是通用形式的量詞,正則表達(dá)式還有三個常用量詞,分別是+?*。它們的形態(tài)雖然不同于{m,n},功能卻是相同的(也可以把它們理解為“量詞簡記法”),具體說明見表2-2。

表2-2 常用量詞

在實(shí)際應(yīng)用中,在很多情況只需要表示這三種意思,所以常用量詞的使用頻率要高于{m,n},下面分別說明。

大家都知道,美國英語和英國英語有些詞的寫法是不一樣的,比如travelertraveller,如果希望“通吃”travelertraveller,就要求第2個l是“至多出現(xiàn)1次,也可能不出現(xiàn)”的,正好使用?量詞:travell?er,如例2-4所示。

例2-4量詞?的應(yīng)用

      re.search(r"^travell?er$", "traveler") != None    #  => True
      re.search(r"^travell?er$", "traveller") != None   #  => True

其實(shí)這樣的情況還有很多,比如favorfavourcolorcolour。此外還有很多其他應(yīng)用場合,比如httphttps,雖然是兩個概念,但都是協(xié)議名,可以用https?匹配;再比如表示價(jià)格的字符串,有可能是100也有可能是¥100,可以用¥?100匹配實(shí)際上,這個問題比較復(fù)雜,因?yàn)椋げ⒉皇且粋€ASCII字符,所以¥?可能會產(chǎn)生問題,具體情況請參考第7章。

量詞也廣泛應(yīng)用于解析HTML代碼。HTML是一種“標(biāo)簽語言”,它包含各種各樣的tag(標(biāo)簽),比如<head><img><table>等,這些tag的名字各異,形式卻相同:從<開始,到>結(jié)束,在<>之間有若干字符,“若干”的意思是長度不確定,但不能為0(<>并不是合法的tag),也不能是>字符如果你對HTML代碼比較了解,可能會有疑問,假如tag內(nèi)部出現(xiàn)>符號,怎么辦?這種情況確實(shí)存在,比如<input name=txt value='>'>。以目前已經(jīng)講解的知識還無法解決這個問題,不過下一章就會給出它的解法。。如果要用一個正則表達(dá)式匹配所有的tag,需要用<匹配開頭的<,用>匹配結(jié)尾的>,用[^>]+匹配中間的“若干字符”,所以整個正則表達(dá)式就是<[^>]+>,程序如例2-5所示。

例2-5量詞+的應(yīng)用

      re.search(r"^<[^>]+>$", "<bold>") != None        #  => True
      re.search(r"^<[^>]+>$", "</table>") != None      #  => True
      re.search(r"^<[^>]+>$", "<>") != None            #  => False

類似的,也可以使用正則表達(dá)式匹配雙引號字符串。不同的是,雙引號字符串的兩個雙引號之間可以沒有任何字符,""也是一個完全合法的雙引號字符串,應(yīng)該使用量詞*,于是整個正則表達(dá)式就成了"[^"]*",程序見例2-6。

例2-6量詞*的應(yīng)用

      re.search(r"^\"[^\"]*\"$", "\"some\"") != None    #  => True
      re.search(r"^\"[^\"]*\"$", "\"\"") != None       #  => True

注:字符串之中表示雙引號需要轉(zhuǎn)義寫成\",這并不是正則表達(dá)式中的規(guī)定,而是為字符串轉(zhuǎn)義考慮。

量詞的使用有很多學(xué)問,不妨多看幾個tag匹配的例子:tag可以粗略分為open tag和close tag,比如<head>就是open tag,而</html>就是close tag;另外還有一類標(biāo)簽是self-closing tag,比如<br/>。現(xiàn)在來看分別匹配這三類tag的正則表達(dá)式。

open tag的特點(diǎn)是以<開頭,然后是“若干字符”(但不能以/開頭),最后是>,所以對應(yīng)的正則表達(dá)式是<[^/][^>]*>;注意:因?yàn)?span id="gicgelm" class="bgcolor_106">[^/]必須匹配一個字符,所以“若干字符”中其他部分必須寫成[^>]*,否則它無法匹配名字為單個字符的標(biāo)簽,比如<b>

close tag的特點(diǎn)是以<開頭,之后是/字符,然后是“若干字符(但不能以/開頭)”,最后是>,所以對應(yīng)的正則表達(dá)式是</[^>]+>

self-closing tag的特點(diǎn)是以<開頭,中間是“若干字符”,最后是/>,所以對應(yīng)的正則表達(dá)式是<[^>]+/>。注意:這里不是<[^>/]+/>,排除型字符組只排除>,而不排除/,因?yàn)橐_認(rèn)的只是在結(jié)尾的>之前出現(xiàn)/,如果寫成<[^>/]+/>,則要求tag內(nèi)部不能出現(xiàn)/,就無法匹配<img src="http://somehost/picture" />這類的tag了。

表2-3列出了匹配幾類tag的表達(dá)式。

表2-3 各類tag的匹配

對比表格中“匹配所有tag的表達(dá)式”和“匹配分類tag的表達(dá)式”,可以發(fā)現(xiàn)它們的模式是相近的,只是細(xì)節(jié)上有差異。也就是說,通過變換字符組和量詞,可以準(zhǔn)確控制正則表達(dá)式能匹配的字符串的范圍,達(dá)到不同的目的。這其實(shí)是使用正則表達(dá)式時的一條根本規(guī)律:使用合適的結(jié)構(gòu)(包括字符組和量詞),精確表達(dá)自己的意圖,界定能匹配的文本。

再仔細(xì)觀察,你或許會發(fā)現(xiàn),匹配open tag的表達(dá)式,也可以匹配self-closing tag:<[^/][^>]*>能夠匹配<br/>,因?yàn)?span id="2smbom9" class="bgcolor_106">[^>]*并不排除對/的匹配。那么將表達(dá)式改為<[^/][^>]*[^/]>,就保證匹配的open tag不會以/>結(jié)尾了。

不過這會產(chǎn)生新的問題:<[^/][^>]*[^/]>能匹配的tag,在<>之間出現(xiàn)了兩個[^/],上一章已經(jīng)講過,排除型字符組表示“在當(dāng)前位置,匹配一個沒有列出的字符”,所以tag里的字符串必須至少包含兩個字符,這樣就無法匹配<u>了。

仔細(xì)想想,真正要表達(dá)的意思是,在tag內(nèi)部的字符串不能以/開頭,也不能以/結(jié)尾,如果這個字符串只包含一個字符,那么它既是開頭,又是結(jié)尾,使用兩個排除型字符組顯然是不合適的,看起來沒辦法解決了。實(shí)際上,只是現(xiàn)有的知識還不足夠解決這個問題而已,在第68 頁有這個問題的詳細(xì)解法。

2.3 數(shù)據(jù)提取

正則表達(dá)式的功能很多,除去之前介紹的驗(yàn)證(字符串能否由正則表達(dá)式匹配),還可以從某個字符串中提取出某個字符串能匹配的所有文本。

上一章提到,re.search()如果匹配成功,返回一個 MatchObject 對象。這個對象包含了匹配的信息,比如表達(dá)式匹配的結(jié)果,可以像例2-7那樣,通過調(diào)用MatchObject.group(0)來獲得。這個方法以后詳細(xì)介紹,現(xiàn)在只需要了解一點(diǎn):調(diào)用它可以得到表達(dá)式匹配的文本。

例2-7通過MatchObject獲得匹配的文本

      #注意這里使用鏈?zhǔn)骄幊?
      print re.search(r"\d{6}", "ab123456cd").group(0)
      123456
      print re.search(r"^<[^>]+>$", "<bold>").group(0)
      <bold>

這里再介紹一個方法:re.findall(pattern,string)。其中pattern是正則表達(dá)式,string是字符串。這個方法會返回一個數(shù)組,其中的元素是在string中依次尋找pattern能匹配的文本。

以郵政編碼的匹配為例,假設(shè)某個字符串中包含兩個郵政編碼:zipcode1:201203, zipcode2:100859,仍然使用之前匹配郵政編碼的正則表達(dá)式\d{6},調(diào)用 re.findall()可以將這兩個郵政編碼提取出來,如例2-8。注意,這次要去掉表達(dá)式首尾的^$,因?yàn)橐褂谜齽t表達(dá)式在字符串中尋找匹配,而不是驗(yàn)證整個字符串能否由正則表達(dá)式匹配。

例2-8使用re.findall()提取數(shù)據(jù)

      print re.findall(r"\d{6}", "zipcode1:201203, zipcode2:100859")
      ['201203', '100859']
      #也可以逐個輸出
      for zipcode in re.findall(r"\d{6}", "zipcode1:201203, zipcode2:100859"):
          print zipcode
      201203
      100859

借助之前的匹配各種tag的正則表達(dá)式,還可以通過re.findall()將某個HTML頁面中所有的tag提取出來,下面以Yahoo首頁為例。

首先要讀入http://www.yahoo.com/的HTML源代碼,在Python中先獲得URL對應(yīng)頁面的源代碼,保存到htmlSource變量中,然后針對匹配各類tag的正則表達(dá)式,分別調(diào)用re.findall(),獲得各類tag的列表(因?yàn)檫@個頁面中包含的tag太多,每類tag只顯示前3個)。

因?yàn)檫@段程序的輸出很多,在交互式界面下不方便操作和觀察,建議將這些代碼單獨(dú)保存為一個.py文件,比如findtags.py,然后輸入python findtags.py運(yùn)行。如果輸入python沒有結(jié)果(一般在Windows下會出現(xiàn)這種情況),需要準(zhǔn)確設(shè)定 PATH變量,比如 d:\Python\python。之后,就會看到例2-9顯示的結(jié)果。

例2-9使用re.findall()提取tag

      #導(dǎo)入需要的package
      import urllib
      import re
      #讀入HTML源代碼
      sock = urllib.urlopen("http://yahoo.org/")
      htmlSource = sock.read()
      sock.close()
      #匹配,輸出結(jié)果([0:3]表示取前3個)
      print "open tags:"
      print re.findall(r"<[^/>][^>]*[^/>]>", htmlSource)[0:3]
      print "close tags:"
      print re.findall(r"</[^>]+>", htmlSource) [0:3]
      print "self-closing tags:"
      print re.findall(r"<[^>/]+/>", htmlSource) [0:3]
      open tags:
      ['<!DOCTYPE   html>',   '<html   lang="en-US"   class="y-fp-bg   y-fp-pg-grad
  bkt701">', '<!-- m2 template 0-->']
      close tags:
      ['</title>', '</script>', '</script>']
      self-closing tags:
      ['<br/>', '<br/>', '<br/>']

2.4 點(diǎn)號

上一章講到了各種字符組,與它相關(guān)的還有一個特殊的元字符:點(diǎn)號.。一般文檔都說,點(diǎn)號可以匹配“任意字符”,點(diǎn)號確實(shí)可以匹配“任意字符”,常見的數(shù)字、字母、各種符號都可以匹配,如例2-10所示。

例2-10點(diǎn)號.的匹配

      re.search(r"^.$", "a") != None      #  => True
      re.search(r"^.$", "0") != None      #  => True
      re.search(r"^.$", "*") != None      #  => True

有一個字符不能由點(diǎn)號匹配,就是換行符\n。這個字符平時看不見,卻存在,而且在處理時并不能忽略(下一章會給出具體的例子)。

如果非要匹配“任意字符”,有兩種辦法:可以指定使用單行匹配模式,在這種模式下,點(diǎn)號可以匹配換行符(?84);或者使用上一章的介紹“自制”通配字符組[\s\S](也可以使用[\d\D][\w\W]),正好涵蓋了所有字符。例2-11清楚地說明,這兩個辦法都可以匹配換行符。

例2-11換行符的匹配

      re.search(r"^.$", "\n") != None         #  => False
      #單行模式
      re.search(r"(?s)^.$", "\n") != None      #  => True
      #自制“通配字符組”
      re.search(r"^[\s\S]$", "\n") != None     #  => True

2.5 濫用點(diǎn)號的問題

因?yàn)辄c(diǎn)號能匹配幾乎所有的字符,所以實(shí)際應(yīng)用中許多人圖省事,隨意使用.*.+,結(jié)果卻事與愿違,下面以雙引號字符串為例來說明。

之前我們使用表達(dá)式"[^"]*"匹配雙引號字符串,而“圖省事”的做法是".*"。通常這么用是沒有問題的,但也可能有意外,例2-12就說明了一種如此。

例2-12 “圖省事”的意外結(jié)果

      #字符串的值是"quoted string"
      print re.search(r"\".*\"", "\"quoted string\"").group(0)
      "quoted string"
      #字符串的值是string" and another"
      print re.search(r"\".*\"", "\"quoted string\" and another\"").group(0)
      "quoted string" and another"

".*"匹配雙引號字符串,不但可以匹配正常的雙引號字符串"quoted string",還可以匹配格式錯誤的字符串"quoted string" and another"。這是為什么呢?

這個問題比較復(fù)雜,現(xiàn)在只簡要介紹,以說明圖省事導(dǎo)致錯誤的原因,更深入的原因涉及正則表達(dá)式的匹配原理,在第8章詳細(xì)介紹。

在正則表達(dá)式".*"中,點(diǎn)號.可以匹配任何字符,*表示可以匹配的字符串長度沒有限制,所以.*在匹配過程結(jié)束以前,每遇到一個字符(除去無法匹配的\n),.*都可以匹配,但是到底是匹配這個字符,還是忽略它,將其交給之后的"來匹配呢?

答案是,具體選擇取決于所使用的量詞。在正則表達(dá)式中的量詞分為幾類,之前介紹的量詞都可以歸到一類,叫做匹配優(yōu)先量詞(greedy quantifier,也有人翻譯為貪婪量詞許多文檔都翻譯為“貪婪量詞”,單獨(dú)來看這是沒問題的,但考慮到正則表達(dá)式中還有其他類型的量詞,其英文名字的形式較為統(tǒng)一,所以我在翻譯《精通正則表達(dá)式》時采用了“匹配優(yōu)先/忽略優(yōu)先/占有優(yōu)先”的名字,也未見讀者反對,故此處延用此譯法。)。匹配優(yōu)先量詞,顧名思義,就是在拿不準(zhǔn)是否要匹配的時候,優(yōu)先嘗試匹配,并且記下這個狀態(tài),以備將來“反悔”。

來看表達(dá)式".*"對字符串"quoted string"的匹配過程。

一開始,"匹配",然后輪到字符q.*可以匹配它,也可以不匹配,因?yàn)槭褂昧似ヅ鋬?yōu)先量詞,所以.*先匹配q,并且記錄下這個狀態(tài)【q也可能是.*不應(yīng)該匹配的】;

接下來是字符 u.*可以匹配它,也可以不匹配,因?yàn)槭褂昧似ヅ鋬?yōu)先量詞,所以.*先匹配u,并且記錄下這個狀態(tài)【u也可能是.*不應(yīng)該匹配的】;

……

現(xiàn)在輪到字符 g.*可以匹配它,也可以不匹配,因?yàn)槭褂昧似ヅ鋬?yōu)先量詞,所以.*先匹配g,并且記錄下這個狀態(tài)【g也可能是.*不應(yīng)該匹配的】;

最后是末尾的".*可以匹配它,也可以不匹配,因?yàn)槭褂昧似ヅ鋬?yōu)先量詞,所以.*先匹配",并且記錄下這個狀態(tài)【"也可能是.*不應(yīng)該匹配的】。

這時候,字符串之后已經(jīng)沒有字符了,但正則表達(dá)式中還有"沒有匹配,所以只能查詢之前保存?zhèn)溆玫臓顟B(tài),看看能不能退回幾步,照顧"的匹配。查詢到最近保存的狀態(tài)是:【"也可能是.*不應(yīng)該匹配的】。于是讓.*“反悔”對"的匹配,把"交給",測試發(fā)現(xiàn)正好能匹配,所以整個匹配宣告成功。這個“反悔”的過程,專業(yè)術(shù)語叫做回溯(backtracking),具體的過程如圖2-1所示。

圖2-1 表達(dá)式".*"對字符串"quoted string"的匹配過程

如果把字符串換成"quoted string" and another".*會首先匹配第一個雙引號之后的所有字符,再進(jìn)行回溯,表達(dá)式中的"匹配了字符串結(jié)尾的字符",整個匹配宣告完成,過程如圖2-2所示。

圖2-2 表達(dá)式".*"的匹配過程

如果要準(zhǔn)確匹配雙引號字符串,就不能圖省事使用".*",而要使用"[^"]*",過程如圖2-3所示。

圖2-3 表達(dá)式"[^"]*"的匹配過程

2.6 忽略優(yōu)先量詞

也有些時候,確實(shí)需要用到.*(或者[\s\S]*),比如匹配HTML代碼中的JavaScript示例就是如此。

      <script type="text/javascript"></script>

匹配的模式仍然是:匹配open tag和close tag,以及它們之間的內(nèi)容。open tag是<script type="text/javascript">,close tag是</script>,這兩段的內(nèi)容是固定的,非常容易寫出對應(yīng)的表達(dá)式,但之間的內(nèi)容怎么匹配呢?在JavaScript代碼中,各種字符都可能出現(xiàn),所以不能用排除型字符組,只能用.*。比如,用一個正則表達(dá)式匹配下面這段HTML源代碼:

      <script type="text/javascript">
      alert("some punctuation <>/");
      </script>

開頭和結(jié)尾的tag都容易匹配,中間的代碼要比較麻煩,因?yàn)辄c(diǎn)號.不能匹配換行符,所以必須使用[\s\S](或者[\d\D][\w\W])。

      <script type="text/javascript">[\s\S]*</script>

這個表達(dá)式確實(shí)可以匹配上面的JavaScript代碼。但是如果遇到更復(fù)雜的情況就會出錯,比如針對下面這段HTML代碼,程序運(yùn)行結(jié)果如例2-13。

      <script type="text/javascript">
      alert("1");
      </script>
      <br />
      <script type="text/javascript">
      alert("2");
      </script>

例2-13匹配JavaScript代碼的錯誤

      #假設(shè)上面的JavaScript代碼保存在變量htmlSourcejsRegex = r"<script type=\"text/javascript\">[\s\S]*</script>"
      print re.search(jsRegex, htmlSource).group(0)
      <script type="text/javascript">
      alert("1");
      </script>
      <br />
      <script type="text/javascript">
      alert("2");
      </script>

<script type="text/javascript">[\s\S]*</script>來匹配,會一次性匹配兩段JavaScript代碼,甚至包含之間的非JavaScript代碼。

按照匹配原理,[\s\S]*先匹配所有的文本,回溯時交還最后的</script>,整個表達(dá)式的匹配就成功了,邏輯就是如此,無可改進(jìn)。而且,這個問題也不能模仿之前雙引號字符串匹配,用[^"]*匹配<script…></script>之間的代碼,因?yàn)榕懦妥址M只能排除單個字符,[^</script>]不能表示“不是</script>的字符串”。

換個角度來看,通過改變[\s\S]*的匹配策略解決問題:在不確定是否要匹配的場合,先嘗試不匹配的選擇,測試正則表達(dá)式中后面的元素,如果失敗,再退回來嘗試.*匹配,如此就沒問題了。

循著這個思路,正則表達(dá)式中還提供了忽略優(yōu)先量詞(lazy quantifier或reluctant quantifier,也有人翻譯為懶惰量詞),如果不確定是否要匹配,忽略優(yōu)先量詞會選擇“不匹配”的狀態(tài),再嘗試表達(dá)式中之后的元素,如果嘗試失敗,再回溯,選擇之前保存的“匹配”的狀態(tài)。

[\s\S]*來說,把*改為*?就是使用了忽略優(yōu)先量詞,*?限定的元素出現(xiàn)次數(shù)范圍與*完全一樣,都表示“可能出現(xiàn),也可能不出現(xiàn),出現(xiàn)次數(shù)沒有上限”。區(qū)別在于,在實(shí)際匹配過程中,遇到[\s\S]能匹配的字符,先嘗試“忽略”,如果后面的元素(具體到這個表達(dá)式中,是</script>)不能匹配,再嘗試“匹配”,這樣就保證了結(jié)果的正確性,代碼見例2-14。

例2-14準(zhǔn)確匹配JavaScript代碼

      #仍然假設(shè)JavaScript代碼保存在變量htmlSourcejsRegex = r"<script type=\"text/javascript\">[\s\S]*?</script>"
      print re.search(jsRegex, htmlSource) .group(0)
      <script type="text/javascript">
      alert("1");
      </script>
      #甚至也可以逐次提取出兩段JavaScript代碼
      jsRegex = r"<script type=\"text/javascript\">[\s\S]*?</script>"
      for jsCode in re.findall(jsRegex, htmlSource) :
       print jsCode + "\n"
      <script type="text/javascript">
      alert("1");
      </script>
      <script type="text/javascript">
      alert("2");
      </script>

從表2-4可以看到,匹配優(yōu)先量詞與忽略優(yōu)先量詞逐一對應(yīng),只是在對應(yīng)的匹配優(yōu)先量詞之后添加?,兩者限定的元素能出現(xiàn)的次數(shù)也一樣,遇到不能匹配的情況同樣需要回溯;唯一的區(qū)別在于,忽略優(yōu)先量詞會優(yōu)先選擇“忽略”,而匹配優(yōu)先量詞會優(yōu)先選擇“匹配”。

表2-4 匹配優(yōu)先量詞與忽略優(yōu)先量詞

忽略優(yōu)先量詞還可以完成許多其他功能,典型的例子就是提取代碼中的C語言注釋。

C語言的注釋有兩種:一種是在行末,以//開頭;另一種可以跨多行,以/*開頭,以*/結(jié)束。第一種注釋很好匹配,使用//.*即可,因?yàn)辄c(diǎn)號.不能匹配換行符,所以//.*匹配的就是從//直到行末的文本,注意這里使用了量詞*,因?yàn)?span id="w9elk7l" class="bgcolor_105">//可能就是該行最后兩個字符;第二種注釋稍微復(fù)雜一點(diǎn),因?yàn)?span id="sk7rvla" class="bgcolor_105">/*…*/的注釋和JavaScript一樣,可能分成許多段,所以必須用到忽略優(yōu)先量詞;同時因?yàn)樽⑨尶赡軝M跨多行,所以必須使用[\s\S]。因此,整個表達(dá)式就是/\*[\s\S]*?\*/(別忘了*的轉(zhuǎn)義)。

另一個典型的例子是提取出HTML代碼中的超鏈接。常見的超鏈接形似<a href="http://somehost/somepath">text</a>。它以<a開頭,以</a>結(jié)束,href屬性是超鏈接的地址。我們無法預(yù)先判斷<a></a>之間到底會出現(xiàn)哪些字符,不會出現(xiàn)哪些字符,只知道其中的內(nèi)容一直到</a>結(jié)束根據(jù)HTML規(guī)范,<a>這個tag可用來表示超鏈接,也可以用作書簽,或兼作兩種用途,考慮到書簽的情況很少見,這里沒有做特殊處理。,程序代碼見例2-15。

例2-15提取網(wǎng)頁中所有的超鏈接tag

      #仍然獲得yahoo網(wǎng)站的源代碼,存放在htmlSourcefor hyperlink in re.findall(r"<a\s[\s\S]+?</a>", htmlSource):
       print hyperlink
      #更多結(jié)果未列出
      <a >Web</a>
      <a >Images</a>
      <a >Video</a>

值得注意的是,在這個表達(dá)式中的<a之后并沒有使用普通空格,而是使用字符組簡記法\s。HTML語法并沒有規(guī)定此處的空白只能使用空格字符,也沒有規(guī)定必須使用一個空白字符,所以我們用\s保證“至少出現(xiàn)一個空白字符”(但是不能沒有這個空白字符,否則就不能保證匹配tag name是a)。

之 前 匹 配JavaScript的 表 達(dá) 式 是<script language="text/javascript">[\s\S]*?</script>,它能應(yīng)對的情況實(shí)在太少了:在<script之后可能不是空格,而是空白字符;再之后可能是type="text/javascript",也可能是type="application/javascript",也可能用language取代type(實(shí)際上language是以前的寫法,現(xiàn)在大都用type),甚至可能沒有屬性,直接是<script>嚴(yán)格說起來,如果只出現(xiàn)<script>,無法保證這里出現(xiàn)的就是JavaScript代碼,也可能是VBScript代碼,但考慮到真實(shí)世界中的情況,基本可以認(rèn)為<script標(biāo)識的“就是”JavaScript代碼,所以這里不作區(qū)分。

所以必須改造這個表達(dá)式,將條件放寬:在script之后,可能出現(xiàn)空白字符,也可能直接是>,這部分可以用一個字符組[\s>]來匹配,之后的內(nèi)容統(tǒng)一用[\s\S]+?匹配,忽略優(yōu)先量詞保證了匹配進(jìn)行到到最近的</script>為止。最終得到的表達(dá)式就是<script[\s>] [\s\S]+?</script>

對這個表達(dá)式稍加改造,就可以寫出匹配類似tag的表達(dá)式。在解析頁面時,常見的需求是提取表格中各行、各單元(cell)的內(nèi)容。表格的tag是<tag>,行的tag是<tr>,單元的tag是<td>,所以,它們可以分別用下面的表達(dá)式匹配,請注意其中的[\s>],它兼顧了可能存在的其他屬性(比如<table border="1">),同時排除了可能的錯誤(比如<tablet>)。

      匹配table                  <table[\s>][\s\S]+?</table>
      匹配tr                     <tr[\s>][\s\S]+?</tr>
      匹配td                     <td[\s>][\s\S]+?</td>

在實(shí)際的HTML代碼中,tabletrtd這三個元素經(jīng)常是嵌套的,它們之間存在著包含關(guān)系。但是,僅僅使用正則表達(dá)式匹配,并不能得到“某個table包含哪些tr”、“某個td屬于哪個tr”這種信息。此時需要像例2-16的那樣,用程序整理出來。

例2-16用正則表達(dá)式解析表格

      # 這里用到了Python中的三重引號字符串,以便字符串跨越多行,細(xì)節(jié)可參考第14htmlSource = """<table>
      <tr><td>1-1</td></tr>
      <tr><td>2-1</td><td>2-2</td></tr>
      </table>"""
      for table in re.findall(r"<table[\s>][\s\S]+?</table>", htmlSource):
       for tr in re.findall(r"<tr[\s>][\s\S]+?</tr>", table):
            for td in re.findall(r"<td[\s>][\s\S]+?</td>", tr):
                print td,
            #輸出一個換行符,以便顯示不同的行
            print ""
      <td>1-1</td>
      <td>2-1</td> <td>2-2</td>

注:因?yàn)閠ag是不區(qū)分大小寫的,所以如果還希望匹配大寫的情況,則必須使用字符組,table 寫成[tT][aA][bB][lL][eE]tr寫成[tT][rR],td寫成[tT][dD]

這個例子說明,正則表達(dá)式只能進(jìn)行純粹的文本處理,單純依靠它不能整理出層次結(jié)構(gòu);如果希望解析文本的同時構(gòu)建層次結(jié)構(gòu)信息,則必須將正則表達(dá)式配合程序代碼一起使用。

回過頭想想雙引號字符串的匹配,之前使用的正則表達(dá)式是"[^"]*",其實(shí)也可以使用忽略優(yōu)先量詞解決".*?"(如果雙引號字符串中包含換行符,則使用"[\s\S]*?")。兩種辦法相比,哪個更好呢?

一般來說,"[^"]*"更好。首先,[^"]本身能夠匹配換行符,涵蓋了點(diǎn)號.可能無法應(yīng)付的情況,出于習(xí)慣,很多人更愿意使用點(diǎn)號.而不是[\s\S];其次,匹配優(yōu)先量詞只需要考慮自己限定的元素能否匹配即可,而忽略優(yōu)先量詞必須兼顧它所限定的元素與之后的元素,效率自然大大降低,如果字符串很長,兩者的速度可能有明顯的差異。

而且,有些情況下確實(shí)必須用到匹配優(yōu)先量詞,比如文件名的解析就是如此。UNIX/Linux下的文件名類似這樣/usr/local/bin/python,它包含兩個部分:路徑是/usr/local/bin/;真正的文件名是python。為了在/usr/local/bin/python中解析出兩個部分,使用匹配優(yōu)先量詞是非常方便的。從字符串的起始位置開始,用.*/匹配路徑,根據(jù)之前介紹的知識,它會回溯到最后(最右)的斜線字符/,也就是文件名之前;在字符串的結(jié)尾部分,[^/]*能匹配的就是真正的文件名。前一章介紹過^$,它們分別表示“定位到字符串的開頭”和“定位到字符串的結(jié)尾”,所以應(yīng)該把^加在匹配路徑的表達(dá)式之前,得到^.*/,而把$加在匹配真正文件名的表達(dá)式之后,得到[^/]*$,代碼見例2-17。

例2-17用正則表達(dá)式拆解Linux/UNIX的路徑

      print re.search(r"^.*/", "/usr/local/bin/python").group(0)
      /usr/local/bin
      print re.search(r"[^/]*$", "/usr/local/bin/python").group(0)
      python

Windows下的路徑分隔符是\,比如C:\Program Files\Python 2.7.1\python.exe,所以在正則表達(dá)式中,應(yīng)該把斜線字符/換成反斜線字符\。因?yàn)樵谡齽t表達(dá)式中反斜線字符\是用來轉(zhuǎn)義其他字符的,為了表示反斜線字符本身,必須連寫兩個反斜線,所以兩個表達(dá)式分別改為^.*\\[^\\]*$,代碼見例2-18。

例2-18用正則表達(dá)式拆解Windows的路徑

      #反斜線\必須轉(zhuǎn)義寫成\\
      print re.search(r"^.*\\", "C:\\Program Files\\Python 2.7.1\\python.exe").group(0)
      C:\Program Files\Python 2.7.1\
      print re.search(r"[^\\]*$", "C:\\Program Files\\Python 2.7.1\\python.exe").group(0)
      python.exe

2.7 轉(zhuǎn)義

前面講解了匹配優(yōu)先量詞和忽略優(yōu)先量詞,現(xiàn)在介紹量詞的轉(zhuǎn)義Java等語言還支持“占有優(yōu)先量詞(possessive quantifier)”,但這種量詞較復(fù)雜,使用也不多,所以本書中不介紹占有優(yōu)先量詞。

在正則表達(dá)式中,*+?等作為量詞的字符具有特殊意義,但有些情況下只希望表示這些字符本身,此時就必須使用轉(zhuǎn)義,也就是在它們之前添加反斜線\。

對常用量詞所使用的字符+*?來說,如果希望表示這三個字符本身,直接添加反斜線,變?yōu)?span id="fyhvzfp" class="bgcolor_105">\+、\*\?即可。但是在一般形式的量詞{m,n}中,雖然具有特殊含義的字符不止一個,轉(zhuǎn)義時卻只需要給第一個{添加反斜線即可,也就是說,如果希望匹配字符串{m,n},正則表達(dá)式必須寫成\{m,n}

另外值得一提的是忽略優(yōu)先量詞的轉(zhuǎn)義,雖然忽略優(yōu)先量詞也包含不只一個字符,但是在轉(zhuǎn)義時卻不像一般形式的量詞那樣,只轉(zhuǎn)義第一個字符即可,而需要將兩個量詞全部轉(zhuǎn)義。舉例來說,如果要匹配字符串*?,正則表達(dá)式就必須寫作\*\?,而不是\*?,因?yàn)楹笳叩囊馑际恰?這個字符可能出現(xiàn),也可能不出現(xiàn)”。

表2-5列出了常用量詞的轉(zhuǎn)義形式。

表2-5 各種量詞的轉(zhuǎn)義

之前還介紹了點(diǎn)號.,所以還必須講解點(diǎn)號的轉(zhuǎn)義:點(diǎn)號.是一個元字符,它可以匹配除換行符之外的任何字符,所以如果只想匹配點(diǎn)號本身,必須將它轉(zhuǎn)義為\.

因?yàn)槲崔D(zhuǎn)義的點(diǎn)號可以匹配任何字符,其中也可以包含點(diǎn)號,所以經(jīng)常有人忽略了對點(diǎn)號的轉(zhuǎn)義。如果真的這樣做了,在確實(shí)需要嚴(yán)格匹配點(diǎn)號時就可能出錯,比如匹配小數(shù)(如3.14)、IP地址(如192.168.1.1)、E-mail地址(如someone@somehost.com)。所以,如果要匹配的文本包含點(diǎn)號,一定不要忘記轉(zhuǎn)義正則表達(dá)式中的點(diǎn)號,否則就有可能出現(xiàn)例2-19那樣的錯誤。

例2-19忽略轉(zhuǎn)義點(diǎn)號可能導(dǎo)致錯誤

      #錯誤判斷浮點(diǎn)數(shù)
      print re.search(r"^\d+.\d+$", "3.14") != None     #  => True
      print re.search(r"^\d+.\d+$", "3a14") != None     #  => True
      #準(zhǔn)確判斷浮點(diǎn)數(shù)
      print re.search(r"^\d+\.\d+$", "3.14") != None    #  => True
      print re.search(r"^\d+\.\d+$", "3a14") != None    #  => False
主站蜘蛛池模板: 进贤县| 罗田县| 邵阳县| 韶关市| 古交市| 彰武县| 嘉黎县| 霍城县| 舞阳县| 商都县| 堆龙德庆县| 南和县| 阿瓦提县| 信宜市| 黄浦区| 南溪县| 新建县| 攀枝花市| 东乌珠穆沁旗| 铜鼓县| 尚义县| 东宁县| 北碚区| 永昌县| 故城县| 肇州县| 襄樊市| 色达县| 屏山县| 桂平市| 施甸县| 印江| 景宁| 江华| 襄汾县| 河北省| 定西市| 庆城县| 昌都县| 平潭县| 抚松县|