re — 正则表达式操作 — Python 文档

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

re — 正则表达式操作

源代码: :source:`Lib/re.py`



该模块提供类似于 Perl 中的正则表达式匹配操作。

要搜索的模式和字符串都可以是 Unicode 字符串(str)以及 8 位字符串(bytes)。 但是,Unicode 字符串和 8 位字符串不能混合使用:即不能将 Unicode 字符串与字节模式匹配,反之亦然; 类似地,当要求替换时,替换字符串必须与模式和搜索字符串的类型相同。

正则表达式使用反斜杠字符 ('\') 来指示特殊形式或允许使用特殊字符而不调用其特殊含义。 这与 Python 在字符串文字中出于相同目的使用相同字符的做法相冲突; 例如,要匹配文字反斜杠,可能必须将 '\\\\' 写为模式字符串,因为正则表达式必须为 \\,并且每个反斜杠必须表示为 \\ ] 在常规 Python 字符串文字中。

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

需要注意的是,大多数正则表达式操作都可用作 编译的正则表达式 上的模块级函数和方法。 这些函数是不需要您首先编译正则表达式对象的快捷方式,但会遗漏一些微调参数。

也可以看看

第三方 regex 模块,其 API 与标准库 re 模块兼容,但提供了额外的功能和更全面的 Unicode 支持。


正则表达式语法

正则表达式(或 RE)指定一组与之匹配的字符串; 此模块中的函数可让您检查特定字符串是否与给定正则表达式匹配(或者给定正则表达式是否与特定字符串匹配,归结为同一件事)。

正则表达式可以连接起来形成新的正则表达式; 如果 AB 都是正则表达式,那么 AB 也是一个正则表达式。 一般来说,如果一个字符串 p 匹配 A 并且另一个字符串 q 匹配 B,那么字符串 pq 将匹配AB。 除非 AB 包含低优先级操作,否则这适用; AB之间的边界条件; 或有编号的组参考。 因此,复杂的表达式可以很容易地从更简单的原始表达式构造出来,比如这里描述的那些。 有关正则表达式的理论和实现的详细信息,请参阅 Friedl 书籍 [Frie09],或者几乎所有关于编译器构造的教科书。

下面简要说明正则表达式的格式。 如需更多信息和更温和的介绍,请参阅 正则表达式 HOWTO

正则表达式可以包含特殊字符和普通字符。 大多数普通字符,如 'A''a''0',都是最简单的正则表达式; 他们只是匹配自己。 您可以连接普通字符,因此 last 匹配字符串 'last'。 (在本节的其余部分,我们将在 this special style 中编写 RE,通常不带引号,以及要匹配的字符串 'in single quotes'。)

某些字符,例如 '|''(',是特殊的。 特殊字符要么代表普通字符的类别,要么影响它们周围的正则表达式的解释方式。

重复限定符(*+?{m,n}等)不能直接嵌套。 这避免了与非贪婪修饰符后缀 ? 以及其他实现中的其他修饰符的歧义。 要将第二次重复应用于内部重复,可以使用括号。 例如,表达式 (?:a{6})* 匹配六个 'a' 字符的任意倍数。

特殊字符是:

.
(点。)在默认模式下,这匹配除换行符之外的任何字符。 如果指定了 DOTALL 标志,则它匹配包括换行符在内的任何字符。
^
(Caret.) 匹配字符串的开头,在 MULTILINE 模式下也匹配紧接在每个换行符之后。
$
匹配字符串的结尾或字符串末尾的换行符之前,并且在 MULTILINE 模式下也匹配换行符之前。 foo 匹配 'foo' 和 'foobar',而正则表达式 foo$ 只匹配 'foo'。 更有趣的是,在 'foo1\nfoo2\n' 中搜索 foo.$ 正常匹配 'foo2',但在 MULTILINE 模式下搜索 'foo1'; 在 'foo\n' 中搜索单个 $ 将找到两个(空)匹配项:一个在换行符之前,一个在字符串的末尾。
*
使生成的 RE 匹配前面 RE 的 0 次或多次重复,尽可能多的重复。 ab* 将匹配“a”、“ab”或“a”后跟任意数量的“b”。
+
导致生成的 RE 匹配前面 RE 的 1 次或多次重复。 ab+ 将匹配“a”后跟任何非零数量的“b”; 它不会只匹配“a”。
?
导致生成的 RE 匹配前面 RE 的 0 或 1 次重复。 ab? 将匹配“a”或“ab”。
*?+???
'*''+''?'限定符都是贪婪; 它们匹配尽可能多的文本。 有时这种行为是不受欢迎的; 如果 RE <.*>'<a> b <c>' 匹配,它将匹配整个字符串,而不仅仅是 '<a>'。 在限定符后添加 ? 使其以 non-greedyminimal 方式执行匹配; 因为 将匹配尽可能少的 个字符。 使用 RE <.*?> 只会匹配 '<a>'
{m}
指定应该匹配前一个 RE 的 m 个副本; 较少的匹配会导致整个 RE 不匹配。 例如,a{6} 将正好匹配六个 'a' 字符,但不会匹配五个。
{m,n}
使生成的 RE 匹配从 mn 个重复前面的 RE,尝试匹配尽可能多的重复。 例如,a{3,5} 将匹配 3 到 5 个 'a' 字符。 省略 m 指定零的下限,省略 n 指定无限的上限。 例如,a{4,}b 将匹配 'aaaab' 或一千个 'a' 字符后跟一个 'b',但不匹配 'aaab'。 逗号不能省略,否则修饰符会与前面描述的形式混淆。
{m,n}?
使生成的 RE 匹配从 mn 个重复前面的 RE,尝试匹配尽可能少的 重复。 这是前一个限定符的非贪婪版本。 例如,在 6 个字符的字符串 'aaaaaa' 上,a{3,5} 将匹配 5 个 'a' 字符,而 a{3,5}? 仅匹配 3 个字符。
\

要么转义特殊字符(允许您匹配 '*''?' 等字符),要么表示特殊序列; 下面讨论特殊序列。

如果您不使用原始字符串来表达模式,请记住 Python 还使用反斜杠作为字符串文字中的转义序列; 如果 Python 的解析器无法识别转义序列,则反斜杠和后续字符将包含在结果字符串中。 但是,如果 Python 能够识别结果序列,则反斜杠应重复两次。 这很复杂且难以理解,因此强烈建议您对除最简单的表达式之外的所有表达式都使用原始字符串。

[]

用于表示一组字符。 在一组中:

  • 字符可以单独列出,例如 [amk] 将匹配 'a''m''k'

  • 字符范围可以通过给出两个字符并用 '-' 分隔来表示,例如 [a-z] 将匹配任何小写 ASCII 字母,[0-5][0-9] 将匹配所有两位数从 0059[0-9A-Fa-f] 的数字将匹配任何十六进制数字。 如果 - 被转义(例如 [a\-z]) 或者如果它作为第一个或最后一个字符放置(例如 [-a][a-]),它将匹配文字 '-'

  • 特殊字符在集合内失去其特殊含义。 例如,[(+*)] 将匹配任何文字字符 '(''+''*'')'

  • 字符类,例如 \w\S(定义如下)也可以在集合中使用,尽管它们匹配的字符取决于是 ASCII 还是 LOCALE ] 模式生效。

  • 不在范围内的字符可以通过 补充 集合来匹配。 如果集合的第一个字符是 '^',则集合中所有 不是 的字符都将被匹配。 例如,[^5] 将匹配除 '5' 之外的任何字符,而 [^^] 将匹配除 '^' 之外的任何字符。 ^ 如果不是集合中的第一个字符,则没有特殊含义。

  • 要匹配集合内的文字 ']',请在其前面加上反斜杠,或将其放在集合的开头。 例如,[()[\]{}][]()[{}] 都将匹配一个括号。

  • 将来可能会添加对 Unicode 技术标准 #18 中的嵌套集合和集合操作的支持。 这会改变语法,因此为了促进这种改变,暂时将在不明确的情况下引发 FutureWarning。 这包括以文字 '[' 开头或包含文字字符序列 '--''&&''~~''||' 的集合。 为避免警告,请使用反斜杠将它们转义。

在 3.7 版中更改:如果字符集包含将来会在语义上发生变化的构造,则会引发 FutureWarning

|
A|B,其中 AB 可以是任意 RE,创建一个匹配 AB 的正则表达式]。 '|' 可以通过这种方式分隔任意数量的 RE。 这也可以在组内使用(见下文)。 在扫描目标字符串时,从左到右尝试由 '|' 分隔的 RE。 当一个模式完全匹配时,该分支被接受。 这意味着一旦 A 匹配,B 将不会被进一步测试,即使它会产生更长的整体匹配。 换句话说,'|' 运算符从不贪婪。 要匹配文字 '|',请使用 \|,或将其包含在字符类中,如 [|]
(...)
匹配括号内的任何正则表达式,并指示组的开始和结束; 执行匹配后可以检索组的内容,并且可以稍后在字符串中使用 \number 特殊序列进行匹配,如下所述。 要匹配文字 '('')',请使用 \(\),或将它们包含在字符类中:[(][)]
(?...)
这是一个扩展符号('(' 后面的 '?' 没有其他意义)。 '?' 之后的第一个字符决定了构造的含义和进一步的语法是什么。 扩展通常不会创建新组; (?P<name>...) 是此规则的唯一例外。 以下是当前支持的扩展。
(?aiLmsux)
(一组或多个字母 'a''i''L''m''s''u''x'.) 组匹配空字符串; 字母设置相应的标志:re.A(仅 ASCII 匹配)、re.I(忽略大小写)、re.L(取决于语言环境) 、re.M(多行)、re.S(点匹配所有)、re.U(Unicode 匹配)和 re.X[ X400X](详细),用于整个正则表达式。 (这些标志在 模块内容 中进行了描述。)如果您希望将标志作为正则表达式的一部分而不是将 标志 参数传递给 ,这将非常有用re.compile() 函数。 应首先在表达式字符串中使用标志。
(?:...)

常规括号的非捕获版本。 匹配括号内的任何正则表达式,但组 匹配的子字符串在执行匹配后无法检索或在模式后面引用。

(?aiLmsux-imsx:...)

(来自 'a''i''L''m''s''u''x',可选后跟 '-' 后跟 'i''m''s''x' 中的一个或多个字母].) 字母设置或删除相应的标志:re.A(仅ASCII匹配),re.I(忽略大小写),re.L ](取决于语言环境)、re.M(多行)、re.S(点匹配所有)、re.U(Unicode 匹配)和 ]re.X(详细),用于表达式的一部分。 (这些标志在 模块内容 中有描述。)

字母 'a''L''u' 在用作内联标志时是互斥的,因此它们不能组合或跟在 '-' 之后。 相反,当其中一个出现在内联组中时,它会覆盖封闭组中的匹配模式。 在 Unicode 模式中,(?a:...) 切换到仅 ASCII 匹配,而 (?u:...) 切换到 Unicode 匹配(默认)。 在字节模式中 (?L:...) 切换到区域设置依赖匹配,而 (?a:...) 切换到仅 ASCII 匹配(默认)。 此覆盖仅对窄内联组有效,在组外恢复原始匹配模式。

3.6 版中的新功能。

3.7 版本更改: 字母 'a''L''u' 也可以在一个组中使用。

(?P<name>...)

与常规括号类似,但组匹配的子字符串可通过符号组名 name 访问。 组名必须是有效的 Python 标识符,并且每个组名只能在正则表达式中定义一次。 符号组也是编号组,就好像该组没有命名一样。

命名组可以在三种上下文中引用。 如果模式是 (?P<quote>['"]).*?(?P=quote)(即 匹配用单引号或双引号引用的字符串):

引用组“引用”的上下文

参考方法

在相同的模式本身

  • (?P=quote)(如图)

  • \1

处理匹配对象时 m

  • m.group('quote')

  • m.end('quote')(等)

在传递给 re.sub()repl 参数的字符串中

  • \g<quote>

  • \g<1>

  • \1

(?P=name)
对命名组的反向引用; 它匹配名为 name 的较早组匹配的任何文本。
(?#...)
A comment; the contents of the parentheses are simply ignored.
(?=...)
如果 ... 下一个匹配,则匹配,但不消耗任何字符串。 这称为 超前断言 。 例如,Isaac (?=Asimov) 仅在其后跟 'Asimov' 时才会匹配 'Isaac '
(?!...)
如果 ... 下一个不匹配,则匹配。 这是一个 否定前瞻断言 。 例如,Isaac (?!Asimov) 将匹配 'Isaac ' 仅当它是 not 后跟 'Asimov'
(?<=...)

如果字符串中的当前位置前面是在当前位置结束的 ... 匹配项,则匹配。 这称为 肯定的后视断言(?<=abc)def 将在 'abcdef' 中找到匹配项,因为lookbehind 将备份 3 个字符并检查包含的模式是否匹配。 包含的模式必须只匹配某些固定长度的字符串,这意味着 abca|b 是允许的,但 a*a{3,4} 不是。 请注意,以肯定的后视断言开头的模式将不匹配正在搜索的字符串的开头; 您很可能希望使用 search() 函数而不是 match() 函数:

>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef')
>>> m.group(0)
'def'

此示例查找连字符后的单词:

>>> m = re.search(r'(?<=-)\w+', 'spam-egg')
>>> m.group(0)
'egg'

3.5 版更改: 增加了对固定长度组引用的支持。

(?<!...)
如果字符串中的当前位置前面没有 ... 的匹配项,则匹配。 这称为 否定后视断言 。 与正向后视断言类似,包含的模式必须只匹配某个固定长度的字符串。 以否定的后视断言开头的模式可能会在被搜索字符串的开头匹配。
(?(id/name)yes-pattern|no-pattern)
如果具有给定 idname 的组存在,将尝试与 yes-pattern 匹配,如果不存在,则与 no-pattern 匹配。 no-pattern 是可选的,可以省略。 例如,(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$) 是一个糟糕的电子邮件匹配模式,它将与 '<user@host.com>''user@host.com' 匹配,但不能与 '<user@host.com' 或 [ X142X]。

特殊序列由 '\' 和下表中的一个字符组成。 如果普通字符不是 ASCII 数字或 ASCII 字母,则生成的 RE 将匹配第二个字符。 例如,\$ 匹配字符 '$'

\number
匹配相同号码组的内容。 组从 1 开始编号。 例如,(.+) \1 匹配 'the the''55 55',但不匹配 'thethe'(注意组后面的空格)。 此特殊序列只能用于匹配前 99 个组中的一个。 如果 number 的第一位为 0,或者 number 是 3 个八进制数字,则不会被解释为组匹配,而是被解释为具有八进制值 number 的字符。 在字符类的 '['']' 内,所有数字转义都被视为字符。
\A
仅在字符串的开头匹配。
\b

匹配空字符串,但仅在单词的开头或结尾。 一个词被定义为一个词字符序列。 请注意,形式上,\b 被定义为 \w\W 字符之间的边界(反之亦然),或 \w 和开头/字符串的结尾。 这意味着 r'\bfoo\b' 匹配 'foo''foo.''(foo)''bar foo baz' 但不匹配 'foobar' 或 [ X103X]。

默认情况下,Unicode 字母数字是在 Unicode 模式中使用的,但这可以通过使用 ASCII 标志进行更改。 如果使用 LOCALE 标志,则字边界由当前语言环境确定。 在字符范围内, \b 表示退格字符,以与 Python 的字符串文字兼容。

\B
匹配空字符串,但仅当它在单词的开头或结尾处为 而非 时。 这意味着 r'py\B' 匹配 'python''py3''py2',但不匹配 'py''py.''py!'\B\b 正好相反,因此 Unicode 模式中的单词字符是 Unicode 字母数字或下划线,尽管这可以通过使用 ASCII 标志进行更改。 如果使用 LOCALE 标志,则字边界由当前语言环境确定。
\d
;; 对于 Unicode (str) 模式:
匹配任何 Unicode 十进制数字(即 Unicode 字符类别 [Nd] 中的任何字符)。 这包括 [0-9] 以及许多其他数字字符。 如果使用 ASCII 标志,则仅匹配 [0-9]
对于 8 位(字节)模式:
匹配任何十进制数字; 这相当于 [0-9]
\D
匹配任何非十进制数字的字符。 这与\d相反。 如果使用 ASCII 标志,则这相当于 [^0-9]
\s
;; 对于 Unicode (str) 模式:
匹配 Unicode 空白字符(包括 [ \t\n\r\f\v] 以及许多其他字符,例如许多语言中的排版规则要求的不间断空格)。 如果使用 ASCII 标志,则仅匹配 [ \t\n\r\f\v]
对于 8 位(字节)模式:
匹配 ASCII 字符集中被视为空白的字符; 这相当于 [ \t\n\r\f\v]
\S
匹配任何不是空白字符的字符。 这与\s相反。 如果使用 ASCII 标志,则这相当于 [^ \t\n\r\f\v]
\w
;; 对于 Unicode (str) 模式:
匹配 Unicode 单词字符; 这包括可以成为任何语言中单词一部分的大多数字符,以及数字和下划线。 如果使用 ASCII 标志,则仅匹配 [a-zA-Z0-9_]
对于 8 位(字节)模式:
匹配 ASCII 字符集中被视为字母数字的字符; 这相当于 [a-zA-Z0-9_]。 如果使用 LOCALE 标志,则匹配当前语言环境中被视为字母数字的字符和下划线。
\W
匹配任何不是单词字符的字符。 这与\w相反。 如果使用 ASCII 标志,则这相当于 [^a-zA-Z0-9_]。 如果使用 LOCALE 标志,则匹配既不是当前语言环境中的字母数字也不是下划线的字符。
\Z
仅在字符串末尾匹配。

Python 字符串文字支持的大多数标准转义符也被正则表达式解析器接受:

\a      \b      \f      \n
\r      \t      \u      \U
\v      \x      \\

(请注意,\b 用于表示单词边界,仅在字符类内部表示“退格”。)

'\u''\U' 转义序列只能在 Unicode 模式中识别。 在字节模式中,它们是错误。 ASCII 字母的未知转义保留供将来使用并视为错误。

八进制转义包含在有限的形式中。 如果第一个数字是 0,或者如果有三个八进制数字,则认为是八进制转义。 否则,它是一个组引用。 至于字符串文字,八进制转义的长度总是最多三位数。

3.3 版更改: 添加了 '\u''\U' 转义序列。


3.6 版更改: '\' 和 ASCII 字母组成的未知转义现在是错误。


模块内容

该模块定义了几个函数、常量和一个异常。 一些函数是编译正则表达式的全功能方法的简化版本。 大多数重要的应用程序总是使用编译形式。

3.6 版更改: 标志常量现在是 RegexFlag 的实例,它是 enum.IntFlag 的子类。


re.compile(pattern, flags=0)

将正则表达式模式编译成正则表达式对象,可以使用其match()search()等方法进行匹配,如下所述.

可以通过指定 flags 值来修改表达式的行为。 值可以是以下任何变量,使用按位或(| 运算符)组合。

序列

prog = re.compile(pattern)
result = prog.match(string)

相当于

result = re.match(pattern, string)

但是当表达式将在单个程序中多次使用时,使用 re.compile() 并保存生成的正则表达式对象以供重用会更有效。

笔记

传递给 re.compile() 的最新模式的编译版本和模块级匹配函数被缓存,因此一次只使用几个正则表达式的程序无需担心编译正则表达式表达式。

re.A
re.ASCII

制作 \w\W\b\B\d\D\s\S 只执行 ASCII 匹配而不是完整的 Unicode 匹配。 这仅对 Unicode 模式有意义,而对于字节模式则被忽略。 对应于内联标志 (?a)

请注意,为了向后兼容,re.U 标志仍然存在(以及它的同义词 re.UNICODE 及其嵌入的对应物 (?u)),但这些在 Python 3 中是多余的,因为匹配是默认情况下,字符串使用 Unicode(并且字节不允许使用 Unicode 匹配)。

re.DEBUG
显示有关编译表达式的调试信息。 没有相应的内联标志。
re.I
re.IGNORECASE

执行不区分大小写的匹配; 像 [A-Z] 这样的表达式也会匹配小写字母。 完全 Unicode 匹配(例如 Ü 匹配 ü)也有效,除非使用 re.ASCII 标志禁用非 ASCII 匹配。 当前语言环境不会更改此标志的效果,除非还使用了 re.LOCALE 标志。 对应于内联标志 (?i)

请注意,当 Unicode 模式 [a-z][A-Z]IGNORECASE 标志结合使用时,它们将匹配 52 个 ASCII 字母和 4 个额外的非 ASCII 字母:' İ'(U+0130,拉丁大写字母 I,上面有点),'ı'(U+0131,拉丁小写字母无点 i),'ſ'(U+017F,拉丁小写字母长 s)和 'K'( U+212A,开尔文符号)。 如果使用 ASCII 标志,则仅匹配字母“a”到“z”和“A”到“Z”。

re.L
re.LOCALE

使 \w\W\b\B 和不区分大小写的匹配依赖于当前语言环境。 此标志只能与字节模式一起使用。 不鼓励使用此标志,因为语言环境机制非常不可靠,它一次仅处理一种“文化”,并且仅适用于 8 位语言环境。 Unicode 匹配已经在 Python 3 中默认启用,用于 Unicode (str) 模式,并且它能够处理不同的区域设置/语言。 对应于内联标志 (?L)

3.6 版更改: re.LOCALE 只能用于字节模式,与 re.ASCII 不兼容。

3.7 版更改: 编译时带有 re.LOCALE 标志的正则表达式对象不再依赖于编译时的语言环境。 只有匹配时的语言环境会影响匹配结果。

re.M

re.MULTILINE

指定时,模式字符 '^' 匹配字符串的开头和每行的开头(紧跟在每个换行符之后); 并且模式字符 '$' 在字符串的末尾和每行的末尾(紧接在每个换行符之前)匹配。 默认情况下, '^' 仅匹配字符串的开头,而 '$' 仅匹配字符串的末尾和字符串末尾的换行符(如果有)之前。 对应于内联标志 (?m)
re.S

re.DOTALL

使 '.' 特殊字符匹配任何字符,包括换行符; 如果没有这个标志, '.' 将匹配任何 除了 换行符。 对应于内联标志 (?s)
re.X
re.VERBOSE

此标志允许您通过允许您在视觉上分隔模式的逻辑部分并添加注释来编写看起来更好且更具可读性的正则表达式。 模式中的空格会被忽略,除非在字符类中,或者前面有未转义的反斜杠,或者在 *?(?:(?P<...> 之类的标记中。 当一行包含不在字符类中且前面没有未转义反斜杠的 # 时,从最左边的 # 到行尾的所有字符都将被忽略。

这意味着以下两个匹配十进制数的正则表达式对象在功能上是相等的:

a = re.compile(r"""\d +  # the integral part
                   \.    # the decimal point
                   \d *  # some fractional digits""", re.X)
b = re.compile(r"\d+\.\d*")

对应于内联标志 (?x)

re.search(pattern, string, flags=0)
扫描 string 查找正则表达式 pattern 产生匹配的第一个位置,并返回相应的 匹配对象 。 如果字符串中没有位置与模式匹配,则返回 None; 请注意,这与在字符串中的某个点找到零长度匹配不同。
re.match(pattern, string, flags=0)

如果 string 开头的零个或多个字符与正则表达式 pattern 匹配,则返回相应的 匹配对象 。 如果字符串与模式不匹配,则返回 None; 请注意,这与零长度匹配不同。

请注意,即使在 MULTILINE 模式下,re.match() 也只会匹配字符串的开头,而不匹配每行的开头。

如果你想在任何地方找到匹配细绳 , 用搜索() 相反(另见搜索()与 比赛() )。

re.fullmatch(pattern, string, flags=0)

如果整个 string 匹配正则表达式 pattern,则返回对应的 匹配对象 。 如果字符串与模式不匹配,则返回 None; 请注意,这与零长度匹配不同。

3.4 版中的新功能。

re.split(pattern, string, maxsplit=0, flags=0)

通过 模式 的出现拆分 字符串 。 如果在 pattern 中使用捕获括号,则模式中所有组的文本也会作为结果列表的一部分返回。 如果 maxsplit 不为零,则最多发生 maxsplit 次拆分,并且字符串的其余部分作为列表的最后一个元素返回。

>>> 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.']
>>> re.split('[a-f]+', '0a3B9', flags=re.IGNORECASE)
['0', '3', '9']

如果分隔符中有捕获组并且它在字符串的开头匹配,则结果将以空字符串开头。 这同样适用于字符串的结尾:

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

这样,分隔符组件总是可以在结果列表中的相同相对索引处找到。

模式的空匹配仅在与前一个空匹配不相邻时才拆分字符串。

>>> re.split(r'\b', 'Words, words, words.')
['', 'Words', ', ', 'words', ', ', 'words', '.']
>>> re.split(r'\W*', '...words...')
['', '', 'w', 'o', 'r', 'd', 's', '', '']
>>> re.split(r'(\W*)', '...words...')
['', '...', '', '', 'w', '', 'o', '', 'r', '', 'd', '', 's', '...', '', '', '']

在 3.1 版更改: 添加了可选的标志参数。

3.7 版更改: 添加了对可以匹配空字符串的模式的拆分支持。

re.findall(pattern, string, flags=0)

以字符串列表的形式返回 stringpattern 的所有非重叠匹配项。 string 从左到右扫描,并按找到的顺序返回匹配项。 如果模式中存在一个或多个组,则返回组列表; 如果模式有多个组,这将是一个元组列表。 结果中包含空匹配项。

3.7 版更改: 非空匹配现在可以在前一个空匹配之后开始。

re.finditer(pattern, string, flags=0)

返回一个 迭代器 ,在 字符串 中的 RE 模式 的所有非重叠匹配上产生 匹配对象string 从左到右扫描,并按找到的顺序返回匹配项。 结果中包含空匹配项。

3.7 版更改: 非空匹配现在可以在前一个空匹配之后开始。

re.sub(pattern, repl, string, count=0, flags=0)

返回通过替换 repl 替换 stringpattern 最左边非重叠出现的字符串。 如果未找到模式,则返回 string 不变。 repl 可以是字符串也可以是函数; 如果它是一个字符串,则处理其中的任何反斜杠转义。 即,\n 转换为单个换行符,\r 转换为回车,依此类推。 ASCII 字母的未知转义保留供将来使用并视为错误。 其他未知的转义如 \& 被单独留下。 反向引用,例如 \6,被替换为模式中第 6 组匹配的子字符串。 例如:

>>> re.sub(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):',
...        r'static PyObject*\npy_\1(void)\n{',
...        'def myfunc():')
'static PyObject*\npy_myfunc(void)\n{'

如果 repl 是一个函数,它会在 模式 的每次非重叠出现时调用。 该函数采用单个 匹配对象 参数,并返回替换字符串。 例如:

>>> def dashrepl(matchobj):
...     if matchobj.group(0) == '-': return ' '
...     else: return '-'
>>> re.sub('-{1,2}', dashrepl, 'pro----gram-files')
'pro--gram files'
>>> re.sub(r'\sAND\s', ' & ', 'Baked Beans And Spam', flags=re.IGNORECASE)
'Baked Beans & Spam'

模式可以是字符串或 模式对象

可选参数 count 是要替换的模式出现的最大次数; count 必须是非负整数。 如果省略或为零,则将替换所有出现的内容。 模式的空匹配仅在与前一个空匹配不相邻时才被替换,因此 sub('x*', '-', 'abxd') 返回 '-a-b--d-'

在字符串类型 repl 参数中,除了上述字符转义和反向引用外,\g<name> 将使用名为 name 的组匹配的子字符串,如定义(?P<name>...) 语法。 \g<number>使用对应的组号; \g<2> 因此等价于 \2,但在 \g<2>0 之类的替换中并不含糊。 \20 将被解释为对组 20 的引用,而不是对组 2 的引用,后跟文字字符 '0'。 反向引用 \g<0> 替换了 RE 匹配的整个子字符串。

在 3.1 版更改: 添加了可选的标志参数。

3.5 版更改: 不匹配的组被替换为空字符串。

3.6 版更改: 模式 中由 '\' 和 ASCII 字母组成的未知转义现在是错误。

3.7 版更改: repl 中由 '\' 和 ASCII 字母组成的未知转义现在是错误。

3.7 版更改: 模式的空匹配在与前一个非空匹配相邻时被替换。

re.subn(pattern, repl, string, count=0, flags=0)

执行与 sub() 相同的操作,但返回一个元组 (new_string, number_of_subs_made)

在 3.1 版更改: 添加了可选的标志参数。

3.5 版更改: 不匹配的组被替换为空字符串。

re.escape(pattern)

转义 模式 中的特殊字符。 如果您想匹配可能包含正则表达式元字符的任意文字字符串,这将非常有用。 例如:

>>> print(re.escape('http://www.python.org'))
http://www\.python\.org

>>> legal_chars = string.ascii_lowercase + string.digits + "!#$%&'*+-.^_`|~:"
>>> print('[%s]+' % re.escape(legal_chars))
[abcdefghijklmnopqrstuvwxyz0123456789!\#\$%\&'\*\+\-\.\^_`\|\~:]+

>>> operators = ['+', '-', '*', '/', '**']
>>> print('|'.join(map(re.escape, sorted(operators, reverse=True))))
/|\-|\+|\*\*|\*

此函数不得用于 sub()subn() 中的替换字符串,只应转义反斜杠。 例如:

>>> digits_re = r'\d+'
>>> sample = '/usr/sbin/sendmail - 0 errors, 12 warnings'
>>> print(re.sub(digits_re, digits_re.replace('\\', r'\\'), sample))
/usr/sbin/sendmail - \d+ errors, \d+ warnings

3.3 版更改:'_' 字符不再转义。

3.7 版本变更: 仅对正则表达式中具有特殊含义的字符进行转义。 结果,'!''"''%'"'"',''/'、[ X83X]、';''<''=''>''@'"`"不再转义。

re.purge()
清除正则表达式缓存。
exception re.error(msg, pattern=None, pos=None)

当传递给此处函数之一的字符串不是有效的正则表达式(例如,它可能包含不匹配的括号)或在编译或匹配期间发生其他一些错误时引发异常。 如果字符串不包含与模式匹配的内容,则永远不会出错。 错误实例具有以下附加属性:

msg

未格式化的错误消息。

pattern

正则表达式模式。

pos

pattern 中编译失败的索引(可能是 None)。

lineno

对应于 pos 的行(可能是 None)。

colno

对应于pos的列(可能是None)。

3.5 版更改: 添加了附加属性。


正则表达式对象

编译后的正则表达式对象支持以下方法和属性:

Pattern.search(string[, pos[, endpos]])

扫描 string 查找此正则表达式产生匹配的第一个位置,并返回相应的 匹配对象 。 如果字符串中没有位置与模式匹配,则返回 None; 请注意,这与在字符串中的某个点找到零长度匹配不同。

可选的第二个参数 pos 给出字符串中开始搜索的索引; 默认为 0。 这并不完全等同于对字符串进行切片; '^' 模式字符在字符串的真正开头和换行符之后的位置匹配,但不一定在搜索开始的索引处。

可选参数 endpos 限制字符串的搜索范围; 就好像字符串是 endpos 个字符长,所以只有从 posendpos - 1 的字符会被搜索匹配。 如果 endpos 小于 pos,则找不到匹配项; 否则,如果 rx 是编译后的正则表达式对象,则 rx.search(string, 0, 50) 等价于 rx.search(string[:50], 0)

>>> pattern = re.compile("d")
>>> pattern.search("dog")     # Match at index 0
<re.Match object; span=(0, 1), match='d'>
>>> pattern.search("dog", 1)  # No match; search doesn't include the "d"
Pattern.match(string[, pos[, endpos]])

如果 stringbeginning 处的零个或多个字符与此正则表达式匹配,则返回相应的 匹配对象 。 如果字符串与模式不匹配,则返回 None; 请注意,这与零长度匹配不同。

可选的 posendpos 参数与 search() 方法具有相同的含义。

>>> pattern = re.compile("o")
>>> pattern.match("dog")      # No match as "o" is not at the start of "dog".
>>> pattern.match("dog", 1)   # Match as "o" is the 2nd character of "dog".
<re.Match object; span=(1, 2), match='o'>

如果你想在任何地方找到匹配细绳 , 用搜索() 相反(另见搜索()与 比赛() )。

Pattern.fullmatch(string[, pos[, endpos]])

如果整个 string 匹配此正则表达式,则返回相应的 匹配对象 。 如果字符串与模式不匹配,则返回 None; 请注意,这与零长度匹配不同。

可选的 posendpos 参数与 search() 方法具有相同的含义。

>>> pattern = re.compile("o[gh]")
>>> pattern.fullmatch("dog")      # No match as "o" is not at the start of "dog".
>>> pattern.fullmatch("ogre")     # No match as not the full string matches.
>>> pattern.fullmatch("doggie", 1, 3)   # Matches within given limits.
<re.Match object; span=(1, 3), match='og'>

3.4 版中的新功能。

Pattern.split(string, maxsplit=0)
split() 函数相同,使用编译模式。
Pattern.findall(string[, pos[, endpos]])
类似于 findall() 函数,使用编译模式,但也接受可选的 posendpos 参数,用于限制搜索区域,如 search ()
Pattern.finditer(string[, pos[, endpos]])
类似于 finditer() 函数,使用编译模式,但也接受可选的 posendpos 参数,用于限制搜索区域,如 search ()
Pattern.sub(repl, string, count=0)
sub() 函数相同,使用编译模式。
Pattern.subn(repl, string, count=0)
subn() 函数相同,使用编译模式。
Pattern.flags
正则表达式匹配标志。 这是给予 compile() 的标志、模式中的任何 (?...) 内联标志以及隐式标志(例如 UNICODE)(如果模式是 Unicode 字符串)的组合.
Pattern.groups
模式中捕获组的数量。
Pattern.groupindex
(?P<id>) 定义的任何符号组名称映射到组编号的字典。 如果模式中没有使用符号组,则字典为空。
Pattern.pattern
从中编译模式对象的模式字符串。

3.7 版更改: 添加了对 copy.copy()copy.deepcopy() 的支持。 编译的正则表达式对象被认为是原子的。


匹配对象

匹配对象的布尔值始终为 True。 由于 match()search() 在没有匹配时返回 None,您可以使用简单的 if 测试是否匹配陈述:

match = re.search(pattern, string)
if match:
    process(match)

匹配对象支持以下方法和属性:

Match.expand(template)

返回通过对模板字符串 template 执行反斜杠替换获得的字符串,如 sub() 方法所做的那样。 转义如 \n 被转换为适当的字符,数字反向引用 (\1, \2) 和命名反向引用 (\g<1>, \g<name> ]) 被相应组的内容替换。

3.5 版更改: 不匹配的组被替换为空字符串。

Match.group([group1, ...])

返回匹配的一个或多个子组。 如果只有一个参数,则结果是一个字符串; 如果有多个参数,则结果是一个元组,每个参数一个项目。 没有参数, group1 默认为零(返回整个匹配项)。 如果 groupN 参数为零,则对应的返回值是整个匹配字符串; 如果在包含范围[1..99]内,则为与相应括号组匹配的字符串。 如果组号为负数或大于模式中定义的组数,则会引发 IndexError 异常。 如果某个组包含在不匹配的模式部分中,则对应的结果为 None。 如果某个组包含在多次匹配的模式部分中,则返回最后一个匹配项。

>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
>>> m.group(0)       # The entire match
'Isaac Newton'
>>> m.group(1)       # The first parenthesized subgroup.
'Isaac'
>>> m.group(2)       # The second parenthesized subgroup.
'Newton'
>>> m.group(1, 2)    # Multiple arguments give us a tuple.
('Isaac', 'Newton')

如果正则表达式使用 (?P<name>...) 语法,groupN 参数也可以是通过组名标识组的字符串。 如果字符串参数未用作模式中的组名,则会引发 IndexError 异常。

一个中等复杂的例子:

>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>> m.group('first_name')
'Malcolm'
>>> m.group('last_name')
'Reynolds'

命名组也可以通过它们的索引来引用:

>>> m.group(1)
'Malcolm'
>>> m.group(2)
'Reynolds'

如果一个组匹配多次,则只能访问最后一个匹配:

>>> m = re.match(r"(..)+", "a1b2c3")  # Matches 3 times.
>>> m.group(1)                        # Returns only the last match.
'c3'
Match.__getitem__(g)

这与 m.group(g) 相同。 这样可以更轻松地从比赛中访问单个组:

>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
>>> m[0]       # The entire match
'Isaac Newton'
>>> m[1]       # The first parenthesized subgroup.
'Isaac'
>>> m[2]       # The second parenthesized subgroup.
'Newton'

3.6 版中的新功能。

Match.groups(default=None)

返回一个包含匹配所有子组的元组,从 1 到模式中有多少个组。 default 参数用于未参加比赛的组; 默认为 None

例如:

>>> m = re.match(r"(\d+)\.(\d+)", "24.1632")
>>> m.groups()
('24', '1632')

如果我们将小数点及其后的所有内容设为可选,则并非所有组都可能参加比赛。 这些组将默认为 None,除非给出 default 参数:

>>> m = re.match(r"(\d+)\.?(\d+)?", "24")
>>> m.groups()      # Second group defaults to None.
('24', None)
>>> m.groups('0')   # Now, the second group defaults to '0'.
('24', '0')
Match.groupdict(default=None)

返回包含匹配的所有 named 子组的字典,以子组名称为键。 default 参数用于未参加比赛的组; 默认为 None。 例如:

>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>> m.groupdict()
{'first_name': 'Malcolm', 'last_name': 'Reynolds'}
Match.start([group])
Match.end([group])

返回group匹配的子串首尾索引; group 默认为零(意味着整个匹配的子字符串)。 如果 group 存在但没有参与比赛,则返回 -1。 对于匹配对象 m 和对匹配有贡献的组 g,组 g(相当于 m.group(g))匹配的子串) 是

m.string[m.start(g):m.end(g)]

请注意,如果 group 匹配空字符串,则 m.start(group) 将等于 m.end(group)。 例如m = re.search('b(c?)', 'cba')后,m.start(0)为1,m.end(0)为2,m.start(1)m.end(1)均为2,m.start(2) 引发 IndexError 异常。

从电子邮件地址中删除 remove_this 的示例:

>>> email = "tony@tiremove_thisger.net"
>>> m = re.search("remove_this", email)
>>> email[:m.start()] + email[m.end():]
'tony@tiger.net'
Match.span([group])
对于匹配 m,返回 2 元组 (m.start(group), m.end(group))。 请注意,如果 group 对比赛没有贡献,则为 (-1, -1)group 默认为零,整个匹配。
Match.pos
传递给 正则表达式对象search()match() 方法的 pos 的值。 这是 RE 引擎开始寻找匹配的字符串的索引。
Match.endpos
传递给 正则表达式对象search()match() 方法的 endpos 的值。 这是 RE 引擎不会超过的字符串索引。
Match.lastindex
最后匹配的捕获组的整数索引,如果根本没有匹配的组,则为 None。 例如,表达式 (a)b((a)(b))((ab)) 如果应用于字符串 'ab' 将具有 lastindex == 1,而表达式 [ X142X] 将有 lastindex == 2,如果应用于相同的字符串。
Match.lastgroup
最后匹配的捕获组的名称,如果组没有名称,或者根本没有匹配的组,则为 None
Match.re
正则表达式对象match()search()方法产生了这个匹配实例。
Match.string
传递给 match()search() 的字符串。

3.7 版更改: 添加了对 copy.copy()copy.deepcopy() 的支持。 匹配对象被认为是原子的。


正则表达式示例

检查一对

在这个例子中,我们将使用以下辅助函数来更优雅地显示匹配对象:

def displaymatch(match):
    if match is None:
        return None
    return '<Match: %r, groups=%r>' % (match.group(), match.groups())

假设您正在编写一个扑克程序,其中玩家的手牌表示为 5 个字符的字符串,每个字符代表一张牌,“a”代表 A,“k”代表国王,“q”代表皇后,“j”代表 jack, “t”代表 10,“2”到“9”代表具有该值的卡片。

要查看给定的字符串是否有效,可以执行以下操作:

>>> valid = re.compile(r"^[a2-9tjqk]{5}$")
>>> displaymatch(valid.match("akt5q"))  # Valid.
"<Match: 'akt5q', groups=()>"
>>> displaymatch(valid.match("akt5e"))  # Invalid.
>>> displaymatch(valid.match("akt"))    # Invalid.
>>> displaymatch(valid.match("727ak"))  # Valid.
"<Match: '727ak', groups=()>"

最后一手牌 "727ak" 包含一对或两张价值相同的牌。 要将其与正则表达式匹配,可以使用反向引用:

>>> pair = re.compile(r".*(.).*\1")
>>> displaymatch(pair.match("717ak"))     # Pair of 7s.
"<Match: '717', groups=('7',)>"
>>> displaymatch(pair.match("718ak"))     # No pairs.
>>> displaymatch(pair.match("354aa"))     # Pair of aces.
"<Match: '354aa', groups=('a',)>"

要找出这对卡片由什么卡片组成,可以按以下方式使用匹配对象的 group() 方法:

>>> pair.match("717ak").group(1)
'7'

# Error because re.match() returns None, which doesn't have a group() method:
>>> pair.match("718ak").group(1)
Traceback (most recent call last):
  File "<pyshell#23>", line 1, in <module>
    re.match(r".*(.).*\1", "718ak").group(1)
AttributeError: 'NoneType' object has no attribute 'group'

>>> pair.match("354aa").group(1)
'a'

模拟 scanf()

Python 目前没有与 scanf() 等效的东西。 正则表达式通常比 scanf() 格式字符串更强大,但也更冗长。 下表提供了 scanf() 格式标记和正则表达式之间或多或少的等效映射。

scanf() 代币 正则表达式
%c .
%5c .{5}
%d [-+]?\d+
%e%E%f%g \.\d+)([eE][-+]?\d+)?
%i 0[0-7]*|\d+)
%o [-+]?[0-7]+
%s \S+
%u \d+
%x%X [-+]?(0[xX])?[\dA-Fa-f]+

从像这样的字符串中提取文件名和数字

/usr/sbin/sendmail - 0 errors, 4 warnings

您将使用 scanf() 格式,例如

%s - %d errors, %d warnings

等效的正则表达式是

(\S+) - (\d+) errors, (\d+) warnings

搜索()与 比赛()

Python 提供了两种不同的基于正则表达式的原始操作: re.match() 只在字符串的开头检查匹配,而 re.search() 检查匹配字符串中的任何位置(这是 Perl 默认所做的)。

例如:

>>> re.match("c", "abcdef")    # No match
>>> re.search("c", "abcdef")   # Match
<re.Match object; span=(2, 3), match='c'>

'^' 开头的正则表达式可以与 search() 一起使用来限制字符串开头的匹配:

>>> re.match("c", "abcdef")    # No match
>>> re.search("^c", "abcdef")  # No match
>>> re.search("^a", "abcdef")  # Match
<re.Match object; span=(0, 1), match='a'>

但请注意,在 MULTILINE 模式下,match() 仅匹配字符串的开头,而使用 search() 和以 '^' 将在每行的开头匹配。

>>> re.match('X', 'A\nB\nX', re.MULTILINE)  # No match
>>> re.search('^X', 'A\nB\nX', re.MULTILINE)  # Match
<re.Match object; span=(4, 5), match='X'>

制作电话簿

split() 将字符串拆分为由传递的模式分隔的列表。 该方法对于将文本数据转换为可由 Python 轻松读取和修改的数据结构非常有用,如以下创建电话簿的示例所示。

首先,这是输入。 通常它可能来自一个文件,这里我们使用三引号字符串语法:

>>> text = """Ross McFluff: 834.345.1254 155 Elm Street
...
... Ronald Heathmore: 892.345.3428 436 Finley Avenue
... Frank Burger: 925.541.7625 662 South Dogwood Way
...
...
... Heather Albrecht: 548.326.4584 919 Park Place"""

条目由一个或多个换行符分隔。 现在我们将字符串转换为一个列表,每个非空行都有自己的条目:

>>> entries = re.split("\n+", text)
>>> entries
['Ross McFluff: 834.345.1254 155 Elm Street',
'Ronald Heathmore: 892.345.3428 436 Finley Avenue',
'Frank Burger: 925.541.7625 662 South Dogwood Way',
'Heather Albrecht: 548.326.4584 919 Park Place']

最后,将每个条目拆分为一个包含名字、姓氏、电话号码和地址的列表。 我们使用 split()maxsplit 参数,因为地址中有空格,即我们的拆分模式:

>>> [re.split(":? ", entry, 3) for entry in entries]
[['Ross', 'McFluff', '834.345.1254', '155 Elm Street'],
['Ronald', 'Heathmore', '892.345.3428', '436 Finley Avenue'],
['Frank', 'Burger', '925.541.7625', '662 South Dogwood Way'],
['Heather', 'Albrecht', '548.326.4584', '919 Park Place']]

:? 模式匹配姓氏后面的冒号,因此它不会出现在结果列表中。 使用 maxsplit4,我们可以将门牌号与街道名称分开:

>>> [re.split(":? ", entry, 4) for entry in entries]
[['Ross', 'McFluff', '834.345.1254', '155', 'Elm Street'],
['Ronald', 'Heathmore', '892.345.3428', '436', 'Finley Avenue'],
['Frank', 'Burger', '925.541.7625', '662', 'South Dogwood Way'],
['Heather', 'Albrecht', '548.326.4584', '919', 'Park Place']]

文本处理

sub() 用字符串或函数的结果替换每次出现的模式。 这个例子演示了使用 sub() 和一个函数来“处理”文本,或者随机化一个句子的每个单词中除了第一个和最后一个字符之外的所有字符的顺序:

>>> def repl(m):
...     inner_word = list(m.group(2))
...     random.shuffle(inner_word)
...     return m.group(1) + "".join(inner_word) + m.group(3)
>>> text = "Professor Abdolmalek, please report your absences promptly."
>>> re.sub(r"(\w)(\w+)(\w)", repl, text)
'Poefsrosr Aealmlobdk, pslaee reorpt your abnseces plmrptoy.'
>>> re.sub(r"(\w)(\w+)(\w)", repl, text)
'Pofsroser Aodlambelk, plasee reoprt yuor asnebces potlmrpy.'

查找所有副词

findall() 匹配 all 模式的出现,而不是像 search() 那样只匹配第一个。 例如,如果作者想在某些文本中找到所有副词,他们可以按以下方式使用 findall()

>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"\w+ly", text)
['carefully', 'quickly']

查找所有副词及其位置

如果您想要比匹配文本更多的关于模式所有匹配的信息,finditer() 很有用,因为它提供 匹配对象 而不是字符串。 继续前面的例子,如果作者想在某些文本中找到所有副词 及其位置 ,他们将使用 finditer() 以如下方式:

>>> text = "He was carefully disguised but captured quickly by police."
>>> for m in re.finditer(r"\w+ly", text):
...     print('%02d-%02d: %s' % (m.start(), m.end(), m.group(0)))
07-16: carefully
40-47: quickly

原始字符串表示法

原始字符串表示法 (r"text") 使正则表达式保持理智。 没有它,正则表达式中的每个反斜杠 ('\') 都必须以另一个反斜杠为前缀才能转义它。 例如,以下两行代码在功能上是相同的:

>>> re.match(r"\W(.)\1\W", " ff ")
<re.Match object; span=(0, 4), match=' ff '>
>>> re.match("\\W(.)\\1\\W", " ff ")
<re.Match object; span=(0, 4), match=' ff '>

当想要匹配文字反斜杠时,必须在正则表达式中对其进行转义。 使用原始字符串表示法,这意味着 r"\\"。 如果没有原始字符串表示法,必须使用 "\\\\",使以下代码行在功能上相同:

>>> re.match(r"\\", r"\\")
<re.Match object; span=(0, 1), match='\\'>
>>> re.match("\\\\", r"\\")
<re.Match object; span=(0, 1), match='\\'>

编写分词器

标记器或扫描器 分析字符串以对字符组进行分类。 这是编写编译器或解释器的有用的第一步。

文本类别使用正则表达式指定。 该技术是将它们组合成一个主正则表达式并循环遍历连续匹配:

import collections
import re

Token = collections.namedtuple('Token', ['type', 'value', 'line', 'column'])

def tokenize(code):
    keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'}
    token_specification = [
        ('NUMBER',   r'\d+(\.\d*)?'),  # Integer or decimal number
        ('ASSIGN',   r':='),           # Assignment operator
        ('END',      r';'),            # Statement terminator
        ('ID',       r'[A-Za-z]+'),    # Identifiers
        ('OP',       r'[+\-*/]'),      # Arithmetic operators
        ('NEWLINE',  r'\n'),           # Line endings
        ('SKIP',     r'[ \t]+'),       # Skip over spaces and tabs
        ('MISMATCH', r'.'),            # Any other character
    ]
    tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
    line_num = 1
    line_start = 0
    for mo in re.finditer(tok_regex, code):
        kind = mo.lastgroup
        value = mo.group()
        column = mo.start() - line_start
        if kind == 'NUMBER':
            value = float(value) if '.' in value else int(value)
        elif kind == 'ID' and value in keywords:
            kind = value
        elif kind == 'NEWLINE':
            line_start = mo.end()
            line_num += 1
            continue
        elif kind == 'SKIP':
            continue
        elif kind == 'MISMATCH':
            raise RuntimeError(f'{value!r} unexpected on line {line_num}')
        yield Token(kind, value, line_num, column)

statements = '''
    IF quantity THEN
        total := total + price * quantity;
        tax := price * 0.05;
    ENDIF;
'''

for token in tokenize(statements):
    print(token)

分词器产生以下输出:

Token(type='IF', value='IF', line=2, column=4)
Token(type='ID', value='quantity', line=2, column=7)
Token(type='THEN', value='THEN', line=2, column=16)
Token(type='ID', value='total', line=3, column=8)
Token(type='ASSIGN', value=':=', line=3, column=14)
Token(type='ID', value='total', line=3, column=17)
Token(type='OP', value='+', line=3, column=23)
Token(type='ID', value='price', line=3, column=25)
Token(type='OP', value='*', line=3, column=31)
Token(type='ID', value='quantity', line=3, column=33)
Token(type='END', value=';', line=3, column=41)
Token(type='ID', value='tax', line=4, column=8)
Token(type='ASSIGN', value=':=', line=4, column=12)
Token(type='ID', value='price', line=4, column=15)
Token(type='OP', value='*', line=4, column=21)
Token(type='NUMBER', value=0.05, line=4, column=23)
Token(type='END', value=';', line=4, column=27)
Token(type='ENDIF', value='ENDIF', line=5, column=4)
Token(type='END', value=';', line=5, column=9)
弗里09
弗里德尔,杰弗里。 掌握正则表达式。 第三版,O'Reilly Media,2009 年。 这本书的第三版根本不再涉及 Python,但第一版非常详细地介绍了编写好的正则表达式模式。