<> == 9.7 正则表达式与 re 模块 == 一个正则表达式就是一个用来表示某种模式的字符串。它能帮助你方便的检查一个字符串是否与某种模式匹配。 re 模块使 Python 语言拥有全部的正则表达式功能。 compile 函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。该对象拥有一系列方法用于正则表达式匹配和替换。 re 模块也提供了与这些方法功能完全一致的函数,这些函数使用一个模式字符串做为它们的第一个参数。 要精通正则表达式并不容易,而且这本书的主题也不是正则表达式。 本节的目的是教会你在 Python中使用正则表达式。如果要全面的了解正则表达式,我推荐Jeffrey Friedl写的《Mastering Regular Expressions》这本书。这本书全面透彻的讲解了正则表达式的方方面面。 === 9.7.1 模式字符串语法 === 模式字符串使用特殊的语法来表示一个正则表达式: 字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。 多数字母和数字前加一个反斜杠时会拥有不同的含义。 标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。 反斜杠本身需要使用反斜杠转义。 由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。模式元素(如 r'\t',等价于'\\t')匹配相应的特殊字符。 表 9-2 列出了正则表达式模式语法中的特殊元素。如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。本节后面会提到这些可选的标志。 '''表 9-2 正则表达式模式语法''' {{{ 元素 含义 . 匹配除换行外的任意字符(如果 DOTALL 则连换行也匹配) ^ 匹配字符串开始(如果MULTILINE,也匹配换行符之后) $ 匹配字符串结束(如果MULTILINE,也匹配换行符之前) * 匹配0个或更多个由前面的正则表达式定义的片段,贪婪方式(尽可能多的匹配) + 匹配1个或更多个由前面的正则表达式定义的片段,贪婪方式 ? 匹配0个或1个由前面的正则表达式定义的片段,贪婪方式 *? , +?, ?? 非贪婪版本的 *, +, 和 ? (尽可能少的匹配) {m,n} 匹配 m 到 n 次由前面的正则表达式定义的片段,贪婪方式 {m,n}? 匹配 m 到 n 次由前面的正则表达式定义的片段,非贪婪方式 [...] 匹配方括号内内的字符集中的任意一个字符 | 等于 或 (...) 匹配括号内的表达式,也表示一个组 (?iLmsux) 设置可选参数的另类方式,不影响匹配 (?:...) 类似 (...), 但是不表示一个组 (?P...) 类似 (...), 但该组同时得到一个 id,可以在后面的模式中引用 (?P=id) 匹配前面id组匹配的东西 (?#...) 括号内的内容仅仅是注释,不影响匹配 (?=...) Lookahead assertion; matches if regular expression ... matches what comes next, but does not consume any part of the string (?!...) Negative lookahead assertion; matches if regular expression ... does not match what comes next, and does not consume any part of the string (?<=...) Lookbehind assertion; matches if there is a match for regular expression ... ending at the current position (... must match a fixed length) (?...) 也表示一个分组,不同的是它还给这个分组指定了一个名字, id, 可以是任意的 Python 标识符。所有的分组,命名的或未命名的,均被从左至右编号( 1 至 99), 组号 0 表示整个正则表达式。 对一个字符串的任意正则表达式匹配, 每个分组匹配一个子串 (也可能是一个空串)。 当一个正则表达式使用 | 时, 某些分组可能不匹配任何子串,尽管整个正则表达式是匹配的。当一个分组的匹配是一个空串时,我们称这种情形为该分组没有参与匹配。 举例来说: `r'(.+)\1+\Z'` 该模式匹配任意非空子串的两个或更多个重复。(.+) 匹配一个非空子串 (任意字符任意个), 并且定义了一个分组。 \1+ 部分匹配1至更多次这个分组的重复。\Z 将匹配限制只允许发生在字符串的结尾。 === 9.7.6 选项 === 除了在 re 模块的 compile 函数中提供 flags 选项参数之外,在 (? 和 ) 之间使用"iLmsux" 中的一个或多个字母能让你可以在模式字符串中设置正则表达式选项。 不论哪种方式设置的选项,都是针对整个正则表达式的。为了清晰起见,选项通常总是放在模式的开头。如果使用选项x,则选项必须放置在模式首部,因为x选项影响Python解析模式的方式。 显式的使用选项参数比起将选项参数放在模式字符串中具有更好的可读性。函数 compile 的 flags 参数是一个整数, 通过对re模块中提供的属性中的一个或几个进行 '按位或' 运算得来。 (with Python's bitwise OR operator, |). 为了书写方便,每个属性都有一个简称 (一个大写字母)和一个全名 (一个全大写的标识符,具有更好的可读性)。 '''I 或 IGNORECASE''' 匹配忽略时大小写 '''L 或 LOCALE''' 让 \w, \W, \b, 和 \B 由当前区域设置决定 '''M 或 MULTILINE''' 特殊符号 ^ 和 $ 除了匹配字符串开始和结尾,也匹配每行的开始和结尾 (换行符之后/之前) '''S 或 DOTALL''' 特殊字符 . 匹配任意字符,包括换行符 '''U 或 UNICODE''' \w, \W, \b, 和 \B 由Unicode字符集决定 '''X 或 VERBOSE''' 忽略模式字符串中的空白字符, 除非被转义的空白或空白位于字符集合内(中括号内)。该方式允许用 # 字符添加注释直至行尾。 举个例子,下面用三种不同的方式通过 compile 函数生成了相同的三个正则表达式对象(均与不区分大小写的'hello'模式匹配): {{{#!python import re r1 = re.compile(r'(?i)hello') r2 = re.compile(r'hello', re.I) r3 = re.compile(r'hello', re.IGNORECASE) }}} 第三种方式清晰易读,容易维护,尽管它写起来麻烦一点点。注意虽然在这里使用原始字符串不太必要(没有使用转义字符),不过仍然推荐你一直在书写模式字符串时使用原始字符串。 选项 re.VERBOSE (或 re.X) 允许你通过在模式字符串中添加适当的空白和注释来使得正则表达式更容易阅读和维护。复杂的模式可以写成多行,并且能够为每一行添加注释。因此在这种模式你最好使用三引号字符串来书写模式字符串。举例来说: {{{ repat_num1 = r'(0[0-7]*|0x[\da-fA-F]+|[1-9]\d*)L?\Z' repat_num2 = r'''(?x) # 该模式用来匹配整数 (0 [0-7]* | # 八进制: 前导 0, 0+ 个八进制数字 0x [\da-f-A-F]+ | # 十六进制: 0x, 1+ 十六进制数字 [1-9] \d* ) # 十进制: 前导数字(非0), 0+ 十进制数字 L?\Z # 可有可无的L, 位于字符串结尾 ''' }}} 该例子中的两个模式是完全同义的。但是第二个因为添加了注释并且合理的使用了空白,因此具有更好的可读性和可维护性。 === 9.7.7 Match VS Search === 到目前为止,我们已经学会了使用正则表达式来匹配字符串。如果用 match方法,正则表达式模式 r'box' 匹配字符串 'box' 和 'boxes', 但是不匹配 'inbox'. 也就是说,正则表达式对象的 match 方法总是默认匹配从字符串的起始位置开始匹配。就象模式字符串的第一个元素是 \A 一样。 通常,你可能关心发生在字符串的任意位置的匹配,而不是仅发生在字符串首部的匹配。Python管这种操作叫做 Search ,这是为了和 match 有所区别而取的新术语。要进行 search ,你只需要简单的使用正则表达式对象的 search 方法就可以了。看下面的例子: {{{#!python import re r1 = re.compile(r'box') if r1.match('inbox'): print 'match succeeds' else print 'match fails' # prints: match fails if r1. search('inbox'): print 'search succeeds' # prints: search succeeds else print 'search fails' }}} === 9.7.8 字符串开始和结束 === \A and \Z 分别用来表示字符串的开始和结束。传统上使用 `^` 代表字符串开始, $ 代表字符串结束. 在非多行方式下,`^` 等于 \A, $ 等于 \Z。(模式串中没有 ?m 并且没有使用标志 re.M 或 re.MULTILINE进行编译).对一个多行正则表达式对象来说, `^` 代表任意行的开始 (可能是字符串开始,也可能是一个换行符之后). 类似的, $ 代表任意行的结束(可能是字符串的结束,也可能是一个换行符之前). \A 和 \Z 无论是否多行方式,总是严格匹配字符串的开始和结束。举例来说:下面的代码检查一个文件中是否有以数字结束的行。 {{{#!python import re digatend = re.compile(r'\d$', re.MULTILINE) if re.search(open('afile.txt').read()): print "some lines end with digits" else: print "no lines end with digits" }}} 模式` r'\d\n' `几乎与`r'\d$'`同义, 但是当一个文件以数字却没有回车结尾时,匹配将会失败。 === 9.7.9 正则表达式对象 === 正则表达式对象 r 拥有下列只读属性, 这些属性详细描述了 r 是被如何创建出来的。 (通过模块 re 中的 compile 函数,见后文): ==== flags ==== 传递给 compile 函数的 flags 参数, 如果省略了 flags 参数则为 0 ==== groupindex ==== 一个字典,它的 key 是通过元素`(?P)`定义的组的名字; 相应的值则是该组的序号。 ==== pattern ==== 编译 r 使用的模式字符串 这些属性让你很容易的重新得到该对象被编译时使用的模式字符串和参数,再也不需要将它们保存到一个单独地方了了。 正则表达式对象 r 同样提供了很多方法来定位一个字符串中的正则表达式的匹配, 以便进行替换或其它操作,匹配通常由特殊对象表示, 9.7.10节会详细介绍. ==== findall 方法 ==== r.findall(s) 当 r 没有分组时, findall 返回一个字符串列表, 每个字符串都是 s 中不重叠的 r 的匹配. 下面的例子演示了如何打印出一个文件中的所有单词: {{{#!python import re reword = re.compile(r'\w+') for aword in reword.findall(open('afile.txt').read( )): print aword }}} 当 r 有一个分组时, findall 也返回一个字符串列表,列表中的每个字符串都是这个分组的匹配. 举例来说,如果你希望打印出所有的单词(不包括标点),你只需要在上面的例子里修改一个语句: {{{ reword = re.compile('(\w+)\s') }}} 当 r 有 n 个分组时 ( n > 1), findall 返回一个tuple的列表, 包括每一个不重叠的匹配. 每个tuple 有 n 个元素, 每个元素对应一个分组. 下面的例子打印出每行的第一个和最后一个单词(当然是匹配至少有两个单词的行). {{{#!python import re first_last = re.compile(r'^\W*(\w+)\b.*\b(\w+)\W*$', re.MULTILINE) for first, last in first_last.findall(open('afile.txt').read( )): print first, last }}} ==== match 方法 ==== `r.match(s,start=0,end=sys.maxint)` 当匹配时,返回一个 `match` 对象,否则返回 `None`. 注意这个匹配严格的从字符串的 `start` 位置开始. 要在整个字符串中向前搜索匹配,使用 `r.search` 而不是 `r.match`. 下面的例子演示了如何打印出一个文件中所有以数字开头的行: {{{#!python import re digs = re.compile(r'\d+') for line in open('afile.txt'): if digs.match(line): print line, }}} ==== search 方法 ==== `r.search(s,start=0,end=sys.maxint)` 如果匹配成功,返回一个匹配最左边子串的 match 对象,否则返回 None.下面的例子演示了如何打印出含有数字的行: {{{#!python import re digs = re.compile(r'\d+') for line in open('afile.txt'): if digs.search(line): print line, }}} ==== split 方法 ==== r.split(s,maxsplit=0) 返回一个该匹配分隔的子串列表.举例来说,要排除一个字符串中所有的 'hello' 不区分大小写,可以这样: {{{#!python import re rehello = re.compile(r'hello', re.IGNORECASE) astring = ''.join(rehello.split(astring)) }}} 如果 r 有多个分组,则分隔的规则就会象下面这样。下面这个例子只删除位于一个冒号(:)和数字之间的空白。 {{{#!python import re re_col_ws_dig = re.compile(r'(:)\s+(\d)') astring = ''.join(re_col_ws_dig.split(astring)) }}} 这个例子可以说明更多问题: {{{ >>> pat=r'(abc):(123)4(56)' >>> r=re.compile(pat) >>> s='aaabc:123456defghikabc:123456lll' >>> r.split(s) ['aa', 'abc', '123', '56', 'defghik', 'abc', '123', '56', 'lll'] }}} 若 maxsplit 大于 0, 则至多进行 maxsplit 次分隔,剩下的部分则成为列表的最后一个元素。下面的例子只删除第一个匹配的'hello': {{{ astring = ''.join(rehello.split(astring, 1)) }}} ==== sub 函数 ==== r.sub(repl,s,count=0) 返回字符串s的一个拷贝,该串中的所有匹配均被替换成了 repl 。repl可以是一个字符串或一个可调用对象(函数或别的东西)。仅当不存在前一个匹配时,使用空的匹配(空串)替换. 当 count 大于 0 时, 只有前 count 个匹配被替换。若 count 等于 0, 所有的匹配都被替换. 下面的例子提供了删除目的字符串中第一次出现的 'hello' (不区分大小写)的另外一种方式: {{{#!python import re rehello = re.compile(r'hello', re.IGNORECASE) astring = rehello.sub('', astring, 1) }}} 如果例子中的 sub 方法没有最后一个参数, 则例子中的字符串 astring 中的所有 'hello' 均被删除。 若 repl 是一个可调用对象, repl 必须接受且仅接受一个 match 对象做为参数,并返回用来替换匹配的字符串。 sub 调用 repl, 并提供给 repl 一个合适的 match-object 参数, repl则为每个匹配返回一个替换值。还是举个例子表述的清楚,要将所有首字母为'h',末字母为 'o' 的匹配单词转化为大写,你可以这么做: {{{#!python import re h_word = re.compile(r'\bh\w+o\b', re.IGNORECASE) def up(mo): return mo.group(0).upper( ) astring = h_word.sub(up, astring) }}} 在你不需要对匹配进行替换而是需要对这些匹配进行更复杂的处理时,sub 方法非常有用. 下面这个例子展示了 sub 方法的使用,它用来对一个没有分组的正则表达式生成一个类似 findall 方法的函数. {{{#!python import re def findall(r, s): result = [ ] def foundOne(mo): result.append(mo.group( )) r.sub(foundOne, s) return result }}} 这个例子需要 Python 2.2 或更高, 不仅仅是因为它使用了嵌套作用域, 更大的原因是 Python 2.2 允许 repl 返回None并且视为其返回一个空串(''). 而 Python 2.1则太轴,在Python2.1中,只允许 repl 返回一个字符串. 若 repl 是一个字符串而且它不是一个向后引用时, sub 使用 repl 自身作为替换.向后引用是一个 `\g` 形式的 repl 子串,这里 id 是 r 中一个分组的名字(由 r 中模式字符串中的 (?P) 建立), 或 `\dd`, 这里 dd 是一个或两个数字,代表一个分组的编号. 每个向后引用,不论是命名的还是编号的, 都被其所代码的匹配内容所替换.举例来说,下面这个将每个单词放入大括号中: {{{#!python import re grouped_word = re.compile('(\w+)') astring = grouped_word.sub(r'{\1}', astring) }}} ==== subn 函数 ==== r.subn(repl,s,count=0) subn 与 sub 相同, 只是 subn 返回一个元组 (new_string, n) ,这里 n 是 subn 替换的数字.看这个例子, 要统计子串`'hello'`出现的次数,一个方法是: {{{#!python import re rehello = re.compile(r'hello', re.IGNORECASE) junk, count = rehello.subn('', astring) print 'Found', count, 'occurrences of "hello"' }}} === 9.7.10 Match 对象 === 正则表达式对象的 match 和 search 方法创建并返回 Match 对象. 当 repl 参数为可调用对象时, sub 及 subn 方法也会在幕后悄悄生成 Match 对象并使用.(在那种情况下一个相应的 match 对象被作为实参传递给 repl 对象.一个match 对象 m 支持以下属性: pos 起始位置. 传递给 match 或 search 对象的开始位置参数(也就是字符串s开始匹配位置) endpos 结束位置.传递给 match 或 search 对象的结束位置参数(也就是字符串s结束匹配位置) lastgroup 最后匹配的分组的名字(或分组没有名字或根本没有匹配,返回None) lastindex 最后匹配的分组的整数索引(从1开始). (如果没有匹配返回 None) re 创建 m 对象的方法所属的正则表达式对象. string 传递给方法 match, search, sub, 或 subn 的字符串 s 一个 match 对象也支持以下方法: ==== end, span, start 方法 ==== m.end(groupid=0) m.span(groupid=0) m.start(groupid=0) 这些方法返回 m.string 中的由groupid 确定的分组所匹配的子串的定界索引.这里 groupid 可以是一个分组的编号或名字. 当匹配子串是 m.string[i:j] 时, m.start 返回 i, m.end 返回 j, m.span 返回 (i, j). 当分组没有匹配时, i 和 j 的值为 -1. ==== expand 方法 ==== m.expand(s) 返回 s 的一个拷贝, 其中转义序列和向后引用以和 r.sub 同样的方式进行了了替换. 前一小节有详述. ==== group 方法 ==== m.group(groupid=0,*groupids) 当以一个单一的 groupid 参数(一个分组编号或名字)调用时, m.group 返回匹配该分组的子串或 None(没有匹配时). 一个惯用的方式就是 m.group( ), 也就是 m.group(0), 返回整个匹配子串(group 编号 0 暗指整个正则表达式). 当以多参数的方式调用时, 每个参数必须是一个分组编号或分组名字. 这时 m.group 就返回一个tuple(每个元素对应一个参数), 元素的是匹配相应分组的子串或 None (未发生匹配时). ==== groups 方法 ==== m.groups(default=None) 返回一个tuple,tuple的每个元素分别是模式中的分组。如果匹配失败,则返回 none. ==== groupdict 方法 ==== `m.groupdict(default=None)` 返回一个 key 为表达式 r 中的命名分组名字的一个字典. 每个名字的值是相应的匹配子串,或者在未有匹配时 default 的值. === 9.7.11 模块 re 中的函数 === 模块 re 不仅提供了 9.7.6 节中的属性,它还提供了与正则表达式对象的每个方法功能相同的函数。(findall, match, search, split, sub, and subn), 这些函数的第一个参数就是模式字符串。显然这些函数内部也是将这个模式字符串编译为正则表达式对象后再处理的。通常显示的将模式字符串编译为正则表达式对象然后调用该对象的方式更好些,不过,对于某些模式的一次性使用,使用函数更方便一些。举例来说,统计'hello'在一个字符串中出现的次数(不区分大小写),象下面这样做: {{{#!python import re junk, count = re.subn(r'(?i)hello', '', astring) print 'Found', count, 'occurrences of "hello"' }}} 象上面这种方式使用正则表达式,正则表达式选项必须内嵌在模式字符串内(例如在这个例子里的大小写不敏感选项,即(?i)),这是因为这些函数被设计成不接受标志选项参数。 模块 re 也提供了错误处理,当发生错误时会引发异常(通常都是模式字符串的语法错误)。 re模块还提供了两个另外的函数: ==== compile 函数 ==== compile(pattern,flags=0) 解析模式字符串中的每一个语法元素,若无错误则创建并返回一个正则表达式对象。 ==== escape 函数 ==== escape(s) 返回一个字符串s的拷贝,不过s中的所有非字母数字字符在新串中均已被转义(在其前面添加反斜杠)。