正则表达式——忽略优先量词
也有些时候,确实需要用到.*(或者[\s\S]*),比如匹配HTML代码中的JavaScript示例就是如此。
<script type="text/javascript">…</script>
匹配的模式仍然是:匹配open tag和close tag,以及它们之间的内容。open tag是 ,这两段的内容是固定的,非常容易写出对应的表达式,但之间的内容怎么匹配呢?在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代码保存在变量htmlSource中 jsRegex = 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代码保存在变量htmlSource中 jsRegex = 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 匹配优先量词与忽略优先量词
匹配优先量词 |
忽略优先量词 |
限定次数 |
* |
*? |
可能不出现,也可能出现,出现次数没有上限 |
+ |
+? |
至少出现1次,出现次数没有上限 |
? |
?? |
至多出现1次,也可能不出现 |
{m,n} |
{m,n}? |
出现次数最少为m次,最多为n次 |
{m,} |
{m,}? |
出现次数最少为m次,没有上限 |
{,n} |
{,n}? |
可能不出现,也可能出现,最多出现n次 |
忽略优先量词还可以完成许多其他功能,典型的例子就是提取代码中的C语言注释。
C语言的注释有两种:一种是在行末,以//开头;另一种可以跨多行,以/*开头,以*/结束。第一种注释很好匹配,使用//.*即可,因为点号.不能匹配换行符,所以//.*匹配的就是从//直到行末的文本,注意这里使用了量词*,因为//可能就是该行最后两个字符;第二种注释稍微复杂一点,因为/*…*/的注释和JavaScript一样,可能分成许多段,所以必须用到忽略优先量词;同时因为注释可能横跨多行,所以必须使用[\s\S]。因此,整个表达式就是/\*[\s\S]*?\*/(别忘了*的转义)。
另一个典型的例子是提取出HTML代码中的超链接。常见的超链接形似texthttp://somehost/somepath">text>。它以<a开头,以< a="">结束,href属性是超链接的地址。我们无法预先判断和之间到底会出现哪些字符,不会出现哪些字符,只知道其中的内容一直到结束 ,程序代码见例2-15。
例2-15 提取网页中所有的超链接tag
#仍然获得yahoo网站的源代码,存放在htmlSource中 for hyperlink in re.findall(r"<a\s[\s\S]+?a>", htmlSource): print hyperlink #更多结果未列出 <a href="http://search.yahoo.com/">Weba> <a href="http://images.search.yahoo.com/images">Imagesa> <a href="http://video.search.yahoo.com/video">Videoa>
值得注意的是,在这个表达式中的<a之后并没有使用普通空格,而是使用字符组简记法\s。html语法并没有规定此处的空白只能使用空格字符,也没有规定必须使用一个空白字符,所以我们用\s保证"至少出现一个空白字符"(但是不能没有这个空白字符,否则就不能保证匹配tag p="" name是a)。
之前匹配JavaScript的表达式是 ,它能应对的情况实在太少了:在<script之后可能不是空格,而是空白字符;再之后可能是type="text ,也可能用language取代type(实际上language是以前的写法,现在大都用type),甚至可能没有属性,直接是<script="" javascript?,也可能是type="application/javascript"> 。
所以必须改造这个表达式,将条件放宽:在script之后,可能出现空白字符,也可能直接是>,这部分可以用一个字符组[\s>]来匹配,之后的内容统一用[\s\S]+?匹配,忽略优先量词保证了匹配进行到到最近的为止。最终得到的表达式就是<script[\s>] [\s\S]+?。
对这个表达式稍加改造,就可以写出匹配类似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代码中,table、tr、td这三个元素经常是嵌套的,它们之间存在着包含关系。但是,仅仅使用正则表达式匹配,并不能得到"某个table包含哪些tr"、"某个td属于哪个tr"这种信息。此时需要像例2-16的那样,用程序整理出来。
例2-16 用正则表达式解析表格
htmlSource = """<table> <tr><td>1-1td>tr> <tr><td>2-1td><td>2-2td>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-1td> 2-1 2-2
注:因为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