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

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

第2章 量詞

2.1 一般形式

根據上一章的介紹,可以用字符組[0-9]或者\d 匹配單個數字字符?,F在用正則表達式來驗證更復雜的字符串,比如大陸地區的郵政編碼。

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

例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重復了6次,讀寫都不方便。為此,正則表達式提供了量詞(quantifier),比如上面匹配郵政編碼的表達式,就可以如例2-2那樣,簡寫為\d{6},它使用阿拉伯數字,更簡潔也更直觀。

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

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

量詞限定的出現次數一般都有明確下限,如果沒有,則默認為0。有一些語言(比如Ruby)支持{,n}的記法,這時候并不是“不確定長度的下限”,而是省略了“下限為0”的情況,比如\d{,6}表示“數字字符串最多可以有6個字符”。不過,這種用法并不是所有語言中都通用的,比如Java就不支持這種寫法,所以必須寫明{0,n}。我推薦的做法是:最好使用{0,n}的記法,因為它是廣泛支持的。表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}是通用形式的量詞,正則表達式還有三個常用量詞,分別是+、?、*。它們的形態雖然不同于{m,n},功能卻是相同的(也可以把它們理解為“量詞簡記法”),具體說明見表2-2。

表2-2 常用量詞

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

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

例2-4量詞?的應用

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

其實這樣的情況還有很多,比如favorfavourcolorcolour。此外還有很多其他應用場合,比如httphttps,雖然是兩個概念,但都是協議名,可以用https?匹配;再比如表示價格的字符串,有可能是100也有可能是¥100,可以用¥?100匹配實際上,這個問題比較復雜,因為¥并不是一個ASCII字符,所以¥?可能會產生問題,具體情況請參考第7章。。

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

例2-5量詞+的應用

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

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

例2-6量詞*的應用

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

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

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

open tag的特點是以<開頭,然后是“若干字符”(但不能以/開頭),最后是>,所以對應的正則表達式是<[^/][^>]*>;注意:因為[^/]必須匹配一個字符,所以“若干字符”中其他部分必須寫成[^>]*,否則它無法匹配名字為單個字符的標簽,比如<b>。

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

self-closing tag的特點是以<開頭,中間是“若干字符”,最后是/>,所以對應的正則表達式是<[^>]+/>。注意:這里不是<[^>/]+/>,排除型字符組只排除>,而不排除/,因為要確認的只是在結尾的>之前出現/,如果寫成<[^>/]+/>,則要求tag內部不能出現/,就無法匹配<img src="http://somehost/picture" />這類的tag了。

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

表2-3 各類tag的匹配

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

再仔細觀察,你或許會發現,匹配open tag的表達式,也可以匹配self-closing tag:<[^/][^>]*>能夠匹配<br/>,因為[^>]*并不排除對/的匹配。那么將表達式改為<[^/][^>]*[^/]>,就保證匹配的open tag不會以/>結尾了。

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

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

2.3 數據提取

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

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

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

      #注意這里使用鏈式編程
      print re.search(r"\d{6}", "ab123456cd").group(0)
      123456
      print re.search(r"^<[^>]+>$", "<bold>").group(0)
      <bold>

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

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

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

      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的正則表達式,還可以通過re.findall()將某個HTML頁面中所有的tag提取出來,下面以Yahoo首頁為例。

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

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

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

      #導入需要的package
      import urllib
      import re
      #讀入HTML源代碼
      sock = urllib.urlopen("http://yahoo.org/")
      htmlSource = sock.read()
      sock.close()
      #匹配,輸出結果([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 點號

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

例2-10點號.的匹配

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

有一個字符不能由點號匹配,就是換行符\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 濫用點號的問題

因為點號能匹配幾乎所有的字符,所以實際應用中許多人圖省事,隨意使用.*.+,結果卻事與愿違,下面以雙引號字符串為例來說明。

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

例2-12 “圖省事”的意外結果

      #字符串的值是"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"。這是為什么呢?

這個問題比較復雜,現在只簡要介紹,以說明圖省事導致錯誤的原因,更深入的原因涉及正則表達式的匹配原理,在第8章詳細介紹。

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

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

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

一開始,"匹配",然后輪到字符q,.*可以匹配它,也可以不匹配,因為使用了匹配優先量詞,所以.*先匹配q,并且記錄下這個狀態【q也可能是.*不應該匹配的】;

接下來是字符 u,.*可以匹配它,也可以不匹配,因為使用了匹配優先量詞,所以.*先匹配u,并且記錄下這個狀態【u也可能是.*不應該匹配的】;

……

現在輪到字符 g,.*可以匹配它,也可以不匹配,因為使用了匹配優先量詞,所以.*先匹配g,并且記錄下這個狀態【g也可能是.*不應該匹配的】;

最后是末尾的",.*可以匹配它,也可以不匹配,因為使用了匹配優先量詞,所以.*先匹配",并且記錄下這個狀態【"也可能是.*不應該匹配的】。

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

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

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

圖2-2 表達式".*"的匹配過程

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

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

2.6 忽略優先量詞

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

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

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

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

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

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

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

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

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

      #假設上面的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>,整個表達式的匹配就成功了,邏輯就是如此,無可改進。而且,這個問題也不能模仿之前雙引號字符串匹配,用[^"]*匹配<script…></script>之間的代碼,因為排除型字符組只能排除單個字符,[^</script>]不能表示“不是</script>的字符串”。

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

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

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

例2-14準確匹配JavaScript代碼

      #仍然假設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可以看到,匹配優先量詞與忽略優先量詞逐一對應,只是在對應的匹配優先量詞之后添加?,兩者限定的元素能出現的次數也一樣,遇到不能匹配的情況同樣需要回溯;唯一的區別在于,忽略優先量詞會優先選擇“忽略”,而匹配優先量詞會優先選擇“匹配”。

表2-4 匹配優先量詞與忽略優先量詞

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

C語言的注釋有兩種:一種是在行末,以//開頭;另一種可以跨多行,以/*開頭,以*/結束。第一種注釋很好匹配,使用//.*即可,因為點號.不能匹配換行符,所以//.*匹配的就是從//直到行末的文本,注意這里使用了量詞*,因為//可能就是該行最后兩個字符;第二種注釋稍微復雜一點,因為/*…*/的注釋和JavaScript一樣,可能分成許多段,所以必須用到忽略優先量詞;同時因為注釋可能橫跨多行,所以必須使用[\s\S]。因此,整個表達式就是/\*[\s\S]*?\*/(別忘了*的轉義)。

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

例2-15提取網頁中所有的超鏈接tag

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

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

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

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

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

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

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

例2-16用正則表達式解析表格

      # 這里用到了Python中的三重引號字符串,以便字符串跨越多行,細節可參考第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>

注:因為tag是不區分大小寫的,所以如果還希望匹配大寫的情況,則必須使用字符組,table 寫成[tT][aA][bB][lL][eE],tr寫成[tT][rR],td寫成[tT][dD]。

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

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

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

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

例2-17用正則表達式拆解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,所以在正則表達式中,應該把斜線字符/換成反斜線字符\。因為在正則表達式中反斜線字符\是用來轉義其他字符的,為了表示反斜線字符本身,必須連寫兩個反斜線,所以兩個表達式分別改為^.*\\[^\\]*$,代碼見例2-18。

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

      #反斜線\必須轉義寫成\\
      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 轉義

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

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

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

另外值得一提的是忽略優先量詞的轉義,雖然忽略優先量詞也包含不只一個字符,但是在轉義時卻不像一般形式的量詞那樣,只轉義第一個字符即可,而需要將兩個量詞全部轉義。舉例來說,如果要匹配字符串*?,正則表達式就必須寫作\*\?,而不是\*?,因為后者的意思是“*這個字符可能出現,也可能不出現”。

表2-5列出了常用量詞的轉義形式。

表2-5 各種量詞的轉義

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

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

例2-19忽略轉義點號可能導致錯誤

      #錯誤判斷浮點數
      print re.search(r"^\d+.\d+$", "3.14") != None     #  => True
      print re.search(r"^\d+.\d+$", "3a14") != None     #  => True
      #準確判斷浮點數
      print re.search(r"^\d+\.\d+$", "3.14") != None    #  => True
      print re.search(r"^\d+\.\d+$", "3a14") != None    #  => False
主站蜘蛛池模板: 禄丰县| 德安县| 阜康市| 林州市| 龙泉市| 晋城| 邵东县| 上虞市| 保亭| 鹤壁市| 尖扎县| 铜鼓县| 五指山市| 南安市| 巴塘县| 进贤县| 贵南县| 仙游县| 隆昌县| 南投县| 南平市| 龙川县| 桑日县| 石楼县| 陆丰市| 林周县| 张北县| 龙江县| 辽宁省| 怀安县| 隆林| 达拉特旗| 云安县| 嘉祥县| 文登市| 当涂县| 霍林郭勒市| 永城市| 阜南县| 台东县| 邵东县|