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>...)     类似 (...), 但该组同时得到一个 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)
(?<!...)        Negative lookbehind assertion; matches if there is no match for regular expression ... ending at the current position (... must match a fixed length)
\number         匹配先前匹配过的组(通过序号,组自动从1-99编号)
\A              匹配字符串开始
\b              匹配单词边界
\B              匹配一个空串(非单词边界)
\d              匹配任意数字
\D              匹配任意非数字
\s              匹配任意空白字符
\S              匹配任意非空字符
\w              匹配字母数字
\W              匹配非字母数字
\Z              匹配字符串结束
\\              匹配反斜杠

9.7.2 常用正则表达式

'.*' 作为一个正则表达式片段表示 "任意个数(0或更多个)的任意字符(除换行外)"。也就是说, '.*' 匹配一个字符串中的任意子串,包括空串。

'.+''.*'类似,不过它不包括空串。举例来说:

'pre.*post'

匹配一个字符串含有'pre' ,后面跟着一个 'post',中间可以隔着别的字符,也可以不隔着。 (举例来说, 它匹配 'prepost''pre23post')。

'pre.+post' 仅匹配 'pre''post' 之间有其它字符相隔的情况。(举例来说, 它匹配 'pre23post' 但不匹配 'prepost')。

要强制一个模式仅匹配由 'post' 结尾的字符串,用 \Z 结束这个模式。就象下面这样:

r'pre.*post\Z'

匹配 'prepost', 但不匹配 'preposterous'.

另一个经常用的模式是 \b, 它匹配单词边界. 如果你打算匹配单词 'his' 而不是 'this''history', 使用下面的正则表达式:

r'\bhis\b'

匹配以 'her' 开头的子串,使用:

r'\bher'

匹配以 'its' 结尾的子串,使用:

r'its\b'

匹配以 'her' 开头的任意单词:

r'\bher\w*'

匹配以 'its' 结尾的任意单词:

r'\w*its\b'

9.7.3 字符集合

中括号 ([字符列表]) 用来表示字符集合. 可以用连字符 (-) 来表示连续的数字或字母。最后一个字符也包括在字符集合内,这与 Python range函数不同。中括号内的特殊字符代表他们本身, 除了 \, ], 和 - 之外, 这几个字符你必须转义后才可以代表其本身(通过在其前面放一个反斜线).一个字符集合内,你可以使用转义字符所表示的字符类,如 \d 或 \S 等.不过字符集合内的 \b 表示一个退格字符( backspace ), 而不是一个单词边界. 如果一个字符集合里的第一个字符是尖号 (^), 这个集合就是反集合,也就是说.这个集合匹配尖号后面的字符之外的任何字符.

一个常用集合用来匹配单词(字母,连字符或省略符(单引号)):

r"[a-zA-z'\-]+"

连字符前的转义反斜框在此处不是必需的,因为连字符所在位置使它不会具有歧义性.不过还是建议你一直对其使用转义符号,这会使你的模式字符串可读性更佳.

9.7.4 可选择的匹配

竖线 (|) 表明竖线两边的匹配可以任选其一, 具有较低的优先级。. 如果不使用括号, | 则对其两边的整个模式起作用,直到遇到字符串起始或结束或另一个 | 符号为止.一个模式可以由任意个由 | 符号连接的子模式组成.要匹配一个这样的表达式,第一个子模式首先尝试匹配,如果匹配成功,其它的就被跳过.否则就尝试第二个,...第三个.... | 既是贪婪的又是非贪婪的,因为它根本不关心匹配长度.

如果你有一个单词的列表 L , 匹配任意单词的正则表达式就是:

'|'.join([r'\b%s\b' % word for word in L])

如果 L 的元素是常规字符串,而不仅仅是单词, 你需要用 re.escape(本章后面会有详述) 来转义它们. 如果你不需要 \b 单词边界,那么下面这个正则表达式就能满足你的需要:

'|'.join(map(re.escape,L))

9.7.5 分组

正则表达式可以包含0至99个分组(允许任意个,但只有前99个被支持)。通常模式字符串中的一对未转义的小括号表示一个分组。元素 (?P<id>...) 也表示一个分组,不同的是它还给这个分组指定了一个名字, 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'模式匹配):

   1 import re
   2 r1 = re.compile(r'(?i)hello')
   3 r2 = re.compile(r'hello', re.I)
   4 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, 位于字符串结尾
              '''

该例子中的两个模式是完全同义的。但是第二个因为添加了注释并且合理的使用了空白,因此具有更好的可读性和可维护性。

到目前为止,我们已经学会了使用正则表达式来匹配字符串。如果用 match方法,正则表达式模式 r'box' 匹配字符串 'box' 和 'boxes', 但是不匹配 'inbox'. 也就是说,正则表达式对象的 match 方法总是默认匹配从字符串的起始位置开始匹配。就象模式字符串的第一个元素是 \A 一样。

通常,你可能关心发生在字符串的任意位置的匹配,而不是仅发生在字符串首部的匹配。Python管这种操作叫做 Search ,这是为了和 match 有所区别而取的新术语。要进行 search ,你只需要简单的使用正则表达式对象的 search 方法就可以了。看下面的例子:

   1 import re
   2 r1 = re.compile(r'box')
   3 if r1.match('inbox'): print 'match succeeds'
   4 else print 'match fails'                          # prints: match fails
   5 if r1. search('inbox'): print 'search succeeds'   # prints: search succeeds
   6 else print 'search fails'

9.7.8 字符串开始和结束

\A and \Z 分别用来表示字符串的开始和结束。传统上使用 ^ 代表字符串开始, $ 代表字符串结束. 在非多行方式下,^ 等于 \A, $ 等于 \Z。(模式串中没有 ?m 并且没有使用标志 re.M 或 re.MULTILINE进行编译).对一个多行正则表达式对象来说, ^ 代表任意行的开始 (可能是字符串开始,也可能是一个换行符之后). 类似的, $ 代表任意行的结束(可能是字符串的结束,也可能是一个换行符之前). \A 和 \Z 无论是否多行方式,总是严格匹配字符串的开始和结束。举例来说:下面的代码检查一个文件中是否有以数字结束的行。

   1 import re
   2 digatend = re.compile(r'\d$', re.MULTILINE)
   3 if re.search(open('afile.txt').read()): print "some lines end with digits"
   4 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<id>)定义的组的名字; 相应的值则是该组的序号。

pattern

编译 r 使用的模式字符串

这些属性让你很容易的重新得到该对象被编译时使用的模式字符串和参数,再也不需要将它们保存到一个单独地方了了。

正则表达式对象 r 同样提供了很多方法来定位一个字符串中的正则表达式的匹配, 以便进行替换或其它操作,匹配通常由特殊对象表示, 9.7.10节会详细介绍.

findall 方法

r.findall(s)

当 r 没有分组时, findall 返回一个字符串列表, 每个字符串都是 s 中不重叠的 r 的匹配. 下面的例子演示了如何打印出一个文件中的所有单词:

   1 import re
   2 reword = re.compile(r'\w+')
   3 for aword in reword.findall(open('afile.txt').read(  )):
   4     print aword

当 r 有一个分组时, findall 也返回一个字符串列表,列表中的每个字符串都是这个分组的匹配. 举例来说,如果你希望打印出所有的单词(不包括标点),你只需要在上面的例子里修改一个语句:

reword = re.compile('(\w+)\s')

当 r 有 n 个分组时 ( n > 1), findall 返回一个tuple的列表, 包括每一个不重叠的匹配. 每个tuple 有 n 个元素, 每个元素对应一个分组. 下面的例子打印出每行的第一个和最后一个单词(当然是匹配至少有两个单词的行).

   1 import re
   2 first_last = re.compile(r'^\W*(\w+)\b.*\b(\w+)\W*$', re.MULTILINE)
   3 for first, last in first_last.findall(open('afile.txt').read(  )):
   4     print first, last

match 方法

r.match(s,start=0,end=sys.maxint)

当匹配时,返回一个 match 对象,否则返回 None. 注意这个匹配严格的从字符串的 start 位置开始. 要在整个字符串中向前搜索匹配,使用 r.search 而不是 r.match. 下面的例子演示了如何打印出一个文件中所有以数字开头的行:

   1 import re
   2 digs = re.compile(r'\d+')
   3 for line in open('afile.txt'):
   4     if digs.match(line): print line,

search 方法

r.search(s,start=0,end=sys.maxint)

如果匹配成功,返回一个匹配最左边子串的 match 对象,否则返回 None.下面的例子演示了如何打印出含有数字的行:

   1 import re
   2 digs = re.compile(r'\d+')
   3 for line in open('afile.txt'):
   4     if digs.search(line): print line,

split 方法

r.split(s,maxsplit=0)

返回一个该匹配分隔的子串列表.举例来说,要排除一个字符串中所有的 'hello' 不区分大小写,可以这样:

   1 import re
   2 rehello = re.compile(r'hello', re.IGNORECASE)
   3 astring = ''.join(rehello.split(astring))

如果 r 有多个分组,则分隔的规则就会象下面这样。下面这个例子只删除位于一个冒号(:)和数字之间的空白。

   1 import re
   2 re_col_ws_dig = re.compile(r'(:)\s+(\d)')
   3 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' (不区分大小写)的另外一种方式:

   1 import re
   2 rehello = re.compile(r'hello', re.IGNORECASE)
   3 astring = rehello.sub('', astring, 1)

如果例子中的 sub 方法没有最后一个参数, 则例子中的字符串 astring 中的所有 'hello' 均被删除。

若 repl 是一个可调用对象, repl 必须接受且仅接受一个 match 对象做为参数,并返回用来替换匹配的字符串。 sub 调用 repl, 并提供给 repl 一个合适的 match-object 参数, repl则为每个匹配返回一个替换值。还是举个例子表述的清楚,要将所有首字母为'h',末字母为 'o' 的匹配单词转化为大写,你可以这么做:

   1 import re
   2 h_word = re.compile(r'\bh\w+o\b', re.IGNORECASE)
   3 def up(mo): return mo.group(0).upper(  )
   4 astring = h_word.sub(up, astring)

在你不需要对匹配进行替换而是需要对这些匹配进行更复杂的处理时,sub 方法非常有用. 下面这个例子展示了 sub 方法的使用,它用来对一个没有分组的正则表达式生成一个类似 findall 方法的函数.

   1 import re
   2 def findall(r, s):
   3     result = [  ]
   4     def foundOne(mo): result.append(mo.group(  ))
   5     r.sub(foundOne, s)
   6     return result

这个例子需要 Python 2.2 或更高, 不仅仅是因为它使用了嵌套作用域, 更大的原因是 Python 2.2 允许 repl 返回None并且视为其返回一个空串(). 而 Python 2.1则太轴,在Python2.1中,只允许 repl 返回一个字符串.

若 repl 是一个字符串而且它不是一个向后引用时, sub 使用 repl 自身作为替换.向后引用是一个 \g<id> 形式的 repl 子串,这里 id 是 r 中一个分组的名字(由 r 中模式字符串中的 (?P<id>) 建立), 或 \dd, 这里 dd 是一个或两个数字,代表一个分组的编号. 每个向后引用,不论是命名的还是编号的, 都被其所代码的匹配内容所替换.举例来说,下面这个将每个单词放入大括号中:

   1 import re
   2 grouped_word = re.compile('(\w+)')
   3 astring = grouped_word.sub(r'{\1}', astring)

subn 函数

r.subn(repl,s,count=0)

subn 与 sub 相同, 只是 subn 返回一个元组 (new_string, n) ,这里 n 是 subn 替换的数字.看这个例子, 要统计子串'hello'出现的次数,一个方法是:

   1 import re
   2 rehello = re.compile(r'hello', re.IGNORECASE)
   3 junk, count = rehello.subn('', astring)
   4 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'在一个字符串中出现的次数(不区分大小写),象下面这样做:

   1 import re
   2 junk, count = re.subn(r'(?i)hello', '', astring)
   3 print 'Found', count, 'occurrences of "hello"'

象上面这种方式使用正则表达式,正则表达式选项必须内嵌在模式字符串内(例如在这个例子里的大小写不敏感选项,即(?i)),这是因为这些函数被设计成不接受标志选项参数。

模块 re 也提供了错误处理,当发生错误时会引发异常(通常都是模式字符串的语法错误)。 re模块还提供了两个另外的函数:

compile 函数

compile(pattern,flags=0)

解析模式字符串中的每一个语法元素,若无错误则创建并返回一个正则表达式对象。

escape 函数

escape(s)

返回一个字符串s的拷贝,不过s中的所有非字母数字字符在新串中均已被转义(在其前面添加反斜杠)。

RegExpInPython (last edited 2009-12-25 07:09:17 by localhost)