首页 > 代码库 > Python基础——windows自动化篇(九)-正则表达式

Python基础——windows自动化篇(九)-正则表达式

正则表达式(regexp)

正则表达式在某种意义上可以算是字符串操作中的最高级别了,并不是因为它的语法的复杂,而是它的灵活。理解这一点就需要了解正则表达式的本质,无论多么复杂的正则表达式,它的本质就是字符串,目的就是用来记录其他字符串的规律。看似有些抽象,但是其实很容易理解,大多数人在使用dos命令的时候,会使用到通配符,比如在某个目录列出所有的pdf文档,方法就是dir *.pdf——这里的*表示统配,也就是可以代表任何字符串,这个命令也就是列出来所有符合以下命名的文件:任意字符串+”.pdf”后缀;doc和excel文档中的一些替换规则和格式化规则也涉及到这种简单的正则统配。我们所说的正则,其实就是类似的东西,更为复杂的字符串匹配规则——除非你需要匹配的字符串规则很容易。在Linux平台上,正则表达式是必须的,很多命令都基于正则表达式,如果没有这套高效的字符串匹配逻辑,那么对于使用者是很悲惨的。

讨论Python正则模块之前,必须要把正则表达式本身讨论清楚——事实上深刻理解了正则表达式,python中的使用就容易多了,不过不要妄想一次就可以把正则表达式都理解了,这是不可能的,就算是天才也需要实际的操作才能真正的学会。我不否定世界上有很多天才,但是我始终建议学习正则表达式,最少要学习3遍。完成这部分学习,并不是期望成为正则高手,这并不是这部分学习的目的,而且高手不是看教材就能学习出来的。这部分学习后,希望能够“入门”。遇到问题知道以一种什么样的思路进行思考,需要帮助的时候知道如何去查询相关的资料,这就足够了。我自己每次用到正则也是随查随用,很少做相关的事情,好在正则的相关资料网上多的出奇,足够一个菜鸟成为正则高手了。下面开始进入正则表达式的介绍和讨论。

在python中,正则表达式的模块是re,我们的讨论过程会使用这个模块举例子进行测试,另外,一些在线的正则表达式测试也可以进行测试,比如js的正则表达式测试网站:http://regexpal.com/。

 

字符

这里的字符和字符串中的字符是一样的,大多数字符匹配它本身,除了一些元字符,我们可以简单的认为元字符,就是正则语法中的一些关键字,它们有特殊的含义,有的表示特殊的匹配,有的表示其他的语法规则。简单的字符匹配如下:

Re的基本使用很简单,指定正则表达式匹配字符串,编译,匹配。Re.match方法如果匹配到,则返回一个匹配对象,如果没有匹配到,则返回空。我们可以看到,正则表达式’abc’可以匹配到它本身。事实上除了元字符,其他字符都是匹配它本身的。

 

元字符

正则中的元字符包括很多,具体可以见链接:http://msdn.microsoft.com/zh-cn/library/ae5bf541(v=vs.80).aspx。

元字符中的特殊字符包括:.^$*+?{[]\|()

在正则的语法中,它们有其他的含义和作用——一些是单独使用的,一些是组合使用的。我们先来看其中最常用的一个——[],它表示字符的集合。

 

字符集

包含在[]中的字符集合,有两个作用,一个是用来匹配范围,这里的范围是指,匹配到属于这个范围的任意一个字符;另一个,是包含某些元字符——大部分元字符在[]中会失去本身的特殊含义,成为普通的字符,除了^[]\这四个字符,因为他们是字符集语法。

其中,^放在[]的开头,表示取反,表示和[]中的字符集都不匹配,放在[]中间,就和其他元字符一样了,表示匹配它本身。简单的例子,[^abcd]表示匹配除abcd以外的任何字符。

在字符集中,逐一枚举是这样的方式当然是有解决方案的,可以用-来表示范围,比如[0-9]匹配0到9的数字,[a-zA-Z]匹配所有的字符。

 

\在正则中有两个作用,第一个是转义,第二个是特殊匹配。

 

转义字符

像上面讨论过的,一些元字符并不匹配他们本身,那么如何匹配这些元字符呢?在它们前面加上\进行转义就可以了。这和字符串中的路径是一样的,需要用\转义。所有在正则表达式中的带有特殊含义的字符,都需要\进行转义去匹配它们本身。当然,就像之前路径中转义的\一样,我们同样可以在字符串前加一个r来表示原始字符串,不然如果想要匹配\本身,需要’\\\\’这样麻烦的正则表达式,但是可以直接使用r’\’来表示。

 

特殊匹配

特殊匹配包括一些用\表示的,和一些其他特殊的字符。常用的有下面这些:

******************************************************************************

 

x|y

匹配 x 或 y(分支语句)。例如,‘z|food‘ 匹配“z”或“food”。‘(z|f)ood‘ 匹配“zood”或“food”

\b

匹配一个字边界(开始或结束),例如,“er\b”匹配“never”中的“er”,但不匹配“verb”中的“er”, “\ber\b”匹配”er”

\B

非字边界匹配,和\b相反。“er\B”匹配“verb”中的“er”,但不匹配“never”中的“er”。

\d

数字字符匹配。等效于 [0-9]

\D

非数字字符匹配。等效于 [^0-9]

\n

换行符匹配。等效于 \x0a 和 \cJ

\s

匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效

\S

匹配任何非空白字符。与 [^ \f\n\r\t\v] 等效

\t

制表符匹配。与 \x09 和 \cI 等效

\w

匹配任何字类字符,包括下划线。与“[A-Za-z0-9_]”等效

\W

与任何非单词字符匹配。与“[^A-Za-z0-9_]”等效

.

匹配出换行\n之外的任何字符,等价于[^\n]

^

在字符集之外,匹配字符串的开始*

$

在字符集之外,匹配字符串的结束*

 

 

*:其中,^和$用于完全匹配的情况,各自匹配开始和结束,如果整体的完全匹配可以使用^pattern$的格式,如完全匹配123,写成^123$。
******************************************************************************

 

重复

到此为止,正则表达式中的基本元素几乎都已经简单讨论过了,基本的语法元素中,还有一个重要的部分,就是重复。既然正则表达式是用来记录字符串规则的,那么重复的规则,在字符串中简直是比比皆是。比如电话号码或者QQ号码,就是数字的重复,单词就是字母的重复,还有更多重复的组合等等。正则表达式中,重复的语法包括下面几种:                       

 

*

重复零次或者多次,次数不限

+

重复一次或者更多次

重复0次或者1次

{n}

重复n次

{n,}

重复n次或者更多次

{n,m}

重复n到m次

?

当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是“非贪心的”。“非贪心的”模式匹配搜索到的、尽可能短的字符串,而默认的“贪心的”模式匹配搜索到的、尽可能长的字符串。

 

         最后一个需要解释一下,正则表达式在默认情况下,有一种贪婪原则,也就是尽可能的去匹配长的字符串。比如,在字符串“aaaa”中,“a+?”只匹配单个“a”,而“a+”匹配所有“a”,也就是”aaaa”。这种情况出现在重复中,在重复的标识符后加上一个?,就代表了非贪婪匹配,匹配到最短的字符串。可以简单的写代码如下:

 

Re模块处理基本正则

         基本的正则表达式语法都已经讨论过了,灵活运用这些语法,可以完成大部分一般的匹配操作。下面用一些简单的例子来讨论re模块的使用,并熟悉上面已经讨论过的这些基本的正则语法。之后,会讨论一些正则表达式的高级操作。

         和之前讨论过的思路一样,re模块进行字符串匹配,大体还是3个步骤:

  1. 用re.compile得到一个编译过的pattern object,支持两个参数,一个是正则表达式,是必须的,第二个是flag,默认为0,其他支持的flag包括:

Re.S           DOTALL             表示.匹配任意字符,不包括\n

Re.I           IGNORECASE   表示忽略大小写

Re.L           LOCALES           表示让\w,\W,\b,\B和当前locale一致

Re.M         MULTILINE       表示多行匹配模式,只影响^和$

Re.X          VERBOSE          表示verbose模式,增加正则表达式的可读性

  1. 使用pattern object的各种方法进行对匹配字符串的操作;
  2. 如果pattern object的方法返回match object对象,可以从中得到匹配字符串信息;

 

Parren object 常用的方法包括match,search,findall,其中match是从字符串开始处进行匹配,seach是扫描整体字符串找到第一个匹配,findall用来搜索字符串中所有匹配的内容,并以元组返回。


Parren object 常用的正则方法包括match,search,sub和findall(或finditer),其中match是从字符串开始处进行匹配,seach是扫描整体字符串找到第一个匹配,findall用来搜索字符串中所有匹配的内容,并以元组返回。注意正则方法只是对parrent object而言,还有一种叫做匹配方法,是对已经匹配到的对象而言,即对match object而言。

下面是这些常用的正则方法的示例:

         Match(str,[pos,[endpos]]):匹配方法,给定匹配区间,注意正则的贪心问题,示例如下:

 

         Search(str,[pos,[endpos]]): 搜索方法,给定匹配区间内返回到第一个搜到的值位置,使用方法和match相当,但是并不是从第一位开始,这点和match不同,示例如下:

 

sub(repl, string[, count = 0]) --> newstring:用来查找和替换,用指定的字符串替换被匹配到的字符串,subn将新的字符串和被替换的数量以元组方式返回,如下:

 

findall(string[, pos[, endpos]]) --> list:查找方法,给定匹配区间内查找所有匹配的集合并返回。如下:

Finditer返回一个interator来遍历所有的match object:

 

Match object 常用的方法包括start,end,span,group,分别用来表示匹配区域的开始位,结束位,区间和匹配字符串;常用属性有pos,endpos,string和re,分别表示pattern object的开始位,结束位,被匹配的字符串和匹配对象,即match(str,[pos,[endpos]])中的pos,endpos,string和patter object实例本身,它们的示例如下:

常用方法:

常用属性:

 

正则表达式高级
分组

         分组的出现最早是为了解决正则中的重复问题,单个字符的重复可以直接在字符后加上限定,多字符的重复就需要分组了;当然一个正则中可以出现多个分组,我们可以将它称之为子表达式。分组在实际应用中作用比较大,网络上一个比较常用的示例就是IP地址的匹配,一个最粗糙的就是(\d{1,3}\.){3}\d{1,3},匹配四个三位数字,中间用.分开,这就是分组的最简单的应用。当然IP地址的正则并不是这么去匹配和计算的,由于正则表达式本身不能去进行数字的计算,所以IP地址的匹配稍微麻烦一些,不过这种常用的很容易查到,((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。

         Python中处理分组使用match对象中的group()方法,一个简单的示例如下,取出一个简易的存货信息中的匹配字段:

         分组情况下,group(),start(),end(),span()都可以通过传入索引来得到各自组对应的值,也就是每个组对应的其实位置结束位置,都是相互独立的——也就意味着对于子表达式的嵌套来说,匹配的原理一致。比如:

         我们通常使用分组的方法对字符串进行特定格式的取值,一般情况下,对于元素比较多,顺序不确定的集合,更适合的方式是字典而不是列表,那么group能不能通过传入键值而不是索引进行取值呢?这个当然是可以的。正则的分组中提供了组名的机制,不只是最终取值方便,对于正则表达式内部的引用作用更大。还是刚才的存货的例子,重新写一下:

索引方式:

 

         示例中,用?P<ID>.*代替了.*,这样分组后的匹配就可以支持关键字的方式进行取值。事实上,正则中的这种方法本质是给组命名,对于复杂的正则可能需要不断重复之前已经匹配到的东西,组名的方式就很清晰和方便了。另外,?P的写法是python中的扩展,大写的P代表python,一般其他的正则中写法不同,标准的正则中没有这个字母。

         这种扩展源于perl的正则表达式的扩展,事实上从perl5.0以后对正则的支持,re模块大部分都支持。正则的扩展一般来说同样是使用元字符来标识,perf中使用?来标识,因为?即非转义,又可以直接用在(后进行标识。Python中采取同样的方法,在?后加上大写的P进行标识,省去转义的麻烦。

无捕获组

         和分组以及组名比起来,无捕获组很容易。它的匹配方式和效率和分组没有任何区别,甚至也可以重复子表达式,通常当我们对匹配组的内容不感兴趣的时候,需要用到无捕获组。既然不去捕获,那么它的意义何在?一般来说,无捕获组的意义不在于它自己本身,而在于对其他分组的影响——类似于一个占位的作用。当表达式改变或者分组改变,可以很简单的修改表达式进行分组的匹配,python中用?:标识。看一个简单的无捕获组的示例:

向后引用

       前面几个例子虽然都是使用分组来对字符串进行取值,但是之前提到了分组的根本原因是为了处理字符串的重复。向后引用用于重复匹配和搜索到前面已经匹配到的分组内容,同样的,索引和组名的方式都支持,但是最好还是使用组名的方式,虽然写起来稍微多一些,但是很清楚,不容易出问题,尤其是在正则这种看起来比较复杂的逻辑中。下面是一个经典的示例,匹配两个重复的单词:

分别用索引和组名完成了上面的匹配,注意组名向后引用的格式:(?P=name)

零宽断言

         零宽断言,从字面上理解,就是一种断言。正则中的断言同样是一种标识,其实之前的^$\b这些标识位置的也都是一种断言。零宽断言的目的是为了匹配字符串的位置,而不是字符串和文本本身,基本上正则中的断言都是匹配位置的。通俗的讲,就是这种断言同样是为了寻找某个位置,这个位置满足一定的条件,这就是零宽断言。零宽断言按照匹配方向和是否肯定分为四种情况:

向前界定:

顺序肯定匹配(?=exp),表示被匹配的文本右边要匹配exp表达式,示例如下:

 

向前否定界定:

顺序否定匹配(?!),表示后面不匹配的exp,示例如下:

向后界定:

逆序肯定匹配(?<=exp),表示左面匹配exp表达式,注意所有反向界定的匹配文本必须定长!,示例如下:

向后否定界定:

逆序否定匹配(?<),表示左边不匹配exp,示例如下:

平衡组和递归

正则表达式中,平衡组和递归属于稍微复杂一点的东西,不过好像Python并不提供支持,我了解到的.net framework是提供这种正则的支持,其他语言有些不支持,有些语法不同,python中暂时还没有找到。他们在匹配html这样复杂的文本时用到的比较多,一般来说也都可以通过和组的组合来解决。这个属于稍微高级一点的东西,这里暂时不讨论。

Python基础——windows自动化篇(九)-正则表达式