Python文本: 正则表达式

Published on 2017 - 01 - 24

是时候使用正则表达式(regular expression)探索一些复杂模式匹配的方法了。与之相关的功能都位于标准库模块 re 中,因此首先需要引用它。你需要定义一个用于匹配的模式(pattern)字符串以及一个匹配的对象:源(source)字符串。简单的匹配,如下所示:

result = re.match('You', 'Young Frankenstein')

这里,'You' 是模式,'Young Frankenstein' 是源——你想要检查的字符串。match() 函数用于查看源是否以模式开头。

对于更加复杂的匹配,可以先对模式进行编译以加快匹配速度:

youpattern = re.compile('You')

然后就可以直接使用编译好的模式进行匹配了:

result = youpattern.match('Young Frankenstein')

match() 并不是比较 source 和 pattern 的唯一方法。下面列出了另外一些可用的方法:

  • search() 会返回第一次成功匹配,如果存在的话;
  • findall() 会返回所有不重叠的匹配,如果存在的话;
  • split() 会根据 pattern 将 source 切分成若干段,返回由这些片段组成的列表;
  • sub() 还需一个额外的参数 replacement,它会把 source 中所有匹配的 pattern 改成 replacement。

使用match()进行准确匹配

字符串 'Young Frankenstein' 是以单词 'You' 开头的吗?以下是一些带注释的代码:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('You', source)  # 从源字符串的开头开始匹配
>>> if m:  # 匹配成功返回了对象,将它输出看看匹配得到的是什么
...         print(m.group())
...
You

尝试匹配 'Frank' 又会如何?

>>> m = re.match('Frank', source)
>>> if m:
...         print(m.group())
...

这一次,match() 什么也没有返回,if 也没有执行内部的 print 语句。如前所述,match() 只能检测以模式串作为开头的源字符串。但是 search() 可以检测任何位置的匹配:

>>> m = re.search('Frank', source)
>>> if m:
...         print(m.group())
...
Frank

改变一下匹配的模式:

>>> m = re.match('.*Frank', source)
>>> if m:  # match返回对象
...         print(m.group())
...
Young Frank

以下是对新模式能够匹配成功的简单解释:

  • . 代表任何单一字符;
  • * 代表任意一个它之前的字符,.* 代表任意多个字符(包括 0 个);
  • Frank 是我们想要在源字符串中某处进行匹配的短语。

match() 返回了匹配 .*Frank 的字符串:'Young Frank'。

使用 search() 寻找首次匹配

你可以使用 search() 在源字符串 'Young Frankenstein' 的任意位置寻找模式 'Frank',无需通配符 .*:

>>> m = re.search('Frank', source)
>>> if m:  # search返回对象
...         print(m.group())
...
Frank

使用 findall() 寻找所有匹配

之前的例子都是查找到一个匹配即停止。但如果想要知道一个字符串中出现了多少次字母 'n' 应该怎么办?

>>> m = re.findall('n', source)
>>> m   # findall返回了一个列表
['n', 'n', 'n', 'n']
>>> print('Found', len(m), 'matches')
Found 4 matches

将模式改成 'n',紧跟着任意一个字符,结果又如何?

>>> m = re.findall('n.', source)
>>> m
['ng', 'nk', 'ns']

注意,上面例子中最后一个 'n' 并没有匹配成功,需要通过 ? 说明 'n' 后面的字符是可选的:

>>> m = re.findall('n.?', source)
>>> m
['ng', 'nk', 'ns', 'n']

使用 split() 按匹配切分

下面的示例展示了如何依据模式而不是简单的字符串(就像普通的 split() 方法做的)将一个字符串切分成由一系列子串组成的列表:

>>> m = re.split('n', source)
>>> m    # split返回的列表
['You', 'g Fra', 'ke', 'stei', '']

使用 sub() 替换匹配

这和字符串 replace() 方法有些类似,只不过使用的是模式而不是文本串:

>>> m = re.sub('n', '?', source)
>>> m   # sub返回的字符串
'You?g Fra?ke?stei?'

模式:特殊的字符

许多书中关于正则表达式的描述都是从如何定义它开始的,我觉得这不太符合学习的逻辑。正则表达式不是一两句就能说清楚的小语言,它拥有大量的语言细节,会完全占据你的大脑让你无所适从。它使用的符号实在是太多了,看起来简直就像是幽灵画符一样!

有了上面介绍的方法(match()、search()、findall() 和 sub())做铺垫,现在可以从应用讲起并研究如何构造正则表达式了,即上述方法中的模式。

已经见过的一些基本模式:

  • 普通的文本值代表自身,用于匹配非特殊字符;
  • 使用 . 代表任意除 \n 外的字符;
  • 使用 * 表示任意多个字符(包括 0 个);
  • 使用 ? 表示可选字符(0 个或 1 个)。

接下来要介绍一些特殊字符,参见表 3。

[表3:特殊字符]

模式 匹配
\d 一个数字字符
\D 一个非数字字符
\w 一个字母或数字字符
\W 一个非字母非数字字符
\s 空白符
\S 非空白符
\b 单词边界(一个 \w 与\W 之间的范围,顺序可逆)
\B 非单词边界

Python 的 string 模块中预先定义了一些可供我们测试用的字符串常量。我们将使用其中的 printable 字符串,它包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格符以及标点符号:

>>> import string
>>> printable = string.printable
>>> len(printable)
100
>>> printable[0:50]
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
>>> printable[50:]
'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

printable 中哪些字符是数字?

>>> re.findall('\d', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

哪些字符是数字、字符或下划线?

>>> re.findall('\w', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '_']

哪些属于空格符?

>>> re.findall('\s', printable)
[' ', '\t', '\n', '\r', '\x0b', '\x0c']

正则表达式不仅仅适用于 ASCII 字符,例如 \d 还可以匹配 Unicode 的数字字符,并不局限于 ASCII 中的 '0' 到 '9'。我们从 FileFormat.info 中引入两个新的非 ASCII 编码的小写字母。

这个测试例子中,在模式中添加以下内容:

  • 三个 ASCII 字母
  • 三个不会被 \w 所匹配的标点符号
  • Unicode 中的 LATIN SMALL LETTER E WITH CIRCUMFLEX(\u00ea)
  • Unicode 中的 LATIN SMALL LETTER E WITH BREVE(\u0115)
>>> x = 'abc' + '-/*' + '\u00ea' + '\u0115'

与预期的一样,应用这个模式可以匹配出下面这些字母:

>>> re.findall('\w', x)
['a', 'b', 'c', 'ê', 'ě']

模式:使用标识符

现在试着用表 4 中所包含的一些常用的模式标识符来烹饪一道“符号比萨”大餐。

表中,expr 和其他斜体的单词表示合法的正则表达式。

[表4:模式标识符]

模式 匹配
abc 文本值abc
(expr) expr
expr 1 expr 2
. 除 \n 外的任何字符
^ 源字符串的开头
$ 源字符串的结尾
prev? 0 个或 1 个prev
prev* 0 个或多个prev,尽可能多地匹配
prev*? 0 个或多个prev,尽可能少地匹配
prev+ 1 个或多个prev,尽可能多地匹配
prev+? 1 个或多个prev,尽可能少地匹配
prev{m} m 个连续的prev
prev{m, n} m 到 n 个连续的prev,尽可能多地匹配
prev{m, n}? m 到 n 个连续的prev,尽可能少地匹配
[abc] a 或 b 或 c(和 a
[^abc] 非(a 或 b 或c)
prev (?=next) 如果后面为next,返回prev
prev (?!next) 如果后面非next,返回prev
(?<=prev) next 如果前面为prev,返回next
(?<!prev) next 如果前面非prev,返回next

在看下面的例子时,你可能需要时不时地查阅上面的表格。先来定义我们使用的源字符串:

>>> source = '''I wish I may, I wish I might
... Have a dish of fish tonight.'''

首先,在源字符串中检索 wish:

>>> re.findall('wish', source)
['wish', 'wish']

接着,对源字符串任意位置查询 wish 或者 fish:

>>> re.findall('wish|fish', source)
['wish', 'wish', 'fish']

从字符串开头开始匹配 wish:

>>> re.findall('^wish', source)
[]

从字符串开头开始匹配 I wish:

>>> re.findall('^I wish', source)
['I wish']

从字符串结尾开始匹配 fish:

>>> re.findall('fish$', source)
[]

最后,从字符串结尾开始匹配 fish tonight.:

>>> re.findall('fish tonight.$', source)
['fish tonight.']

^ 和 $ 叫作锚点(anchor):^ 将搜索域定位到源字符串的开头,$ 则定位到末尾。上面例子中的 .$ 可以匹配末尾的任意字符,包括句号,因此能成功匹配。但更准确地说,上面的例子应该使用转义符将 . 转义为句号,这才是我们真正想示意的纯文本值匹配:

>>> re.findall('fish tonight\.$', source)
['fish tonight.']

接下来查询以 w 或 f 开头,后面紧接着 ish 的匹配:

>>> re.findall('[wf]ish', source)
['wish', 'wish', 'fish']

查询以若干个 w、s 或 h 组合的匹配:

>>> re.findall('[wsh]+', source)
['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']

查询以 ght 开头,后面紧跟一个非数字非字母字符的匹配:

>>> re.findall('ght\W', source)
['ght\n', 'ght.']

查询以 I 开头,后面跟着 wish 的匹配(wish 出现次数尽量少):

>>> re.findall('I (?=wish)', source)
['I ', 'I ']

最后查询以 wish 结尾,前面为 I 的匹配(I 出现的次数尽量少):

>>> re.findall('(?<=I) wish', source)
[' wish', ' wish']

有时,正则表达式的语法可能会与 Python 本身的语法冲突。例如,我们期望下面例子中的模式能匹配任何以 fish 开头的词:

>>> re.findall('\bfish', source)
[]

为什么没有匹配成功?Python 字符串会使用一些特殊的转义符。例如上面的 \b,它在字符串中代表退格,但在正则表达式中,它代表一个单词的开头位置。因此,把 Python 的普通字符串用作正则表达式的模式串时需要特别注意,不要像上面一样与转义符产生冲突。或者在任何使用正则表达式的地方都记着在模式串的前面添加字符 r,这样可以告诉 Python 这是一个正则表达式,从而禁用字符串转义符,如下所示:

>>> re.findall(r'\bfish', source)
['fish']

模式:定义匹配的输出

当使用 match() 或 search() 时,所有的匹配会以 m.group() 的形式返回到对象 m 中。如果你用括号将某一模式包裹起来,括号中模式匹配得到的结果归入自己的 group(无名称)中,而调用 m.groups() 可以得到包含这些匹配的元组,如下所示:

>>> m = re.search(r'(. dish\b).*(\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')

(?P< name >expr) 这样的模式会匹配 expr,并将匹配结果存储到名为 name 的组中:

>>> m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')
>>> m.group('DISH')
'a dish'
>>> m.group('FISH')
'fish'

参考文档