正则表达式 HOWTO — Python 文档

来自菜鸟教程
Python/docs/3.7/howto/regex
跳转至:导航、​搜索

正则表达式 HOWTO

作者
是 古晋< amk@amk.ca >

抽象的

本文档是在 Python 中通过 re 模块使用正则表达式的介绍性教程。 它提供了比图书馆参考中的相应部分更温和的介绍。


介绍

正则表达式(称为 RE,或正则表达式,或正则表达式模式)本质上是一种嵌入在 Python 中的微型、高度专业化的编程语言,可通过 re 模块使用。 使用这种小语言,您可以为要匹配的可能字符串集指定规则; 该集合可能包含英语句子、电子邮件地址、TeX 命令或任何您喜欢的内容。 然后,您可以提出诸如“此字符串是否与模式匹配?”或“此字符串中的任何位置是否与模式匹配?”之类的问题。 您还可以使用 RE 修改字符串或以各种方式将其拆分。

正则表达式模式被编译成一系列字节码,然后由用 C 编写的匹配引擎执行。 对于高级使用,可能需要特别注意引擎将如何执行给定的 RE,并以某种方式编写 RE,以生成运行速度更快的字节码。 本文档不涉及优化,因为它要求您对匹配引擎的内部结构有很好的了解。

正则表达式语言相对较小且受限制,因此并非所有可能的字符串处理任务都可以使用正则表达式完成。 还有一些任务可以用正则表达式来完成,但是这些表达式变得非常复杂。 在这些情况下,最好编写 Python 代码来进行处理; 虽然 Python 代码会比复杂的正则表达式慢,但它也可能更容易理解。


简单模式

我们将从学习最简单的正则表达式开始。 由于正则表达式用于对字符串进行操作,因此我们将从最常见的任务开始:匹配字符。

有关正则表达式(确定性和非确定性有限自动机)基础的计算机科学的详细解释,您可以参考几乎所有有关编写编译器的教科书。

匹配字符

大多数字母和字符只会匹配自己。 例如,正则表达式 test 将完全匹配字符串 test。 (您可以启用不区分大小写的模式,让这个 RE 也匹配 TestTEST;稍后会详细介绍。)

这条规则也有例外; 某些字符是特殊的 元字符 ,并且与自身不匹配。 相反,它们表示应该匹配一些不寻常的东西,或者它们通过重复它们或改变它们的含义来影响 RE 的其他部分。 本文档的大部分内容都致力于讨论各种元字符及其作用。

这是元字符的完整列表; 它们的含义将在本 HOWTO 的其余部分讨论。

. ^ $ * + ? { } [ ] \ | ( )

我们将看到的第一个元字符是 []。 它们用于指定字符类,即您希望匹配的一组字符。 字符可以单独列出,也可以通过给出两个字符并用 '-' 分隔它们来表示字符范围。 例如,[abc] 将匹配任何字符 abc; 这与 [a-c] 相同,它使用范围来表示相同的字符集。 如果您只想匹配小写字母,则您的 RE 将是 [a-z]

元字符在类中不活跃。 例如,[akm$] 将匹配任何字符 'a''k''m''$''$' 通常是一个元字符,但在字符类中它被剥夺了它的特殊性质。

您可以通过 补充 集合来匹配类中未列出的字符。 这通过包含 '^' 作为类的第一个字符来表示。 例如,[^5] 将匹配除 '5' 之外的任何字符。 如果插入符号出现在字符类的其他地方,则它没有特殊含义。 例如:[5^] 将匹配 '5''^'

也许最重要的元字符是反斜杠 \。 与 Python 字符串文字一样,反斜杠后面可以跟各种字符以表示各种特殊序列。 它还用于转义所有元字符,以便您仍然可以在模式中匹配它们; 例如,如果您需要匹配一个 [\,您可以在它们前面加上一个反斜杠以去除它们的特殊含义:\[\\

一些以 '\' 开头的特殊序列表示通常有用的预定义字符集,例如数字集、字母集或任何非空白字符集。

让我们举个例子: \w 匹配任何字母数字字符。 如果正则表达式模式以字节表示,则相当于 [a-zA-Z0-9_] 类。 如果正则表达式模式是字符串,则 \w 将匹配 unicodedata 模块提供的 Unicode 数据库中所有标记为字母的字符。 通过在编译正则表达式时提供 re.ASCII 标志,您可以在字符串模式中使用更受限制的 \w 定义。

以下特殊序列列表并不完整。 有关 Unicode 字符串模式的序列和扩展类定义的完整列表,请参阅标准库参考中 正则表达式语法 的最后一部分。 通常,Unicode 版本匹配 Unicode 数据库中适当类别中的任何字符。

\d
匹配任何十进制数字; 这相当于 [0-9] 类。
\D
匹配任何非数字字符; 这相当于类 [^0-9]
\s
匹配任何空白字符; 这相当于 [ \t\n\r\f\v] 类。
\S
匹配任何非空白字符; 这相当于 [^ \t\n\r\f\v] 类。
\w
匹配任何字母数字字符; 这相当于 [a-zA-Z0-9_] 类。
\W
匹配任何非字母数字字符; 这相当于 [^a-zA-Z0-9_] 类。

这些序列可以包含在字符类中。 例如,[\s,.] 是一个字符类,它将匹配任何空白字符,或 ',''.'

本节中的最后一个元字符是 .。 它匹配除换行符以外的任何内容,并且有一种替代模式 (re.DOTALL),它甚至可以匹配换行符。 . 通常用于您想要匹配“任何字符”的地方。


重复的事情

能够匹配不同的字符集是正则表达式可以做的第一件事,这在字符串上可用的方法中是不可能的。 但是,如果这是正则表达式的唯一附加功能,它们将不会有太大的进步。 另一项功能是您可以指定 RE 的某些部分必须重复一定次数。

用于重复我们将看到的内容的第一个元字符是 ** 与文字字符 '*' 不匹配; 相反,它指定前一个字符可以匹配零次或多次,而不是只匹配一次。

例如,ca*t 将匹配 'ct'(0 'a' 个字符)、'cat'(1 'a')、'caaat' (3 个 'a' 个字符),依此类推。

*这样的重复是贪婪; 重复 RE 时,匹配引擎将尝试尽可能多地重复它。 如果模式的后面部分不匹配,则匹配引擎将备份并以更少的重复次数重试。

一个循序渐进的例子将使这一点更加明显。 让我们考虑表达式 a[bcd]*b。 这匹配字母 'a'、来自类 [bcd] 的零个或多个字母,最后以 'b' 结尾。 现在想象将这个 RE 与字符串 'abcbd' 进行匹配。

匹配 解释
1 a RE 中的 a 匹配。
2 abcbd 引擎匹配 [bcd]*,尽可能地匹配到字符串的末尾。
3 失败 引擎尝试匹配 b,但当前位置在字符串的末尾,因此失败。
4 abcb 备份,以便 [bcd]* 少匹配一个字符。
5 失败 再次尝试 b,但当前位置在最后一个字符,即 'd'
6 abc 再次备份,使 [bcd]* 仅匹配 bc
6 abcb 再次尝试 b。 这次当前位置的字符是'b',所以成功了。

现在已经到了 RE 的末尾,它匹配了 'abcb'。 这演示了匹配引擎如何尽可能地首先运行,如果没有找到匹配项,它将逐渐备份并一次又一次地重试 RE 的其余部分。 它将备份,直到它尝试了 [bcd]* 的零匹配,如果随后失败,引擎将得出结论,该字符串根本不匹配 RE。

另一个重复元字符是 +,它匹配一次或多次。 注意【X48X】【X52X】和【X62X】【X66X】的区别; * 匹配 零次 或多次,因此可能根本不存在重复的任何内容,而 + 至少需要 一次 出现。 举个类似的例子,ca+t 将匹配 'cat' (1 'a'), 'caaat' (3 'a's),但不会' t 匹配 'ct'

还有两个重复的限定词。 问号字符 ? 匹配一次或零次; 您可以将其视为将某些内容标记为可选的。 例如,home-?brew 匹配 'homebrew''home-brew'

最复杂的重复限定符是 {m,n},其中 mn 是十进制整数。 这个限定符意味着必须至少有 m 次重复,最多 n 次重复。 例如,a/{1,3}b 将匹配 'a/b''a//b''a///b'。 它不会匹配没有斜线的 'ab' 或有四个斜线的 'a////b'

您可以省略 mn; 在这种情况下,会为缺失值假定一个合理的值。 省略 m 被解释为 0 的下限,而省略 n 会导致上限为无穷大。

还原论倾向的读者可能会注意到,其他三个限定词都可以使用这种表示法来表达。 {0,}等同于*{1,}等同于+{0,1}等同于? . 最好尽可能使用 *+?,因为它们更短且更易于阅读。


使用正则表达式

既然我们已经看过一些简单的正则表达式,那么我们如何在 Python 中实际使用它们呢? re 模块为正则表达式引擎提供了一个接口,允许您将 RE 编译为对象,然后与它们进行匹配。

编译正则表达式

正则表达式被编译成模式对象,这些对象具有用于各种操作的方法,例如搜索模式匹配或执行字符串替换。

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() 还接受一个可选的 flags 参数,用于启用各种特殊功能和语法变化。 稍后我们将讨论可用的设置,但现在仅举一个例子:

>>> p = re.compile('ab*', re.IGNORECASE)

RE 作为字符串传递给 re.compile()。 RE 被作为字符串处理,因为正则表达式不是核心 Python 语言的一部分,并且没有创建特殊的语法来表达它们。 (有些应用程序根本不需要 RE,所以没有必要通过包含它们来膨胀语言规范。)相反,re 模块只是 Python 中包含的 C 扩展模块,就像socketzlib 模块。

将 RE 放在字符串中使 Python 语言更简单,但有一个缺点,即下一节的主题。


反斜杠瘟疫

如前所述,正则表达式使用反斜杠字符 ('\') 来表示特殊形式或允许使用特殊字符而不调用其特殊含义。 这与 Python 在字符串文字中出于相同目的使用相同字符相冲突。

假设您要编写一个与字符串 \section 匹配的 RE,该字符串可能在 LaTeX 文件中找到。 要确定要在程序代码中写入什么内容,请从要匹配的所需字符串开始。 接下来,您必须通过在任何反斜杠和其他元字符前面加上反斜杠来对它们进行转义,从而得到字符串 \\section。 必须传递给 re.compile() 的结果字符串必须是 \\section。 但是,要将其表示为 Python 字符串文字,两个反斜杠必须再次转义 '

人物 阶段
\section 要匹配的文本字符串
\\section re.compile() 的转义反斜杠
"\\\\section" 转义字符串文字的反斜杠

简而言之,要匹配文字反斜杠,必须将 '\\\\' 写为 RE 字符串,因为正则表达式必须为 \\,并且每个反斜杠必须表示为 \\在常规 Python 字符串文字中。 在具有重复反斜杠的 RE 中,这会导致大量重复的反斜杠并使生成的字符串难以理解。

解决方案是对正则表达式使用 Python 的原始字符串表示法; 在前缀为 'r' 的字符串文字中,反斜杠不会以任何特殊方式处理,因此 r"\n" 是包含 '\''n' 的两个字符的字符串,而"\n" 是包含换行符的单字符字符串。 正则表达式通常使用这种原始字符串表示法用 Python 代码编写。

此外,在正则表达式中有效但作为 Python 字符串文字无效的特殊转义序列现在会导致 DeprecationWarning 并最终成为 SyntaxError,这意味着序列将如果未使用原始字符串表示法或转义反斜杠,则无效。

普通字符串 原始字符串
"ab*" r"ab*"
"\\\\section" r"\\section"
"\\w+\\s+\\1" r"\w+\s+\1"


表演比赛

一旦你有一个表示编译的正则表达式的对象,你用它做什么? 模式对象有几个方法和属性。 这里只介绍最重要的; 有关完整列表,请参阅 re 文档。

方法/属性 目的
match() 确定 RE 是否在字符串的开头匹配。
search() 扫描字符串,查找此 RE 匹配的任何位置。
findall() 查找 RE 匹配的所有子字符串,并将它们作为列表返回。
finditer() 查找 RE 匹配的所有子字符串,并将它们作为 迭代器 返回。

match()search() 如果找不到匹配项,则返回 None。 如果它们成功,则返回一个 匹配对象 实例,其中包含有关匹配的信息:它的开始和结束位置、匹配的子字符串等等。

您可以通过交互试验 re 模块来了解这一点。 如果您有 tkinter 可用,您可能还想查看 :source:`Tools/demo/redemo.py`,这是 Python 发行版中包含的演示程序。 它允许您输入 RE 和字符串,并显示 RE 是否匹配或失败。 redemo.py 在尝试调试复杂的 RE 时非常有用。

本 HOWTO 使用标准 Python 解释器作为示例。 首先,运行 Python 解释器,导入 re 模块,并编译一个 RE:

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

现在,您可以尝试将各种字符串与 RE [a-z]+ 进行匹配。 空字符串根本不应该匹配,因为 + 表示“一次或多次重复”。 match() 在这种情况下应该返回 None,这将导致解释器不打印输出。 您可以明确打印 match() 的结果以明确这一点。

>>> p.match("")
>>> print(p.match(""))
None

现在,让我们在它应该匹配的字符串上尝试它,例如 tempo。 在这种情况下,match() 将返回一个 匹配对象 ,因此您应该将结果存储在一个变量中以备后用。

>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>

现在您可以查询 匹配对象 以获取有关匹配字符串的信息。 匹配对象实例也有几个方法和属性; 最重要的是:

方法/属性 目的
group() 返回正则匹配的字符串
start() 返回匹配的起始位置
end() 返回匹配的结束位置
span() 返回一个包含匹配(开始,结束)位置的元组

尝试这些方法将很快阐明它们的含义:

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回与 RE 匹配的子字符串。 start()end() 返回匹配的开始和结束索引。 span() 在单个元组中返回开始和结束索引。 由于 match() 方法只检查 RE 是否在字符串的开头匹配,因此 start() 将始终为零。 但是,模式的 search() 方法扫描字符串,因此在这种情况下匹配可能不会从零开始。

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

在实际程序中,最常见的风格是将匹配对象存储在一个变量中,然后检查它是否是None。 这通常看起来像:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

两种模式方法返回模式的所有匹配项。 findall() 返回匹配字符串列表:

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

r 前缀使文字成为原始字符串文字,在此示例中是必需的,因为 Python 无法识别的普通“熟”字符串文字中的转义序列,与正则表达式相反,现在导致DeprecationWarning 最终会变成 SyntaxError。 参见 反斜杠瘟疫

findall() 必须先创建整个列表,然后才能将其作为结果返回。 finditer() 方法将一系列 匹配对象 实例作为 迭代器 返回:

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

模块级函数

您不必创建模式对象并调用其方法; re 模块还提供了名为 match()search()findall()sub 的顶级函数() 等等。 这些函数采用与相应模式方法相同的参数,并将 RE 字符串添加为第一个参数,并且仍然返回 None匹配对象 实例。

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<re.Match object; span=(0, 5), match='From '>

在幕后,这些函数只是为您创建一个模式对象并在其上调用适当的方法。 它们还将编译后的对象存储在缓存中,因此将来使用相同 RE 的调用将不需要一次又一次地解析模式。

您应该使用这些模块级函数,还是应该自己获取模式并调用其方法? 如果您在循环中访问正则表达式,预编译它将节省一些函数调用。 在循环之外,由于内部缓存,没有太大区别。


编译标志

编译标志使您可以修改正则表达式工作方式的某些方面。 标志在 re 模块中有两个名称,一个是长名称,例如 IGNORECASE,另一个是短的单字母形式,例如 I。 (如果您熟悉 Perl 的模式修饰符,单字母形式使用相同的字母;例如,re.VERBOSE 的缩写是 re.X。)标志可以通过按位或运算指定; 例如,re.I | re.M 设置 IM 标志。

这是可用标志的表格,然后是每个标志的更详细说明。

旗帜 意义
ASCIIA 使 \w\b\s\d 等几个转义符仅匹配具有相应属性的 ASCII 字符。
DOTALLS 使 . 匹配任何字符,包括换行符。
IGNORECASEI 进行不区分大小写的匹配。
LOCALEL 进行区域感知匹配。
MULTILINEM 多线匹配,影响^$
VERBOSEX(用于“扩展”) 启用详细的 RE,可以更清晰、更易于理解地组织。
I

IGNORECASE

执行不区分大小写的匹配; 字符类和文字字符串将通过忽略大小写来匹配字母。 例如, [A-Z] 也会匹配小写字母。 除非使用 ASCII 标志禁用非 ASCII 匹配,否则完全 Unicode 匹配也有效。 当 Unicode 模式 [a-z][A-Z]IGNORECASE 标志结合使用时,它们将匹配 52 个 ASCII 字母和 4 个额外的非 ASCII 字母:'İ' ( U+0130,拉丁文大写字母 I,上面有点),'ı'(U+0131,拉丁文小写字母无点 i),'ſ'(U+017F,拉丁文小写字母长 s)和 'K'(U+212A) ,开尔文符号)。 Spam 将匹配 'Spam''spam''spAM''ſpam'(后者仅在 Unicode 模式下匹配)。 这种小写不考虑当前的语言环境; 如果您还设置了 LOCALE 标志,它也会如此。
L
LOCALE

使 \w\W\b\B 和不区分大小写的匹配依赖于当前语言环境而不是 Unicode 数据库。

语言环境是 C 库的一项功能,旨在帮助编写考虑语言差异的程序。 例如,如果您正在处理编码的法语文本,您希望能够编写 \w+ 来匹配单词,但 \w 仅匹配字符类 [A-Za-z]字节模式; 它不会匹配对应于 éç 的字节。 如果您的系统配置正确并且选择了法语区域设置,某些 C 函数会告诉程序对应于 é 的字节也应该被视为一个字母。 在编译正则表达式时设置 LOCALE 标志将导致生成的编译对象将这些 C 函数用于 \w; 这速度较慢,但也使 \w+ 能够像您期望的那样匹配法语单词。 在 Python 3 中不鼓励使用此标志,因为区域设置机制非常不可靠,它一次只能处理一种“文化”,并且仅适用于 8 位区域设置。 Unicode 匹配已经在 Python 3 中默认启用,用于 Unicode (str) 模式,并且它能够处理不同的区域设置/语言。

M
MULTILINE

^$ 尚未解释;它们将在 更多元字符 部分介绍。)

通常 ^ 只匹配字符串的开头,而 $ 只匹配字符串的末尾和字符串末尾的换行符(如果有)之前。 指定此标志时,^ 匹配字符串的开头和字符串中每一行的开头,紧跟在每个换行符之后。 类似地,$ 元字符匹配字符串末尾和每行末尾(紧接在每个换行符之前)。

S

DOTALL

使 '.' 特殊字符匹配任何字符,包括换行符; 如果没有这个标志, '.' 将匹配任何 除了 换行符。
A

ASCII

使 \w\W\b\B\s\S 仅执行 ASCII 匹配而不是完全匹配Unicode 匹配。 这仅对 Unicode 模式有意义,而对于字节模式则被忽略。
X
VERBOSE

此标志允许您编写更具可读性的正则表达式,因为您可以更灵活地格式化它们。 指定此标志后,将忽略 RE 字符串中的空格,除非空格在字符类中或前面有未转义的反斜杠; 这使您可以更清晰地组织和缩进 RE。 此标志还允许您将注释放入引擎将忽略的正则中; 注释由 '#' 标记,它既不在字符类中,也不以未转义的反斜杠开头。

例如,这是一个使用 re.VERBOSE; 的 RE。 看看它有多容易阅读?

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

如果没有详细设置,RE 将如下所示:

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

在上面的例子中,Python 的字符串文字的自动连接已被用来将 RE 分解成更小的部分,但它仍然比使用 re.VERBOSE 的版本更难理解。


更多的模式力量

到目前为止,我们只介绍了正则表达式的一部分功能。 在本节中,我们将介绍一些新的元字符,以及如何使用组来检索匹配的文本部分。

更多元字符

有一些我们还没有涉及的元字符。 本节将介绍其中的大部分内容。

剩下的一些要讨论的元字符是 零宽度断言 。 它们不会使发动机通过弦线前进; 相反,它们根本不消耗字符,只是成功或失败。 例如,\b是当前位置位于词边界的断言; \b 根本不会改变位置。 这意味着永远不应该重复零宽度断言,因为如果它们在给定位置匹配一次,它们显然可以被无限次匹配。

|

交替,或“或”运算符。 如果 AB 是正则表达式,则 A|B 将匹配与 AB 匹配的任何字符串。 | 具有非常低的优先级,以使其在交替多字符串时合理工作。 Crow|Servo 将匹配 'Crow''Servo',而不是 'Cro''w''S''ervo'

要匹配文字 '|',请使用 \|,或将其包含在字符类中,如 [|]

^

匹配行首。 除非设置了 MULTILINE 标志,否则这只会在字符串的开头匹配。 在 MULTILINE 模式下,这也会在字符串中的每个换行符之后立即匹配。

例如,如果您希望仅在行首匹配单词 From,则使用的 RE 是 ^From

>>> print(re.search('^From', 'From Here to Eternity'))  
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

要匹配文字 '^',请使用 \^

$

在行尾匹配,该行被定义为字符串的结尾或任何后跟换行符的位置。

>>> print(re.search('}$', '{block}'))  
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<re.Match object; span=(6, 7), match='}'>

要匹配文字 '$',请使用 \$ 或将其包含在字符类中,如 [$]

\A

仅在字符串的开头匹配。 当不在 MULTILINE 模式下时,\A^ 实际上是相同的。 在 MULTILINE 模式下,它们是不同的: \A 仍然只匹配字符串的开头,但 ^ 可以匹配字符串中换行符后的任何位置.

\Z

仅在字符串末尾匹配。

\b

字界。 这是一个零宽度断言,仅在单词的开头或结尾匹配。 单词被定义为一系列字母数字字符,因此单词的结尾由空格或非字母数字字符表示。

下面的例子只匹配 class 是一个完整的单词; 当它包含在另一个词中时,它不会匹配。

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

使用这个特殊序列时,您应该记住两个微妙之处。 首先,这是 Python 的字符串文字和正则表达式序列之间最严重的冲突。 在 Python 的字符串文字中,\b 是退格字符,ASCII 值为 8。 如果您不使用原始字符串,那么 Python 会将 \b 转换为退格,并且您的 RE 将不会按您的预期匹配。 下面的示例看起来与我们之前的 RE 相同,但省略了 RE 字符串前面的 'r'

>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match object; span=(0, 7), match='\x08class\x08'>

其次,在字符类中,该断言没有用处,\b 表示退格字符,以便与 Python 的字符串文字兼容。

\B

另一个零宽度断言,这与\b相反,仅在当前位置不在单词边界时匹配。


分组

通常,您需要获得更多信息,而不仅仅是 RE 是否匹配。 正则表达式通常用于通过将 RE 分成几个匹配不同感兴趣组件的子组来剖析字符串。 例如,一个 RFC-822 标题行分为标题名称和值,用 ':' 分隔,如下所示:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

这可以通过编写一个匹配整个标题行的正则表达式来处理,并且有一个匹配标题名称的组和另一个匹配标题值的组。

组由 '('')' 元字符标记。 '('')' 的含义与它们在数学表达式中的含义大致相同; 它们将包含在其中的表达式组合在一起,您可以使用重复限定符重复组的内容,例如 *+?{m,n}。 例如,(ab)* 将匹配零个或多个重复的 ab

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

'('')' 表示的组也捕获它们匹配的文本的开始和结束索引; 这可以通过将参数传递给 group()start()end()span() 来检索。 组从 0 开始编号。 组 0 始终存在; 它是整个 RE,因此 match object 方法都将组 0 作为其默认参数。 稍后我们将看到如何表达不捕获它们匹配的文本范围的组。

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

子组从左到右编号,从 1 向上。 组可以嵌套; 要确定数字,只需计算左括号字符,从左到右。

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

group() 可以一次传递多个组号,在这种情况下,它将返回一个包含这些组对应值的元组。

>>> m.group(2,1,2)
('b', 'abc', 'b')

groups() 方法返回一个包含所有子组的字符串的元组,从 1 到有多少。

>>> m.groups()
('abc', 'b')

模式中的反向引用允许您指定先前捕获组的内容也必须在字符串的当前位置找到。 例如,\1 如果可以在当前位置找到组 1 的确切内容,则成功,否则失败。 请记住,Python 的字符串文字也使用反斜杠后跟数字以允许在字符串中包含任意字符,因此在 RE 中合并反向引用时一定要使用原始字符串。

例如,以下 RE 检测字符串中的双字。

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

像这样的反向引用通常不适用于仅搜索字符串——很少有文本格式以这种方式重复数据——但你很快就会发现它们在执行字符串替换时 非常 很有用.


非捕获组和命名组

精心设计的 RE 可能会使用许多组,既可以捕获感兴趣的子串,也可以对 RE 本身进行分组和结构化。 在复杂的 RE 中,很难跟踪组号。 有两个功能可以帮助解决这个问题。 它们都使用通用的正则表达式扩展语法,所以我们先来看看。

Perl 5 以其对标准正则表达式的强大补充而闻名。 对于这些新功能,Perl 开发人员无法选择新的单键元字符或以 \ 开头的新特殊序列,而不会使 Perl 的正则表达式与标准 RE 混淆地不同。 例如,如果他们选择 & 作为新的元字符,旧的表达式会假设 & 是一个常规字符并且不会通过写 \& 或 [ X187X]。

Perl 开发人员选择的解决方案是使用 (?...) 作为扩展语法。 ? 紧跟在括号之后是语法错误,因为 ? 没有什么可重复的,所以这没有引入任何兼容性问题。 ? 之后的字符表示正在使用的扩展名,因此 (?=foo) 是一回事(正向前瞻断言),而 (?:foo) 是另一回事(非捕获组包含子表达式 foo)。

Python 支持多个 Perl 的扩展,并在 Perl 的扩展语法中添加了扩展语法。 如果问号后的第一个字符是 P,则您知道它是特定于 Python 的扩展。

现在我们已经了解了通用扩展语法,我们可以返回到简化复杂 RE 中组的工作的功能。

有时您想使用组来表示正则表达式的一部分,但对检索组的内容不感兴趣。 您可以使用非捕获组来明确说明这一事实:(?:...),您可以在其中用任何其他正则表达式替换 ...

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

除了您无法检索组匹配的内容这一事实之外,非捕获组的行为与捕获组完全相同; 您可以将任何内容放入其中,使用重复元字符(例如 *)重复它,并将其嵌套在其他组中(捕获或非捕获)。 (?:...) 在修改现有模式时特别有用,因为您可以添加新组而无需更改所有其他组的编号方式。 应该提到的是,捕获组和非捕获组之间的搜索没有性能差异; 两种形式都不比另一种形式快。

一个更重要的特征是命名组:组可以通过名称引用,而不是用数字来引用。

命名组的语法是 Python 特定的扩展之一:(?P<name>...)name 显然是组的名称。 命名组的行为与捕获组完全相同,并且另外将名称与组相关联。 处理捕获组的 match object 方法都接受按数字引用组的整数或包含所需组名称的字符串。 命名组仍被赋予编号,因此您可以通过两种方式检索有关组的信息:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

命名组很方便,因为它们让您可以使用容易记住的名称,而不必记住数字。 这是来自 imaplib 模块的示例 RE:

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

检索 m.group('zonem') 显然要容易得多,而不必记住检索第 9 组。

(...)\1 等表达式中反向引用的语法指的是组的编号。 自然有一个变体使用组名而不是数字。 这是另一个 Python 扩展: (?P=name) 表示应该在当前点再次匹配名为 name 的组的内容。 查找双字的正则表达式\b(\w+)\s+\1\b也可以写成\b(?P<word>\w+)\s+(?P=word)\b

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

前瞻断言

另一个零宽度断言是先行断言。 前瞻断言有正面和负面两种形式,如下所示:

(?=...)
正向前瞻断言。 如果包含的正则表达式(此处由 ... 表示)在当前位置成功匹配,则成功,否则失败。 但是,一旦尝试了包含的表达式,匹配引擎就根本不会前进; 模式的其余部分在断言开始的地方尝试。
(?!...)
否定前瞻断言。 这与肯定断言相反; 如果包含的表达式 在字符串中的当前位置匹配,则它成功。

为了具体说明这一点,让我们看一个前瞻有用的情况。 考虑一个简单的模式来匹配文件名并将其拆分为基本名称和扩展名,用 . 分隔。 例如,在 news.rc 中,news 是基本名称,而 rc 是文件名的扩展名。

与此匹配的模式非常简单:

.*[.].*$

请注意, . 需要特别处理,因为它是一个元字符,因此它位于字符类中以仅匹配该特定字符。 还要注意尾随的 $; 添加此项是为了确保字符串的所有其余部分都必须包含在扩展名中。 此正则表达式匹配 foo.barautoexec.batsendmail.cfprinters.conf

现在,考虑将问题复杂化一点; 如果要匹配扩展名不是 bat 的文件名怎么办? 一些不正确的尝试:

.*[.][^b].*$ 上面的第一次尝试试图通过要求扩展名的第一个字符不是 b 来排除 bat。 这是错误的,因为模式也不匹配 foo.bar

.*[.]([^b]..|.[^a].|..[^t])$

当您尝试通过要求匹配以下情况之一来修补第一个解决方案时,表达式变得更加混乱:扩展名的第一个字符不是 b; 第二个字符不是 a; 或者第三个字符不是 t。 这接受 foo.bar 并拒绝 autoexec.bat,但它需要三个字母的扩展名,并且不会接受具有两个字母扩展名的文件名,例如 sendmail.cf。 我们将再次使模式复杂化以修复它。

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

在第三次尝试中,第二个和第三个字母都是可选的,以允许匹配短于三个字符的扩展名,例如 sendmail.cf

该模式现在变得非常复杂,这使得阅读和理解变得困难。 更糟糕的是,如果问题发生变化并且您想将 batexe 作为扩展排除,则模式会变得更加复杂和混乱。

一个负面的前瞻可以消除所有这些混乱:

.*[.](?!bat$)[^.]*$ 负前瞻意味着:如果表达式 bat 此时不匹配,则尝试模式的其余部分; 如果 bat$ 匹配,整个模式将失败。 需要尾随 $ 以确保允许使用类似 sample.batch 的扩展名,其中扩展名仅以 bat 开头。 当文件名中有多个点时,[^.]* 确保模式有效。

排除另一个文件扩展名现在很容易; 只需在断言中添加它作为替代。 以下模式不包括以 batexe 结尾的文件名:

.*[.](?!bat$|exe$)[^.]*$


修改字符串

到目前为止,我们只是对静态字符串执行了搜索。 正则表达式也常用于以各种方式修改字符串,使用以下模式方法:

方法/属性 目的
split() 将字符串拆分为一个列表,在 RE 匹配的任何地方拆分它
sub() 查找 RE 匹配的所有子字符串,并用不同的字符串替换它们
subn() sub() 做同样的事情,但返回新字符串和替换次数

拆分字符串

模式的 split() 方法在正则匹配的地方将字符串分开,返回片段列表。 它类似于字符串的 split() 方法,但在分隔符中提供了更多的通用性,您可以将其拆分; string split() 仅支持按空格或固定字符串拆分。 正如您所期望的,还有一个模块级的 re.split() 函数。

.split(string[, maxsplit=0])
通过正则表达式的匹配拆分 string。 如果在 RE 中使用了捕获括号,则它们的内容也将作为结果列表的一部分返回。 如果 maxsplit 非零,则最多执行 maxsplit 次分割。

您可以通过为 maxsplit 传递一个值来限制进行的拆分次数。 当 maxsplit 不为零时,最多会进行 maxsplit 次拆分,并将字符串的剩余部分作为列表的最后一个元素返回。 在以下示例中,分隔符是任何非字母数字字符序列。

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

有时您不仅对分隔符之间的文本感兴趣,还需要知道分隔符是什么。 如果在 RE 中使用了捕获括号,则它们的值也会作为列表的一部分返回。 比较以下调用:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

模块级函数 re.split() 添加 RE 用作第一个参数,但其他方面相同。

>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

搜索和替换

另一个常见的任务是找到一个模式的所有匹配项,并用不同的字符串替换它们。 sub() 方法接受一个替换值,它可以是一个字符串或一个函数,以及要处理的字符串。

.sub(replacement, string[, count=0])

返回通过替换 replacement 替换 string 中 RE 最左边的非重叠出现而获得的字符串。 如果未找到模式,则返回 string 不变。

可选参数 count 是要替换的模式出现的最大次数; count 必须是非负整数。 默认值 0 表示替换所有匹配项。

这是一个使用 sub() 方法的简单示例。 它用单词 colour 替换颜色名称:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

subn() 方法执行相同的工作,但返回一个包含新字符串值和执行替换次数的 2 元组:

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

空匹配仅在它们与前一个空匹配不相邻时才被替换。

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'

如果 replacement 是字符串,则处理其中的任何反斜杠转义。 即,\n 转换为单个换行符,\r 转换为回车,依此类推。 诸如 \& 之类的未知转义被单独留下。 反向引用,例如 \6,将替换为与 RE 中相应组匹配的子字符串。 这使您可以在生成的替换字符串中合并部分原始文本。

此示例匹配单词 section 后跟括在 {} 中的字符串,并将 section 更改为 subsection

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

还有一种语法用于引用由 (?P<name>...) 语法定义的命名组。 \g<name>将使用名为name的组匹配的子串,\g<number>使用对应的组号。 \g<2> 因此等价于 \2,但在替换字符串(如 \g<2>0)中不会有歧义。 (\20 将被解释为对第 20 组的引用,而不是对后跟文字字符 '0' 的第 2 组的引用。)以下替换都是等效的,但使用了替换字符串。

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

replacement 也可以是一个函数,它给你更多的控制。 如果 replacement 是一个函数,则该函数会在 pattern 的每次非重叠出现时调用。 在每次调用时,该函数都会为匹配传递一个 匹配对象 参数,并且可以使用此信息来计算所需的替换字符串并返回它。

在以下示例中,替换函数将十进制转换为十六进制:

>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

当使用模块级 re.sub() 函数时,模式作为第一个参数传递。 模式可以作为对象或字符串提供; 如果需要指定正则表达式标志,则必须使用模式对象作为第一个参数,或者在模式字符串中使用嵌入的修饰符,例如 sub("(?i)b+", "x", "bbbb BBBB") 返回 'x x'


常见问题

对于某些应用程序来说,正则表达式是一种强大的工具,但在某些方面,它们的行为并不直观,有时它们的行为并不符合您的预期。 本节将指出一些最常见的陷阱。

使用字符串方法

有时使用 re 模块是错误的。 如果您匹配固定字符串或单个字符类,并且您没有使用任何 re 功能,例如 IGNORECASE 标志,那么正则表达式的全部功能可能不需要。 字符串有多种方法可以使用固定字符串执行操作,而且它们通常要快得多,因为实现是为此目的优化的单个小型 C 循环,而不是大型的、更通用的正则表达式引擎。

一个例子可能是用另一个固定字符串替换单个固定字符串; 例如,您可以将 word 替换为 deedre.sub() 似乎是用于此的函数,但请考虑 replace() 方法。 请注意,replace() 也将替换 word 内的单词,将 swordfish 变成 sdeedfish,但幼稚的 RE word 会这样做,也。 (为了避免对部分单词执行替换,模式必须是 \bword\b,以便要求 word 在任一侧都有一个单词边界。 这使工作超出了 replace() 的能力。)

另一个常见任务是从字符串中删除每个出现的单个字符或将其替换为另一个单个字符。 您可以使用 re.sub('\n', ' ', S) 之类的方法来执行此操作,但是 translate() 能够完成这两项任务,并且比任何正则表达式操作都快。

总之,在转向 re 模块之前,请考虑是否可以使用更快更简单的字符串方法来解决您的问题。


贪婪与非贪婪

当重复一个正则表达式时,如在 a* 中,结果动作是尽可能多地使用模式。 当您尝试匹配一对平衡的定界符(例如围绕 HTML 标记的尖括号)时,这一事实通常会困扰您。 由于 .* 的贪婪特性,匹配单个 HTML 标签的幼稚模式不起作用。

>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>

RE 匹配 '<html>' 中的 '<'.* 消耗字符串的其余部分。 但是,RE 中还有更多剩余,并且 > 无法在字符串末尾匹配,因此正则表达式引擎必须逐个字符地回溯,直到找到 >。 最终匹配从'<html>'中的'<'延伸到'</title>'中的'>',这不是你想要的。

在这种情况下,解决方案是使用非贪婪限定符 *?+???{m,n}?,它们匹配为 little 文字尽可能。 在上面的例子中,在第一个 '<' 匹配后立即尝试 '>',并且当它失败时,引擎一次前进一个字符,每次重试 '>'步。 这会产生正确的结果:

>>> print(re.match('<.*?>', s).group())
<html>

(请注意,使用正则表达式解析 HTML 或 XML 很痛苦。 快速和肮脏的模式将处理常见情况,但 HTML 和 XML 有特殊情况,会破坏明显的正则表达式; 当您编写处理所有可能情况的正则表达式时,模式将 非常 复杂。 将 HTML 或 XML 解析器模块用于此类任务。)


使用 re.VERBOSE

到现在为止,您可能已经注意到正则表达式是一种非常紧凑的表示法,但它们的可读性并不差。 中等复杂度的 RE 可能变成反斜杠、括号和元字符的冗长集合,使它们难以阅读和理解。

对于此类 RE,在编译正则表达式时指定 re.VERBOSE 标志会很有帮助,因为它允许您更清晰地格式化正则表达式。

re.VERBOSE 标志有多种效果。 字符类中的 不是 的正则表达式中的空格被忽略。 这意味着诸如 dog | cat 之类的表达式等效于可读性较差的 dog|cat,但 [a b] 仍会匹配字符 'a''b' ] 或空格。 此外,您还可以在 RE 中放置注释; 注释从 # 字符扩展到下一个换行符。 当与三引号字符串一起使用时,这可以使 RE 的格式更加整齐:

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

这比以下内容更具可读性:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

回馈

正则表达式是一个复杂的话题。 这份文件是否有助于您理解它们? 是否有不清楚的部分,或者您遇到的问题没有在这里涵盖? 如果是这样,请将改进建议发送给作者。

关于正则表达式的最完整的书几乎肯定是由 O'Reilly 出版的 Jeffrey Friedl 的 Mastering Regular Expressions。 不幸的是,它只专注于 Perl 和 Java 风格的正则表达式,根本不包含任何 Python 材料,因此它不能用作 Python 编程的参考。 (第一版涵盖了 Python 现已删除的 regex 模块,这对您没有多大帮助。)考虑从您的库中查看它。