正则指引
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第2章 量词

2.1 一般形式

根据上一章的介绍,可以用字符组[0-9]或者\d 匹配单个数字字符。现在用正则表达式来验证更复杂的字符串,比如大陆地区的邮政编码。

粗略来看,邮政编码并没有特殊的规定,只是6位数字构成的字符串,比如201203100858,所以用正则表达式来表示就是\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 href="http://search.yahoo.com/">Web</a>
      <a href="http://images.search.yahoo.com/images">Images</a>
      <a href="http://video.search.yahoo.com/video">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代码中,tabletrtd这三个元素经常是嵌套的,它们之间存在着包含关系。但是,仅仅使用正则表达式匹配,并不能得到“某个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