第二章 正则表达式

本来想重新把基础部分再看一遍

但是又想想

还不如直接往下看

不懂再往前查

这样还记得牢一些

等以后再看回基础 

再巩固

看了看正则表达式

python确实好用

这里只是粗略看了看

用到再回来查

明天学网络编程

2.1 介绍/动机
  处理文本和数据是件大事。如果您不相信我说的话,请仔细看看现如今的计算机主要都在做些什么工作。文字处理、网页填表、来自数据库的信息流、股票报价信息、新闻列表,这个清单还会不断地增长。因为我们可能不知道这些需要计算机编程处理文本或数据的具体内容,所以能把这些文本或数据以某种可被计算机识别和处理的模式表达出来是非常有用的。假设我在运营一个电子邮件档案公司,而您是我的一位顾客,比如说,您想获得自己去年二月间收发的所有邮件,如果我能设计一个计算机程序来整理信息然后将它转发给您,而不是通过人工方法通读您的邮件后再手动地处理您的请求,如此处理将会非常不错。因为如果有人会看遍您的邮件信息,哪怕只是是用眼睛看一下邮件上的时间,您可能都会对此感到担心(甚至愤怒)。又比如,您可能会认为凡是带有“ILOVEYOU”这样主题的邮件都是已感染病毒的信息,并要求从您的个人邮箱中删除它们。这就引出一个问题,我们如何通过编程使计算机具有在文本中检索某种模式的能力。正则表达式(RE)为高级文本模式匹配,以及搜索-替代等功能提供了基础。正则表达式(RE)是一些由字符和特殊符号组成的字符串,它们描述了这些字符和字符的某种重复方式,因此能按某种模式匹配一个有相似特征的字符串的集合,因此能按某模式匹配一系列有相似特征的字符串 。换句话说,它们能匹配多个字符串 - 一个只能匹配一个字符串的RE 模式是乏味且毫无作用的,你说是不是。

 

你可以用这个正则表达式匹配有效的Python 标志符。 "[A-Za-z]\w+ " 的含义是:第一个字符是字母,即,由大写字母A 到Z 或是小写字母a 到z 组成, 它后面至少(+)跟有一个由字
母或数字组成的字符(\w). 如图,你看到有很多字符串被过滤,只有那些符合我们要求的RE 模式的字符串被筛选出来。比如,“4xZ”,因为它是以数字开头的,所以被过滤了。
Python 通过标准库的re 模块支持正则表达式(RE)。本节我们将向你简要地介绍这。限于篇幅,内容将仅涉及Python 编程中正则表达式(RE)方面最常见的内容。你们(对正则)的经验(熟悉程度)肯定不同。我们强烈建议您阅读一些官方帮助文档和与此主题有关的文本。那么你对字符串的理解方式就会有所改变。


核心笔记:查找与匹配的比较
  本章通篇涉及到对查找和匹配用法的讲述。当我们完全讨论与字符串中模式有关的正则表达式时,我们会用术语 “matching”(“匹配”),指的是术语pattern-matching(模式匹配)。在Python
专门术语中,有两种主要方法完成模式匹配:搜索(searching)和匹配(matching)。搜索,即在字符串任意部分中查找匹配的模式,而匹配是指,判断一个字符串能否从起始处全部或部分的匹配某个模式。搜索通过search()函数或方法来实现,而匹配是以调用match()函数或方法实现的。总之,当我们说模式的时候,我们全部使用术语“matching”(“匹配”);我们按照Python 如何完成模式匹配的方式来区分“搜索”和“匹配”。

2.1.1 您的第一个正则表达式
  我们上面已经提到,正则表达式是含有文本和特别字符的字符串,这些文本和特别字符描述的模式可以识别各种字符串。我们还简单阐述了正则表达式字母表,以及用于匹配通用文本的正则表达式字母表——所有大小写字母及数字的集合。也存在特别的字母表,比如,只含有字符"0"和"1"的字母表. 该字母表可以表示所有二进制整数的集合,即,"0," "1," "00," "01," "10," "11,"
"100," 等.

  让我们看看正则表达式的基本情况,虽然正则表达式常被视为是“高级主题”,有时候它们也是非常简单的。我们列出一些用一般文本的标准字母组成简单的正则表达式及它们所描述的字符串。
以下的正则表达式是最基本,最普通的。它们仅由一个字符串定义了一个模式,该模式仅匹配这个字符串本身,该字符串由正则表达式定义。以下是正则表达式(RE)和匹配它们的字符串。
RE Pattern String(s) Matched
foo foo
Python Python
abc123 abc123
上表中第一个正则表达式模式是"foo"。这个模式不包含任何特殊符号去匹配其他符号,它仅匹配自身所描述的,所以只有字符串"foo"匹配此模式。同理,“Python”和“abc123.”也一样。正则
表达式的强大之处在于特殊符号的应用,特殊符号定义了字符集合,子组匹配,模式重复次数。正是这些特殊符号使得一个正则表达式可以匹配字符串集合而不只是一个字符串。

2.2 正则表达式使用的特殊符号和字符
  现在,我们来介绍最常用的元字符(metacharacters)——特殊字符和符号,正是它们赋予了正则表达式强大的功能和灵活性。正则表达式中最常见的符号和字符见表15.1.
  表15.1 常用正则表达式符号和特殊字符
    记号                说明    

  SYMBOLS

  literal                 匹配字符串的值   

  re1|re2                匹配正则表达式   

  .                   匹配任何字符(换行符除外)   

  ^                   匹配字符串的开始   

  $                    匹配字符串的结尾   

  *                    匹配前面出现的正则表达式0次或多次

  +                  匹配前面出现的正则表达式1次或多次

  ?                匹配前面出现的正则表达式0次或1次

  {N}                  匹配前面出现过得正则表达式n次

  {M,N}              匹配重复出现M到N次的正则表达式

  [.....]                匹配字符组里出现的任一字符

  [..x-y..]                匹配从字符x到y的任一字符

  [^...]                不匹配此字符集里的任一字符包括某一范围内的字符

  |*|+|?|              用于上面出现的任一“非贪婪”版本重复匹配次数(*+?{})

  {...}                匹配封闭括号中的正则表达式,并保存为子组

  \d                    匹配任何数字

  \w                     匹配任何数字字母字符

  \s                    匹配任何空白符

  \b                     匹配单词边界

  \nn                    匹配以保存的子组

  \c                    逐一匹配特殊字符

  \A(\Z)                  匹配字符串的起始(结束)

 

2.2.1 用管道符号( | )匹配多个正则表达式模式
  管道符号( | ), 就是您键盘上的竖杠,表示一个或操作,它的意思是选择被管道符号分隔的多个不同的正则表达式中的一个。例如,下面的一些使用或操作的模式,和它们所匹配的字符串:

 

  正则表达式模式          匹配的字符串

  at|home              at,home

  r1|r2                r1,r2

 

15.2.2 匹配任意一个单个的字符( . )
  点字符或句号(.)符号匹配除换行符(NEWLINE)外的任意一个单个字符(Python 的正则表达式有一个编译标识 [S or DOTALL],该标识能 去掉 这一限制,使 ( . ) 在匹配时包括换行符(NEWLINEs)。)(这里括号缺一半) 无论是字母、数字、不包括“\n”的空白符、可打印的字符、还是非打印字符,或是一个符号,“点”,( . )都可以匹配他们。


    正表达式模式         匹配的字符串
      f.o             在"f"和"o"中间的任何字符,如fao, f9o, f#o 等
      ..              任意两个字符
      .end             匹配在字符串end 前面的任意一个字符


问:我怎样才能匹配点号(dot)或句号(period)?
答:为了明确地匹配一个点号(dot)本身,你必须(在前面)使用反斜线“\”对它进行转义。


15.2.4 从字符串的开头或结尾或单词边界开始匹配( ^/$ /\b /\B )
  还有些符号和特殊字符是用来从字符串的开头或结尾开始搜索正则表达式模式的。如果想从字符串的开头开始匹配一个模式,你必须用脱字符号( ^ , 即,Caret)或特殊字符 \A (大写字母A 前面加上一个反斜线). 后者主要是为那些没有caret 符号的键盘使用的,比如说国际键盘。类似,美元符号 ( $ ) 或 \Z 是用来(零宽度)匹配字符串的结尾的。用这些符号的模式与我们将在本章讲述的其它大多数符号是不同的,因为这些符号指定了(匹配字符)的位置。在上面的核心笔记里,我们曾说过 “matching”和“searching” 之间的区别,“matching”是试图从整个字符串的开头进行匹配,而 “searching” 则可从一个字符串的任意位置开始匹配。正因为这几个字符和搜索的位置有关,所以需要和搜索模式一起使用。下面是几个“擦边球”的正则表达式搜索模式:

 

正则表达式模式         匹配的字符串
^From             匹配任何以From 开始的字符串
/bin/tcsh$           匹配任何以 /bin/tcsh 结束的字符串
^Subject: hi$         匹配仅由 Subject: hi 组成的字符串
特别说明,如果你想匹配这两个字符中的任何一个(或全部),就必须用反斜线进行转义。例如,如果你想匹配任何以 美元符号($) 结尾的字符串,一个可行的解决办法是用正则表达式模式
“.*\$$”.特殊字符 \b and \B 用来匹配单词边界。两者之间的区别是,\b 匹配的模式是一个单词边界,就是说,与之对应的模式一定在一个单词的开头,不论这个单词的前面是有字符(该词在一个字符串的中间),还是没有字符(该单词在一行的起始处)。同样地,\B 只匹配出现在一个单词中间的模式(即,不在单词边界上的字符)。看下面几个例子:


RE Pattern       Strings Matched
the           任何包含有"the"的字符串
\bthe           任何以"the"开始的字符串
\bthe\b         仅匹配单词 “the”
\Bthe         任意包含“the”但不以“the”开头的单词

 

15.2.5 创建字符类( [ ] )
尽管点号可用来匹配任意字符,但有时候你需要匹配某些个特殊的字符。正因为如此,方括号( [ ] )被发明出来。使用方括号的正则表达式会匹配方括号里的任何一个字符。几个例子如下:
正则表达式模式       匹配的字符串

b[aeiu]t         bat, bet, bit, but
[cr][23][dp][o2]     一个包含4 个字符的字符串: 第一个字符是 “r” 或 “c”,后面是 “2”或 “3”,再接下来是 “d” 或 “p”,最后是 “o” 或 “2“ ,例如:c2do, r3p2, r2d2, c3po, 等等。


关于正则表达式 “[cr][23][dp][o2]” 的一点要说明: 如果只让 “r2d2” 或 “c3po” 成为有效的字符串,就需要限定更为严格的正则表达式。但因为方括号只有"逻辑或"(“logical OR”)
的功能,所以用方括号不能实现这一限定要求。唯一的解决办法是用管道符号(pipe), 例如:

“r2d2|c3po”.
  对仅有单个字符的正则表达式 ,使用管道符号和方括号的效果是等价的。举例来说,正则表达式“ab” , 只匹配以"a"开头后面再跟一个"b"的字符串。如果我们只想要一个字母的字符串,即,
“a” 或者 “b” 中的一个,就可以使用正则表达式 “[ab]” 。因为 “a” 和 “b” 是单个的字符串,我们也可以用正则表达式 “a|b”。但是,如果我们想用模式匹配"ab",后面接着是"cd"的字符串,就不能用方括号了,因为方括号只适用于单个字符的情况。这样,唯一的办法是用“ab|cd”,这和我们刚才提到的 “r2d2|c3po”的道理是相同的。

 

2.2.5 指定范围 ( - ) 和 否定( ^ )
  方括号除匹配单个字符外,还可以支持所指定的字符范围。方括号里一对符号中间的连字符(-)用来表示一个字符的范围,例如,A–Z, a–z, 或 0–9 分别代表大写字母、小写字母和十进制数
字。这是一个按字母顺序排序的范围,所以它不限于只用在字母和十进制数字上。另外,如果在左方括号后第一个字符是上箭头符号(^),就表示不匹配指定字符集里的任意字符。

正则表达式模式           匹配的字符
z.[0-9]          字符"z",后面跟任意一个字符,然后是一个十进制数字
[r-u][env-y][us]       “r” “s,” “t” 或 “u” 中的任意一个字符,后面跟的是 “e,”“n,” “v,” “w,” “x,” 或 “y”中的任意一个字符,再后面是字符“u” 或 “s”.
[^aeiou]         一个非元音字符 (练习: 为什么我们说”非元音“, 而不说”辅音字母“?)
[^\t\n]           除TAB 制表符和换行符以外的任意一个字符
["-a]             在使用ASCII 字符集的系统中,顺序值在‘"‘ 和 “a”之间 的任意一个字符,即,顺序号在34 和97 之间的某一个字符。

 

 


15.2.6 使用闭包操作符 ( *, +, ?, {} ) 实现多次出现/重复匹配


现在我们来介绍最常用的正则表达式符号,即,特殊符号 “*”, “+”, 和 “?”, 它们可以用于匹配字符串模式出现一次、多次、或未出现的情况。星号或称星号操作符匹配它左边那个正则
表达式出现零次或零次以上的情况(在计算机语言和编译器原理里,此操作符被叫做 Kleene 闭包操作符)。加号(+)操作符匹配它左边那个正则表达式模式至少出现一次的情况(它也被称为正闭包操作符),而问号操作符( ? )匹配它左边那个正则表达式模式出现零次或一次的情况。还有花括号操作符({ }), 花括号里可以是单个的值,也可以是由逗号分开的一对值。如果是
一个值,如,{N},则表示匹配N 次出现;如果是一对值,即,{M, N},就表示匹配M 次到N 次出现。可以在这些符号前用反斜线进行转义,使它们失去特殊作用,即, “\*” 将匹配星号本身等。在上表中,我们注意到问号出现了不只一次(被重载),问号有两种含义:1.单独使用时表示匹配出现零次或一次的情况,2.紧跟在表示重复的元字符后面时,表示要求搜索引擎匹配的字符串越短
越好。例如:(+?)

前面提到"越短越好..."是什么意思呢?当使用了表示重复的元字符(*+?{m,n})时,正则表达式引擎在匹配模式时会尽量"吸收"更多的字符。这就叫做"贪心"。问号告诉正则表达式引擎尽可能地
偷懒,要求当前匹配消耗的字符越少越好,留下尽可能多的字符给后面的模式(如果存在)。 - 在本章末尾,我们举一个有代表性的例子来说明必须使用非贪心模式的情况。
现在,让我们接着来看一些使用闭包操作符的例子:
RE Pattern                   Strings Matched
[dn]ot?                     字符"d"或"o", 后面是一个"o", 最后是最多一个字符"t",即,do, no, dot,not
0?[1-9]                  从1 到9 中的任意一位数字,前面可能还有一个"0". 例如:可以把它看成一月到九月的数字表示形式,不管是一位数字还是两位数字的表示形式。
[0-9]{15,16}             15 或16 位数字表示,例如:信用卡号码
</?[^>]+>                 匹配所有合法(和无效的)HTML 标签的字符串
[KQRBNP][a-h][1-8]-[a-h][1-8]        在“长代数”记谱法中,表示的国际象棋合法的棋盘。即, “K,” “Q,” “R,” “B,” “N,” 或 “P” 等字母后面加上两个用                   连                       起"a1"到"h8"之间的棋盘坐标。前面的编号表示从哪里开始走棋,后面的编号代表走到哪个位置(棋格)去。

 

 

2.2.8 用圆括号(()) 组建组

  
现在,或许我们可以匹配一个字符串和丢弃那些不匹配的字符串了,但有时候,我们也许对匹配的数据本身更有兴趣。我们不仅想知道是否整个字符串匹配我们的条件(正则表达式),还想在匹
配成功时取出某个特定的字符串或子字符串。要达到这个目的,只需要给正则表达式的两边加上一对圆括号。


  一对圆括号(()) 和正则表达式一起使用时可以实现以下任意一个(或两个)功能:
  􀁺 对正则表达式进行分组
  􀁺 匹配子组
  有时你需要对正则表达式进行分组,其中一个很好的例子就是,你要用两个不同的正则表达式去比较一个字符串。另一个理由是为整个正则表达式添加一个重复操作符(即不是仅重复单个字符或单一字符集)。使用圆括号的一个额外好处就是匹配的子串会被保存到一个子组,便于今后使用。这些子组可以在同一次匹配或搜索中被重复调用,或被提取出来做进一步处理。在小节15.3.9 的结尾你会读到一些提取子组的例子。
  为什么需要使用子组匹配呢? 主要是有时除了进行匹配操作外,你还想要提取匹配模式的内容。如果想知道在成功的匹配中,是哪些字符串匹配了我们的正则表达式模式。例如,我们想用正则表达式“\w+-\d+”匹配一些内容,但又想把第一部分的字符和第二部分的数字分别保存,该怎么做呢?如果我们给两个子模式都加上圆括号,即,将它写成 “(\w+)-(\d+)” , 那我们就可以对这
两个匹配的子组分别进行访问了。当然你也可以使用其他方法达到同样目的,比如,先写一段代码判断是否找到匹配的对象,然后再执行另一个程式(也必须再写一段代码)来解析整个匹配的部分,从
中提取出两个部分来。然而相比之下把正则表达式划分为子组是更好的实现办法,因为Python 已经在re 模块里支持此功能,那为什么不让Python 来做这项工作,而非要重复发明一个轮子呢?
正则表达式模式           匹配的字符串
\d+(\.\d*)?           表示简单的浮点数,即, 任意个十进制数字,后面跟一个可选的小数点,然后再接零或多个十进制数字。例如:“0.004,” “2,” “75.”,等等。
(Mr?s?\. )?[A-Z][a-z]* [ A-Za-z-]+     名字和姓氏,对名字的限制(首字母大写,其它字母(如果存在)小写), 全名前有可选的称谓(“Mr.,”“Mrs.,” “Ms.,” 或 “M.,”),姓氏没有什么
                       限制,允许有多个单词、横线、大写字母。

2.3 正则表达式和Python 语言
    既然我们已知道了有关正则表达式本身的所有知识,那让我们来详细研究当前Python 的默认正则表达式模块 re 模块吧. re 模块在Python1.5 版本被引入。如果你正在使用Python 的早期版本,你将只能用已过时的regex、regsub 模块。这些模块具有Emacs 风格,功能不丰富,而且与现在的re 模块也不兼容。regex 和regsub 这两个模块已在Python 2.5 版本时被移除了,在 Python2.5 及其后续版本,引入这两个模块中的任何一个将会引发Import Error 异常。但正则表达式本身是不变的,所以本小节中的大多数基本概念仍然适用于旧版的regex 和reg-sub 模块。与旧模块形成鲜明对比的是,新的re 模块支持功能更强大、更通用的Perl 风格(具体说是Perl5 的风格)的正则表达式,允许多线程共享同一经过编译的正则表达式对象,同时它还支持对正则表达式分组进行命名和按名字调用。另外, 有一个名叫reconvert 的转换模块是帮助开发者从regex/regsub 模块 迁移到re 模块的。但请注意,正则表达式有不同的风格,我们主要研究当今python 语言中使用的正则表达式。re 引擎已在Python1.6 版本中被重写,改进了它的性能并添加了对Unicode 的支持。接口并没有改变,因此模块的名字也保持不变。新的re 引擎,内部被叫做sre, 替代了1.5 版本中内部名为pcre 的re 引擎。

 

2.3.1 re 模块: 核心函数和方法
表15.2 列出了re 模块最常用的函数和方法。其中有很多函数也与已编译的正则表达式对象(regex objects) 和正则"匹配对象"(RE “match objects”)的方法同名并且具有相同功能。
在本小节,我们来看两个主要的函数/方法,match() 和 search(), 以及compile()函数。在下一节我们还会再介绍更多个,但如果想进一步了解我们涉及或没有涉及的更多相关信息,我们建议
你参阅Python 的文档。
表15.2 常见的正则表达式函数与方法
函数/方法               描述
re                 模块的函数
compile(pattern,flags=0)       对正则表达式模式pattern 进行编译,flags 是可选标志符,并返回一个regex 对象
re                     模块的函数和regex 对象的方法
match(pattern,string, flags=0) 尝    试用正则表达式模式pattern 匹配字符串string,

flags                 是可选标志符,如果匹配成功,则返回一个匹配对象;否则返回None
search(pattern,string, flags=0)       在字符串string 中查找正则表达式模式pattern 的第一次出现,flags 是可选标志符,如果匹配成功,则返回一个匹配对象;否则返回None
findall(pattern,string[,flags])a      在字符串string 中查找正则表达式模式pattern 的所有(非重复)出现;返回一个匹配对象的列表
finditer(pattern,string[, flags])b       和findall()相同,但返回的不是列表而是迭代器;对于每个匹配,该迭代器返回一个匹配对象表 15.3 常见的正则表达式函数与方法(继续)
    函数/方法     描述
    匹配对象的方法
split(pattern,string, max=0)       根据正则表达式pattern 中的分隔符把字符string 分割为一个列表,返回成功匹配的列表,最多分割max 次(默认是分割所有匹配的地方)。
sub(pattern, repl, string, max=0)     把字符串string 中所有匹配正则表达式pattern 的地方替换成字符串repl,如果max 的值没有给出,则对所有匹配的地方进行替换(另外,请参考subn(),                它还会返回一个表示替换次数的数值)。
group(num=0)            返回全部匹配对象(或指定编号是num 的子组)
groups()               返回一个包含全部匹配的子组的元组(如果没有成功匹配,就返回一个空元
组)
a. Python 1.5.2 中新增; 2.4 中增加标识参数
b. Python 2.2 新增;2.4 中增加标识参数


核心笔记: RE 编译(何时应该使用compile 函数?)
在第十四章,我们曾说过Python 的代码最终会被编译为字节码,然后才被解释器执行。我们特别提到用调用eval() 或 exec()调用一个代码对象而不是一个字符串,在性能上会有明显地提升,
这是因为对前者来说, 编译过程不必执行。换句话说,使用预编译代码对象要比使用字符串快,因为解释器在执行字符串形式的代码前必须先把它编译成代码对象。这个概念也适用于正则表达式,在模式匹配之前,正则表达式模式必须先被编译成regex 对象。由于正则表达式在执行过程中被多次用于比较,我们强烈建议先对它做预编译,而且,既然正则表达式的编译是必须的,那使用么预先编译来提升执行性能无疑是明智之举。re.compile() 就是用来提供此功能的。其实模块函数会对已编译对象进行缓存,所以不是所有使用相同正则表达式模式的search()和
match()都需要编译。即使这样,你仍然节省了查询缓存,和用相同的字符串反复调用函数的性能开。 在Python1.5.2 版本里, 缓存区可以容纳20 个已编译的正则表达式对象,而在1.6 版本里,由于另外添加了对Unicode 的支持,编译引擎的速度变慢了一些,所以缓存区被扩展到可以容纳100个已编译的regex 对象。

2.3.2 使用compile()编译正则表达式

我们稍后要讲到的大多数re 模块函数都可以作为regex 对象的方法。注意,尽管我们建议预编译,但它并不是必需的。如果你需要编译,就用方法,如果不需要,可以使用函数。幸运的是无论
你用哪种方式-函数还是方法,名字都是相同的。(也许你曾对此好奇,这正是模块函数和方法完全一样的原因,例如,search(), match()等等) 在后面的例子里,我们将用字符串,这样可以省去一个小步骤。我们仍会用到几个预编译代码对象,这样你可以知道它的过程是怎么回事。编译rex 对象时给出一些可选标志符,可以得到特殊的编译对象。这些对象将允许不区别大小
写的匹配,或使用系统的本地设置定义的字母表进行匹配等。详情请参阅有关文档。这些标志符也可以做为参数传给模块 (改字) 版本的match()和search()进行特定模式的匹配,其中一些标志符
已在前面做过简短介绍(例如,DOTALL,LOCALE) - 这些标志符多数用于编译,也正因如此它们可以被传给模块版本的match()和search(),而match()和search()肯定要对正则表达式模式编译一次。如果你想在regex 对象的方法中使用这些标志符,则必须在编译对象时传递这些参数。除下面的方法外,regex 对象还有一些数据属性,其中两个是创建时给定的编译标志符和正则
表达式模式。

 


2.3.3 匹配对象 和 group(), groups() 方法
在处理正则表达式时,除regex 对象外,还有另一种对象类型 - 匹配对象。这些对象是在match()或search()被成功调用之后所返回的结果。匹配对象有两个主要方法:group() 和 groups().
group()方法或者返回所有匹配对象或是根据要求返回某个特定子组。groups()则很简单,它返回一个包含唯一或所有子组的元组。如果正则表达式中没有子组的话, groups() 将返回一个空元
组,而group()仍会返回全部匹配对象。Python 语言中的正则表达式支持对匹配对象进行命名的功能,这部分内容超出了本介绍性小节对正则表达式的讨论范围。我们建议你阅读re 模块的文档,里面有我们省略掉的关于这些高级主题的详细内容。


15.3.4 用match()匹配字符串

我们先来研究re 模块的函数、正则表达式对象(regex object)的方法: match(). match()函数尝试从字符串的开头开始对模式进行匹配。如果匹配成功,就返回一个匹配对象,而如果匹配失
败了,就返回None。匹配对象的group() 方法可以用来显示那个成功的匹配。下面是如何运用match()[及 group()]的一个例子:
>>> m = re.match('foo', 'foo') # pattern matches string ,模式匹配字符串
>>> if m is not None: # show match if successful 如果成功,显示匹配
... m.group()
...
'foo'
模式"foo"完全匹配字符串"foo"。在交互解析器中,我们能确定 m 就是一个匹配对象的实例。
>>> m # confirm match object returned 确定返回匹配对象
<re.MatchObject instance at 80ebf48>
这是当匹配失败时的例子,它返回None:
>>> m = re.match('foo', 'bar')# pattern does not match string 模式不匹配字符串
>>> if m is not None: m.group()# (1-line version of if clause) 一行的if 子句
...
>>>
上面的匹配失败,所以m 被赋值为None,因为我们写的if 语句中没有什么行动,所以也没有什么指令动作被执行。在以后的例子中,为了简洁,在可能的情况下,我们会省去if 检查语句,但在
实际编程中,最好写上它,以防止出现AttributeError 异常(失败后返回None, 此时它是没有group()属性[方法]的)。
即使字符串比模式要长,匹配也可能成功;只要模式是从字符串的开始进行匹配的。例如,模式"foo" 在字符串“food on the table”中找到一个匹配,因为它是从该字符串开头进行匹配的:
>>> m = re.match('foo', 'food on the table') # match succeeds # 匹配成功
>>> m.group()
'foo'

如你看到的,尽管字符串比模式要长,但从字符串开头有一个成功的匹配。 子串 "foo"是从那个较长的字符串中抽取出来的匹配部分。

 

我们甚至可以充分利用Python 语言面向对象的特性,间接省略中间结果,将最终结果保存到一
起:
>>> re.match('foo', 'food on the table').group()
'foo'
注意,上面的例子中,如果匹配失败,会引发一个AttributeError 异常.
2.3.5 search() 在一个字符串中查找一个模式 (搜索与匹配的比较)
其实,你要搜索的模式出现在一个字符串中间的机率要比出现在字符串开头的机率更大一些。这正是search()派上用场的时候。search 和match 的工作方式一样,不同之处在于search 会检查
参数字符串任意位置的地方给定正则表达式模式的匹配情况。如果搜索到成功的匹配,会返回一个匹配对象,否则返回None。
现在我们来举例说明match()和search()之间的区别。让我们举一个对长字符串进行匹配的例子。这次,我们用字符串"foo"去匹配“seafood”:
>>> m = re.match('foo', 'seafood') # no match 匹配失败
>>> if m is not None: m.group()
...
>>>
如你所见,这里没有匹配成功。match()尝试从字符串起始处进行匹配模式,即,模式中的"f"试匹配到字符串中首字母"s"上, 这样匹配肯定是失败的。但字符串"foo" 确实出现在“seafood”
中,那我们如何才能让Python 得出肯定的结果呢? 答案是用search()函数。search() 查找字符串中模式首次出现的位置,而不是尝试(在起始处)匹配。严格地说,search() 是从左到右进行搜索。
>>> m = re.search('foo', 'seafood') # use search() instead 改用search()
>>> if m is not None: m.group()
...
'foo' # search succeeds where match failed 用search 成功匹配,用match 匹配失败

>>>

在本小节以后的内容里,将通过大量的例子展示如何在Python 语言中运用正则表达式,我们会用到regex 对象的方法match()和search(),匹配对象的方法group()、groups(),以及正则表达式
语法中的绝大多数特殊字符和符号。

15.3.6 匹配多个字符串( | )
在15.2 小节里,我们在正则表达式“bat|bet|bit”中使用了管道符号. 下面,我们把这个正则表达式用到Python 的代码里:
>>> bt = 'bat|bet|bit' # RE pattern: bat, bet, bit #正则表达式模式: bat, bet, bit
>>> m = re.match(bt, 'bat') # 'bat' is a match #'bat' 是匹配的
>>> if m is not None: m.group()
...
'bat'
>>> m = re.match(bt, 'blt') # no match for 'blt' #没有匹配'blt'的模式
>>> if m is not None: m.group()
...
>>> m = re.match(bt, 'He bit me!')# does not match string #不匹配字符串
>>> if m is not None: m.group()
...
>>> m = re.search(bt, 'He bit me!')# found 'bit' via search #搜索到'bit'
>>> if m is not None: m.group()
...
'bit'
2.3.7 匹配任意单个字符( . )
以下的例子中,我们将说明点号是不能匹配换行符或非字符(即,空字符串)的:
>>> anyend = '.end'
>>> m = re.match(anyend, 'bend') # dot matches 'b' #点号匹配'b'
>>> if m is not None: m.group()
...
'bend'
>>> m = re.match(anyend, 'end') # no char to match #没有字符匹配
>>> if m is not None: m.group()
...
>>> m = re.match(anyend, '\nend') # any char except \n #匹配字符(\n 除外)
>>> if m is not None: m.group()
...
>>> m = re.search('.end', 'The end.')# matches ' ' in search . #匹配' '

 

>>> if m is not None: m.group()
...
' end'
下面的例子是来搜索一个真正点号(小数点)的正则表达式,在正则表达式中,用反斜线对它进
行转义,使点号失去它的特殊意义:
>>> patt314 = '3.14' # RE dot #正则表达式点号
>>> pi_patt = '3\.14' # literal dot (dec. point) #浮点(小数点)
>>> m = re.match(pi_patt, '3.14') # exact match #完全匹配
>>> if m is not None: m.group()
...
'3.14'
>>> m = re.match(patt314, '3014') # dot matches '0' #点号匹配 '0'
>>> if m is not None: m.group()
...
'3014'
>>> m = re.match(patt314, '3.14') # dot matches '.' #点号匹配 '.'
>>> if m is not None: m.group()
...
'3.14'
15.3.8 创建字符集合( [ ] )
前面,我们曾讨论过 “[cr][23][dp][o2]”和“r2d2|c3po”是不同的。 从下面的例子中,可以看出“r2d2|c3po” 与“[cr][23][dp][o2]” 相比有更加严格的限制:
>>> m = re.match('[cr][23][dp][o2]', 'c3po')# matches 'c3po' #匹配'c3po'
>>> if m is not None: m.group()
...
'c3po'
>>> m = re.match('[cr][23][dp][o2]', 'c2do')# matches 'c2do' #匹配'c2do'
>>> if m is not None: m.group()
...
'c2do'
>>> m = re.match('r2d2|c3po', 'c2do')# does not match 'c2do' #不匹配'c2do'
>>> if m is not None: m.group()
...
>>> m = re.match('r2d2|c3po', 'r2d2')# matches 'r2d2' #匹配'r2d2'

 

>>> if m is not None: m.group()
...
'r2d2'
2.3.9 重复、特殊字符和子组
正则表达式中最常见的情况包括特殊字符的使用,正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。我们曾看到过一个关于简单电子邮件地址的正则表达
式(“\w+@\w+\.com”). 或许我们想要匹配的邮件地址比这个正则表达式的允许的要多。比如,为了在域名前添加主机名称支持,即, 支持“www.xxx.com”,而不只是允许“xxx.com”做整个域名,我们就必须修改现有的正则表达式。为了表示主机名是可选的,我们要写一个模式匹配主机名(后面跟一个点号),然后用问号“?”表示此模式可出现0 次或1 次,表示此部分是可选的,再把这个可选的正则表达式插入到我们前面的那个正则表达式中去:“\w+@(\w+\.)?\w+\.com” 。 从下面的例子中可以看出,这个表达式容许“.com”前面有一个或两个名字:
>>> patt = '\w+@(\w+\.)?\w+\.com'
>>> re.match(patt, 'nobody@xxx.com').group()
'nobody@xxx.com'
>>> re.match(patt, 'nobody@www.xxx.com').group()
'nobody@www.xxx.com'

接下来,我们用以下模式进一步扩展我们的例子,允许任意数量的子域名存在。请特别注意细节的变化,将 ? 改为 *:“\w+@(\w+\.)*\w+\.com”:

>> patt = '\w+@(\w+\.)*\w+\.com'
>>> re.match(patt, 'nobody@www.xxx.yyy.zzz.com').group()
'nobody@www.xxx.yyy.zzz.com'
但我们必须要说明的是仅用字母或数字组成的字符不能满足邮件地址中可能出现的各种字符。上述正则表达式不匹配如“xxx-yyy.com” 这样的域名或其他带有非单词字符(如“\W”等)的域名。
前面,我们曾讨论过用括号匹配并保存子组做进一步处理的好处,这样做比在确定正则表达式匹配后,再单写一个子程序来解析一个字符串要好。我们还特别提到用来匹配 以"-"分隔的字母或
数字组成的字符串和数字串的正则表达式 “\w+-\d+” ,以及如何通过对此正则表达式划分子组以构建一个新的正则表达式, “(\w+)-(\d+)” 来完成任务,下面是旧版正则表达式的执行情况:
>>> m = re.match('\w\w\w-\d\d\d', 'abc-123')
>>> if m is not None: m.group()

 

 

'abc-123'
>>> m = re.match('\w\w\w-\d\d\d', 'abc-xyz')
>>> if m is not None: m.group()
...
>>>
上面的代码中,一个正则表达式被用来匹配由三个字母或数字组成的字符串,再接着三个数字的字符串。这个正则表达式匹配“abc-123”,但不匹配“abc-xyz”。我们现在来修改正则表达式,使
它能分别提取包含字母或数字的部分和仅含数字的部分。请注意我们是如何用group()方法访问每个子组以及用groups()方法获取一个包含所有匹配子组的元组的:
>>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
>>> m.group() # entire match 所有匹配部分
'abc-123'
>>> m.group(1) # subgroup 1 匹配的子组1
'abc'
>>> m.group(2) # subgroup 2 匹配的子组2
'123'
>>> m.groups() # all subgroups 所有匹配子组
('abc', '123')
如你所见,group()通常用来显示所有匹配部分,也可用来获取个别匹配的子组。我们可用groups()方法获得一个包含所有匹配子组的元组。下面这个简单的例子通过子组的不同排列组合,帮助我们理解得更透彻:
>>> m = re.match('ab', 'ab') # no subgroups #无子组
>>> m.group() # entire match
'ab'
>>> m.groups() # all subgroups #所有匹配的子组
()
>>>
>>> m = re.match('(ab)', 'ab') # one subgroup #一个子组
>>> m.group() # entire match #所有匹配
'ab'
>>> m.group(1) # subgroup 1 #匹配的子组1
'ab'
>>> m.groups() # all subgroups #所有匹配子组

('ab',)
>>>
>>> m = re.match('(a)(b)', 'ab') # two subgroups #两个子组
>>> m.group() # entire match
'ab'
>>> m.group(1) # subgroup 1 匹配的子组1
'a'
>>> m.group(2) # subgroup 2 匹配的子组2
'b'
>>> m.groups() # all subgroups 所有匹配子组的元组
('a', 'b')
>>>
>>> m = re.match('(a(b))', 'ab') # two subgroups #两个子组
>>> m.group() # entire match #所有匹配部分
'ab'
>>> m.group(1) # subgroup 1 #匹配的子组1
'ab'
>>> m.group(2) # subgroup 2 #匹配的子组2
'b'
>>> m.groups() # all subgroups #所有匹配的子组的元组
('ab', 'b')
2.3.10 从字符串的开头或结尾匹配及在单词边界上的匹配
下面的例子强调了锚点性正则表达式操作符。这些锚点性正则表达式操作符主要被用于搜索而不是匹配,因为match()总是从字符串的开头进行匹配的。
>>> m = re.search('^The', 'The end.') # match #匹配
>>> if m is not None: m.group()
...
'The'
>>> m = re.search('^The', 'end. The') # not at beginning #不在开头
>>> if m is not None: m.group()
...
>>> m = re.search(r'\bthe', 'bite the dog') # at a boundary #在词边界
>>> if m is not None: m.group()
...
'the'
>>> m = re.search(r'\bthe', 'bitethe dog') # no boundary #无边界

 

>>> if m is not None: m.group()
...
>>> m = re.search(r'\Bthe', 'bitethe dog') # no boundary #无边界
>>> if m is not None: m.group()
...
'the'
你可能在这里注意到了原始字符串(raw strings) 的出现。在本章末尾的核心笔记中,有关于它的说明。通常,在正则表达式中使用原始字符串是个好主意。你还应该了解另外四个re 模块函数和regex 对象方法: findall(), sub(), subn() 和 split().
2.3.11 用findall()找到每个出现的匹配部
findall()自Python1.5.2 版本被引入。它用于非重叠地查找某字符串中一个正则表达式模式出现的情况。findall()和search()相似之处在于二者都执行字符串搜索,但findall()和match()与
search()不同之处是,findall()总返回一个列表。如果findall()没有找到匹配的部分,会返回空列表;如果成功找到匹配部分,则返回所有匹配部分的列表(按从左到右出现的顺序排列)。
>>> re.findall('car', 'car')
['car']
>>> re.findall('car', 'scary')
['car']
>>> re.findall('car', 'carry the barcardi to the car')
['car', 'car', 'car']
包含子组的搜索会返回更复杂的一个列表,这样做是有意义的,因为子组是允许你从单个正则表达式中抽取特定模式的一种机制,比如,匹配一个完整电话号码中的一部分(例如,区号),或完
整电子邮件地址的一部分(例如,登录名)。正则表达式仅有一个子组时,findall()返回子组匹配的字符串组成的列表;如果表达式有多个子组,返回的结果是一个元组的列表,元组中每个元素都是一个子组的匹配内容,像这样的元组(每一个成功的匹配对应一个元组)构成了返回列表中的元素。这些内容初次听到可能令人费解,但如果
你看看各种例子,就会明白了。
2.3.12 用sub()[和 subn()]进行搜索和替换
有两种函数/方法用于完成搜索和代替的功能: sub()和subn(). 二者几乎是一样的,都是将某字符串中所有匹配正则表达式模式的部分进行替换。用来替换的部分通常是一个字符串,但也可能

是一个函数,该函数返回一个用来替换的字符串。subn()和sub()一样,但它还返回一个表示替换次数的数字,替换后的字符串和表示替换次数的数字作为一个元组的元素返回。
>>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
'attn: Mr. Smith\012\012Dear Mr. Smith,\012'
>>>
>>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
('attn: Mr. Smith\012\012Dear Mr. Smith,\012', 2)
>>>
>>> print re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
attn: Mr. Smith
Dear Mr. Smith,
>>> re.sub('[ae]', 'X', 'abcdef')
'XbcdXf'
>>> re.subn('[ae]', 'X', 'abcdef')
('XbcdXf', 2)
2.3.13 用split()分割(分隔模式)
re 模块和正则表达式对象的方法split()与字符串的split()方法相似,前者是根据正则表达式模式分隔字符串,后者是根据固定的字符串分割,因此与后者相比,显著提升了字符分割的能力。如
果你不想在每个模式匹配的地方都分割字符串,你可以通过设定一个值参数(非零)来指定分割的最大次数。
如果分隔符没有使用由特殊符号表示的正则表达式来匹配多个模式,那re.split()和string.split()的执行过程是一样的,见以下的例子(在每一个冒号处分隔):
>>> re.split(':', 'str1:str2:str3')
['str1', 'str2', 'str3']
但运用正则表达式后,我们会发现re.split()成了一个功能更强大的工具。比如,Unix 系统下who 命令输出所有已登录系统的用户的信息:
% who
wesc console Jun 20 20:33

wesc pts/9 Jun 22 01:38(192.168.0.6)
wesc pts/1 Jun 20 20:33(:0.0)
wesc pts/2 Jun 20 20:33(:0.0)
wesc pts/4 Jun 20 20:33(:0.0)
wesc pts/3 Jun 20 20:33(:0.0)
wesc pts/5 Jun 20 20:33(:0.0)
wesc pts/6 Jun 20 20:33(:0.0)
wesc pts/7 Jun 20 20:33(:0.0)
wesc pts/8 Jun 20 20:33(:0.0)
假如我们想要保存用户的登录信息,比如说,登录名,用户登录时的电传,他们的登录的时间以及登录地址。用上面的string.split()很难有效果,因为分隔这些数据的空白符号是毫无规律且
不确定的。还有一个问题,就是在登录时间的数据中,月,日,时之间有一个空格。而我们一般想把这些有关时间的数据排在一起。你需要用某种方式来描述这样一种模式:“在两个或更多个空格符处进行分隔”。正则表达式很容易做到这一点。我们能很快写出这个正则表达式模式:“\s\s+”,含义是至少2 个空白字符。我们来写一个名为rewho.py 的程序,它读入who 命令的输出 - 假设已保存到名为whodata.txt 的文件中。起初,我们写的rewho.py 脚本看起来像这样:
import re
f = open('whodata.txt', 'r')
for eachLine in f.readlines():
print re.split('\s\s+', eachLine)
f.close()
我们现在执行who 命令,将输出结果保存到文件whodata.txt,然后调用rewho.py 来看看结果:
% who > whodata.txt
% rewho.py
['wesc', 'console', 'Jun 20 20:33\012']
['wesc', 'pts/9', 'Jun 22 01:38\011(192.168.0.6)\012']
['wesc', 'pts/1', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/2', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/4', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/3', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/5', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/6', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/7', 'Jun 20 20:33\011(:0.0)\012']
['wesc', 'pts/8', 'Jun 20 20:33\011(:0.0)\012']

 

 

 

这是不错的尝试,但还不完全正确。首先,我们原先没有预料到输出中会包含一个TAB 符号(ASCII\011) (它看上去像是至少两个空格,对不?)。而且, 我们可能对保存用来结束每行的换行符NEWLINE(ASCII \012) 也没什么兴趣。我们现在就做些改动来修正这些问题,同时提升程序的整体质量。首先,我们改从脚本里执行who 命令,而不是从外部调用它后将命令的输出结果保存到文件whodata.txt - 这样重复的步骤很快会令人厌烦的。要从我们写的脚本里调用另一个程序,可以用os.popen()命令,这个命令在14.5.2 小节已介绍过。尽管 os.popen()只能在Unix 系统中使用,但本例子意在阐明re.split()的用法,它可是跨系统平台的。我们去掉每行行尾的换行符(NEWLINE),并添加检查单个TAB 符号的模式,把TAB 做为re.split()
的可选分隔符。例15.1,是脚本rewho.py 的最终版本:
例 15.1 Unix 下who 命令输出结果进行分隔 (rewho.py)
此脚本调用who 命令,解析命令的输出结果,根据不同的空白符号分隔数据。
1 #!/usr/bin/env python
2
3 from os import popen
4 from re import split
5
6 f = popen('who', 'r')
7 for eachLine in f.readlines():
8 print split('\s\s+|\t', eachLine.strip())
9 f.close()
f.readlines() can be shortened to the file iterator f.
注:f.readlines()可以被简写成文件的迭代器f.
运行脚本,我们得到如下(正确)结果:
% rewho.py
['wesc', 'console', 'Jun 20 20:33']
['wesc', 'pts/9', 'Jun 22 01:38', '(192.168.0.6)']
['wesc', 'pts/1', 'Jun 20 20:33', '(:0.0)']
['wesc', 'pts/2', 'Jun 20 20:33', '(:0.0)']
['wesc', 'pts/4', 'Jun 20 20:33', '(:0.0)']
['wesc', 'pts/3', 'Jun 20 20:33', '(:0.0)']
['wesc', 'pts/5', 'Jun 20 20:33', '(:0.0)']

 

['wesc', 'pts/6', 'Jun 20 20:33', '(:0.0)']
['wesc', 'pts/7', 'Jun 20 20:33', '(:0.0)']
['wesc', 'pts/8', 'Jun 20 20:33', '(:0.0)']
在DOS/Windows 环境下,用dir 命令代替who 命令,也可完成此练习。
趁我们还熟悉ASCII 字符,我们要提醒注意的是正则表达式的特殊字符和特殊ASCII 字符是容易混淆的。我们可能用\n 来表示一个ASCII 换行字符,但也可以用\d 表示匹配一个数字的正则表达
式。如果同一个符号在ASCII 和正则表达式中都可以用,就容易出问题了,所以在下页的核心笔记中,我们推荐使用Python 语言中的"原始字符串"来避免混淆。还要注意:“\w” and “\W”这两个表示字母或数字的字符受L 或 LOCALE 编译标志符的影响,在Python 1.6 至Python 2.0 以后的版本中受(U 或 UNICODE 的)Unicode 标志符号影响。
核心笔记 : Python 原始字符串(raw strings)的用法你可能已经看到前面关于原始字符串用法的一些例子了。原始字符串的产生正是由于有正则表
达式的存在。原因是ASCII 字符和正则表达式特殊字符间所产生的冲突。比如,特殊符号“\b”在ASCII 字符中代表退格键,但同时“\b”也是一个正则表达式的特殊符号,代表“匹配一个单词边界”。为了让RE 编译器把两个字符“\b”当成你想要表达的字符串,而不是一个退格键,你需要用另一个反斜线对它进行转义,即可以这样写:“\\b”。
但这样做会把问题复杂化,特别是当你的正则表达式字符串里有很多特殊字符时,就更容易令人困惑了。在第六章,我们曾介绍过原始字符串,它经常被用于简化正则表达式的复杂程度。
事实上,很多Python 程序员在定义正则表达式时都只使用原始字符串。下面的例子用来说明退格键“\b” 和正则表达式“\b”(包含或不包含原始字符串)之间的区别:
>>> m = re.match('\bblow', 'blow') # backspace, no match #退格键,没有匹配
>>> if m is not None: m.group()
...
>>> m = re.match('\\bblow', 'blow') # escaped \, now it works #用\转义后,现在匹
配了
>>> if m is not None: m.group()
...
'blow'
>>> m = re.match(r'\bblow', 'blow') # use raw string instead #改用原始字符串
>>> if m is not None: m.group()
...
'blow'
你可能注意到我们在正则表达式里使用“\d”,没用原始字符串,也没出现什么问题。那是因为
ASCII 里没有对应的特殊字符,所以正则表达式编译器能够知道你指的是一个十进制数字。

posted @ 2015-12-11 21:58  Fanyear  阅读(1334)  评论(0编辑  收藏  举报