
第2章 量词
2.1 一般形式
根据第1章的介绍,可以用字符组[0-9]或者\d匹配单个数字字符。现在用正则表达式来验证更复杂的字符串,比如中国大陆地区的邮政编码。
粗略来看,邮政编码并没有特殊的规定,只是6位数字构成的字符串,比如201203、100858,所以用正则表达式来表示就是\d\d\d\d\d\d,如例2-1所示,只有同时满足“长度是6个字符”和“每个字符都是数字”这两个条件,匹配才成功(同样,这里不能忽略^和$)。
例2-1 匹配邮政编码

虽然这不难理解,但\d重复了6次,读写都不方便。为此,正则表达式提供了量词(quantifier),比如上面匹配邮政编码的表达式,就可以如例2-2那样,简写为\d{6},它使用阿拉伯数字,更简洁也更直观。
例2-2 使用量词简化字符组

量词还可以表示不确定的长度,其通用形式是{m,n},其中m和n是两个数字(有些人习惯在代码中的逗号之后添加空格,这样更好看,但是量词中的逗号之后不能有空格),它限定之前的元素[1]能够出现的次数,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 表示不确定长度的量词

读者可能会好奇,有些量词没有指定上限,那么重复次数真的没有上限吗?重复亿万次也不会有问题吗?这是一个好问题,严谨的程序员当然希望知道答案。虽然普通的文档没有说明,但我们可以查阅相关的源代码来找到答案—通常,这个隐式的上限是65536。按照我使用正则表达式的经验,在绝大多数情况下,这个上限是足够的,所以一般可以认为“不存在上限”。
2.2 常用量词
{m,n}是通用形式的量词,正则表达式还有3个常用量词,分别是+、?、*。它们的形态虽然不同于{m,n},功能却是相同的(也可以把它们理解为“量词简记法”),具体说明见表2-2。
表2-2 常用量词

在实际应用中,在很多情况下只需要表示这三种意思,所以常用量词的使用频率要高于{m,n},下面分别说明。
大家都知道,美国英语和英国英语有些词的写法是不一样的,比如traveler和traveller,如果希望“通吃”traveler和traveller,就要求第2个l是“至多出现1次,也可能不出现”的,正好使用?量词:travell?er,如例2-4所示。
例2-4 量词?的应用

其实这样的情况还有很多,比如favor和favour、color和colour。此外还有很多其他应用场合,比如http和https,虽然是两个概念,但都是协议名,可以用https?匹配;再比如表示价格的字符串,有可能是100也有可能是¥100,可以用¥?100匹配。[2]
量词也广泛应用于解析HTML代码。HTML是一种“标签语言”,它包含各种各样的tag(标签),比如<head>、<img>、<table>等,这些tag的名字各异,形式却相同:从<开始,到>结束,在<和>之间有若干字符,“若干”的意思是长度不确定,但不能为0(<>并不是合法的tag),也不能是>字符。[3]如果要用一个正则表达式匹配所有的tag,需要用<匹配开头的<,用>匹配结尾的>,用[^>]+匹配中间的“若干字符”,所以整个正则表达式就是<[^>]+>,程序如例2-5所示。

例2-5 量词+的应用类似的,也可以使用正则表达式匹配双引号字符串。不同的是,双引号字符串的两个双引号之间可以没有任何字符,""是一个完全合法的字符串。应该使用量词*,于是整个正则表达式就成了"[^"]*",程序见例2-6。
例2-6 量词*的应用

注:字符串中表示双引号需要转义写成\",这并不是正则表达式中的规定,而是为字符串转义考虑。
量词的使用有很多学问,不妨多看几个tag匹配的例子:tag可以粗略分为open tag和close tag,比如<head>就是open tag,而</html>就是close tag;另外还有一类标签是self-closing tag,比如
。现在来看分别匹配这三类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:<[^/][^>]*>能够匹配
,因为[^>]*并不排除对/的匹配。那么将表达式改为<[^/][^>]*[^/]>,就保证匹配的open tag不会以/>结尾了。
不过这会产生新的问题:<[^/][^>]*[^/]>能匹配的tag,在<和>之间出现了两个[^/],上一章已经讲过,排除型字符组表示“在当前位置,匹配一个没有列出的字符”,所以tag里的字符串必须至少包含两个字符,这样就无法匹配<u>了。
仔细想想,真正要表达的意思是,在tag内部的字符串不能以/开头,也不能以/结尾,如果这个字符串只包含一个字符,那么它既是开头,又是结尾,使用两个排除型字符组显然是不合适的,看起来没办法解决了。其实,也只是现有的知识还不足够解决这个问题而已,在第69页有这个问题的详细解法。
2.3 数据提取
正则表达式的功能很多,除去之前介绍的验证(字符串能否由正则表达式匹配),还可以从某个字符串中提取出某个字符串能匹配的所有文本。
第1章提到,re.search()如果匹配成功,返回一个MatchObject对象。这个对象包含了匹配的信息,比如表达式匹配的结果,可以像例2-7那样,通过调用MatchObject.group(0)来获得。这个方法以后会详细介绍,现在只需要了解一点:调用它可以得到表达式匹配的文本。
例2-7 通过MatchObject获得匹配的文本

这里再介绍一个方法:re.findall(pattern,string)。其中pattern是正则表达式,string是字符串。这个方法会返回一个数组,其中的元素是在string中依次寻找pattern能匹配的文本。
以邮政编码的匹配为例,假设某个字符串中包含两个邮政编码:zipcode1:201203,zipcode2:100859,仍然使用之前匹配邮政编码的正则表达式\d{6},调用re.findall()可以将这两个邮政编码提取出来,如例2-8所示。注意,这次要去掉表达式首尾的^和$,因为要使用正则表达式在字符串中寻找匹配,而不是验证整个字符串能否由正则表达式匹配。
例2-8 使用re.findall()提取数据

借助之前匹配各种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


2.4 点号
第1章讲到了各种字符组,与它相关的还有一个特殊的元字符:点号.。一般文档里都会提到,点号可以匹配“任意字符”,点号确实可以匹配“任意字符”,常见的数字、字母、各种符号都可以,如例2-10所示。
例2-10 点号.的匹配

有一个字符不能由点号匹配,就是换行符\n。这个字符平时看不见,却存在,而且在处理时并不能忽略(第3章会给出具体的例子)。
如果非要匹配“任意字符”,有两种办法:可以指定使用单行匹配模式,在这种模式下,点号可以匹配换行符(☞86);或者使用第1章介绍的“自制”通配字符组[\s\S](也可以使用[\d\D]或[\w\W]),正好涵盖了所有字符。例2-11清楚地说明,这两个办法都可以匹配换行符。
例2-11 换行符的匹配

2.5 滥用点号的问题
因为点号能匹配几乎所有的字符,所以实际应用中许多人图省事,随意使用.*或.+,结果却事与愿违,下面以双引号字符串为例来说明。
之前我们使用表达式"[^"]*"匹配双引号字符串,而“图省事”的做法是".*"。通常这么用是没有问题的,但也可能有意外,例2-12就是一种“出乎意料”的情况。
例2-12 “图省事”的意外结果


用".*"匹配双引号字符串,不但可以匹配正常的双引号字符串"quoted string",还可以匹配格式错误的字符串"quoted string" and another"。这是为什么呢?
这个问题比较复杂,现在只简要介绍,以说明图省事导致错误的原因,更深入的原因涉及正则表达式的匹配原理,在第8章详细介绍。
在正则表达式".*"中,点号.可以匹配任何字符,*表示可以匹配的字符串长度没有限制,所以.*在匹配过程结束以前,每遇到一个字符(除去无法匹配的\n),.*都可以匹配,但是到底是匹配这个字符,还是忽略它,将其交给之后的"来匹配呢?
答案是,具体选择取决于所使用的量词。正则表达式中的量词分为几类,之前介绍的量词都可以归到一类,叫作匹配优先量词(greedy quantifier,也有人将其翻译为贪婪量词[4])。匹配优先量词,顾名思义,就是在拿不准是否要匹配的时候,优先尝试匹配,并且记下这个状态,以备将来“反悔”。
来看表达式".*"对字符串"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示例就是如此。

匹配的模式仍然是:匹配open tag和close tag,以及它们之间的内容。open tag是<script type="text/javascript">,close tag是</script>,这两段的内容是固定的,非常容易写出对应的表达式,但之间的内容怎么匹配呢?在JavaScript代码中,各种字符都可能出现,所以不能用排除型字符组,只能用.*。比如,用一个正则表达式匹配下面这段HTML源代码:

开头和结尾的tag都容易匹配,中间的代码比较麻烦,因为点号.不能匹配换行符,所以必须使用[\s\S](或者[\d\D]、[\w\W])。

这个表达式确实可以匹配上面的JavaScript代码。但是如果遇到更复杂的情况就会出错,比如针对下面这段HTML代码,程序运行结果如例2-13。

例2-13 匹配JavaScript代码的错误


用<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代码


从表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>结束[5],程序代码见例2-15。
例2-15 提取网页中所有的超链接tag


值得注意的是,这个表达式中的<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>。[6]
所以必须改造这个表达式,将条件放宽:在script之后,可能出现空白字符,也可能直接是>,这部分可以用一个字符组[\s>]来匹配,之后的内容统一用[\s\S]+?匹配,忽略优先量词保证了匹配进行到最近的</script>为止。最终得到的表达式就是<script[\s>][\s\S]+?</script>。
对这个表达式稍加改造,就可以写出匹配类似tag的表达式。在解析页面时,常见的需求是提取表格中各行、各单元(cell)的内容。表格的tag是<table>,行的tag是<tr>,单元的tag是<td>,所以,它们可以分别用下面的表达式匹配,请注意其中的[\s>],它兼顾了可能存在的其他属性(比如<table border="1">),同时排除了可能的错误(比如<tablet>)。

在实际的HTML代码中,table、tr、td这三个元素经常是嵌套的,它们之间存在着包含关系。但是,仅仅使用正则表达式匹配,并不能得到“某个table包含哪些tr”“某个td属于哪个tr”这种信息。此时需要像例2-16那样,用程序整理出来。
例2-16 用正则表达式解析表格


注:因为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中解析出两个部分,使用匹配优先量词是非常方便的。从字符串的起始位置开始,用.*/匹配路径,根据之前介绍的知识,它会回溯到最后(最右)的斜线字符/,也就是文件名之前;在字符串的结尾部分,[^/]*能匹配的就是真正的文件名。第1章介绍过^和$,它们分别表示“定位到字符串的开头”和“定位到字符串的结尾”,所以应该把^加在匹配路径的表达式之前,得到^.*/,而把$加在匹配真正文件名的表达式之后,得到[^/]*$,代码见例2-17。
例2-17 用正则表达式拆解Linux/UNIX的路径

Windows下的路径分隔符是\,比如C:\Program Files\Python 2.7.1\python.exe,所以在正则表达式中,应该把斜线字符/换成反斜线字符\。因为在正则表达式中反斜线字符\是用来转义其他字符的,为了表示反斜线字符本身,必须连写两个反斜线,所以两个表达式分别改为^.*\\和[^\\]*$,代码见例2-18。
例2-18 用正则表达式拆解Windows的路径

2.7 转义
前面讲解了匹配优先量词和忽略优先量词,现在介绍量词的转义。[7]
在正则表达式中,*、+、?等作为量词的字符具有特殊意义,但有些情况下我们需要的就是这些字符本身,此时就必须使用转义,也就是在它们之前添加反斜线\。
对常用量词所使用的字符+、*、?来说,如果希望表示这三个字符本身,直接添加反斜线,变为\+、\*、\?即可。但是在一般形式的量词{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 忽略转义点号可能导致错误

[1]在第1章中提到,字符组是正则表达式的基本“结构”之一,而此处提到之前的“元素”,在此做一点解释。在本书中,“结构”一般指的是正则表达式所提供功能的记法。比如字符组就是一种结构,第3章要提到的括号也是一种结构;而“元素”指的是具体的正则表达式中的某个部分,比如某个具体表达式中的字符组[a-z],可以算作一个元素,“元素”也叫“子表达式”(sub-expression)。
[2]实际上,这个问题比较复杂,因为¥并不是一个ASCII字符,所以¥?可能会遇到古怪的问题,具体情况请参考第7章。
[3]如果你对HTML代码比较了解,可能会有疑问,假如tag内部出现>符号,怎么办?这种情况确实存在,比如<input name=txt value=">">。以目前已经讲解的知识还无法解决这个问题,不过第3章就会给出它的解法。
[4]许多文档都翻译为“贪婪量词”,单独来看这是没问题的,但考虑到正则表达式中还有其他类型的量词,其英文名字的形式较为统一,所以我在翻译《精通正则表达式》时采用了“匹配优先/忽略优先/占有优先”的名字,未见读者提出反对,故此处延用此译法。
[5]根据HTML规范,<a>这个tag可用来表示超链接,也可以用作书签,或兼作两种用途,考虑到书签的情况很少见,这里没有做特殊处理。
[6]严格说起来,如果只出现<script>,无法保证这里出现的就是JavaScript代码,也可能是VBScript代码,但考虑到真实世界中的情况,基本可以认为<script标识的“就是”JavaScript代码,所以这里不进行区分。
[7]Java等语言还支持“占有优先量词(possessive quantifier)”,但这种量词较复杂,使用也不多,所以本书中不介绍占有优先量词。