Python 正则表达式

正则表达式是一种用于匹配文本模式的强大语言。本页面对正则表达式本身进行了基本介绍,足以完成我们的 Python 练习,并展示了正则表达式在 Python 中的工作方式。Python“re”模块支持正则表达式。

在 Python 中,正则表达式搜索通常写为:

match = re.search(pat, str)

re.search() 方法会使用正则表达式模式和字符串,并在字符串中搜索该模式。如果搜索成功,search() 会返回一个匹配对象,否则会返回 None。因此,通常紧随其后是一个 if 语句,以测试搜索是否成功,如下例所示,该示例搜索格式“word:”,后跟一个由 3 个字母组成的单词(详情如下):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

代码 match = re.search(pat, str) 会将搜索结果存储在名为“match”的变量中。然后,if 语句测试匹配结果;如果为 true,则表示搜索成功,且 match.group() 为匹配文本(例如“word:cat”)。否则,如果匹配为 false(更具体为 None),则搜索不会成功,并且没有匹配的文本。

模式字符串开头的“r”指定 Python“raw”字符串,该字符串无需更改即可通过反斜杠,这对于正则表达式来说非常方便(Java 非常需要此功能!)。我建议在编写模式字符串时始终使用“r”作为一种习惯。

基本模式

正则表达式的优势在于,它们可以指定模式,而不仅仅是固定字符。以下是匹配单个字符的最基本的模式:

  • a、X、9、< - 普通字符本身就是完全匹配的。因具有特殊含义而不匹配的元字符包括: 。^ $ * + ? { [ ] \ | ( )(详情如下)
  • (句点)- 匹配除换行符“\n”之外的任何单个字符
  • \w --(小写 w)匹配“单词”字符:字母、数字或下划线 [a-zA-Z0-9_]。请注意,尽管“word”是这种助记符的助记符,但它只能匹配单个字符,而非整个单词。\W(大写 W)匹配任何非字词字符。
  • \b - 字词和非字词之间的边界
  • \s --(小写 s)匹配单个空白字符 -- 空格、换行符、回车符、制表符、格式 [ \n\r\t\f]。\S(大写 S)匹配任何非空白字符。
  • \t、\n、\r -- 制表符、换行符、回车符
  • \d - 十进制数字 [0-9](某些较旧的正则表达式实用程序不支持 \d,但它们都支持 \w 和 \s)
  • ^ = start, $ = end -- 匹配字符串的开头或结尾
  • \ -- 禁止字符的“特殊性”。例如,使用 \. 匹配英文句点,使用 \\ 匹配正斜线。如果您不确定某个字符是否具有特殊含义(例如“@”),可以尝试在它前面加上斜杠 \@。如果该字符不是有效的转义序列(如 \c),则 Python 程序将会停止运行并报错。

基本示例

笑话:你怎么称呼三只眼睛的猪?呼!

在正则表达式中搜索字符串中的模式时应遵循的基本规则如下:

  • 搜索会按从头到尾的顺序遍历字符串,直到找到的第一个匹配项为止
  • 必须匹配所有格式,但不必匹配整个字符串
  • 如果 match = re.search(pat, str) 成功,则 match 不是 None,特别是 match.group() 是匹配文本
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

重复

在模式中使用 + 和 * 指定重复情况时,情况会变得更有趣

  • + -- 模式左侧出现 1 次或多次,例如“i+”= 一个或多个 i
  • * - 左侧模式出现 0 次或多次
  • ? -- 与其左侧匹配 0 次或 1 次该模式

最左边和最大

首先,搜索查找该模式最左边的匹配项,其次,它会尝试使用尽可能多的字符串,即 + 和 * 尽可能地使用(可以说 + 和 * 表示“贪心”)。

重复示例

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

电子邮件示例

假设您想在字符串“xyz alice-b@google.com purple monkey”中查找电子邮件地址。我们将使用以下示例演示更多正则表达式功能。以下是使用 r'\w+@\w+' 模式的尝试:

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

在这种情况下,搜索不会获得完整的电子邮件地址,因为 \w 与地址中的“-”或“.”不匹配。我们将使用以下正则表达式功能修复此问题。

方括号

方括号可用于表示一组字符,因此 [abc] 匹配“a”“b”或“c”。代码 \w、\s 等也可以使用方括号内,但点 (.) 仅表示字面上的点。对于电子邮件问题,您可以通过方括号轻松将“.”和“-”添加到可出现在 @ 周围且格式为 r'[\w.-]+@[\w.-]+' 的字符集,以获取完整的电子邮件地址:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(更多方括号功能)您还可以使用短划线来表示范围,让 [a-z] 匹配所有小写字母。若要使用短划线而不指明范围,请将短划线放在最后,例如 [abc-]。方括号组开头的高帽 (^) 会将其反转,因此 [^ab] 表示除“a”或“b”以外的任何字符。

组提取

通过正则表达式的“分组”功能,您可以从匹配文本中挑选出来。假设对于电子邮件问题,我们想要分别提取用户名和主机。为此,请在模式中的用户名和主机两边添加圆括号 ( ),如下所示:r'([\w.-]+)@([\w.-]+)'。在这种情况下,圆括号不会更改模式将匹配的内容,而是在匹配文本内建立逻辑“组”。如果搜索成功,则 match.group(1) 是与第 1 个左括号对应的匹配文本,而 match.group(2) 是与第 2 个左括号对应的文本。普通的 match.group() 仍然是完整的匹配文本。

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

使用正则表达式的常见工作流程是为要查找的内容编写模式,添加圆括号以提取所需的部分。

Findall

findall() 可能是 re 模块中最强大的一个函数。在上文中,我们使用了 re.search() 查找某个模式的第一个匹配项。 findall() 会查找 *所有* 匹配项并以字符串列表的形式返回这些匹配项,每个字符串表示一个匹配项。
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

利用 Google 文件极客查找所有

对于文件,您可能习惯于编写一个循环来遍历文件行,然后可以在每行上调用 findall()。您可以让 findall() 为您执行迭代,这样会更好!只需将整个文件文本输入 findall(),让它在一步中返回所有匹配项的列表(请注意,f.read() 会在单个字符串中返回文件的完整文本):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall 和 网上论坛

圆括号 ( ) 组机制可与 findall() 结合使用。如果模式包含 2 个或更多圆括号组,则 findall() 会返回 *元组*列表,而不是返回字符串列表。每个元组代表模式的一个匹配,该元组内是 group(1)、group(2) 数据。因此,如果电子邮件格式中添加 2 个圆括号组,则 findall() 会返回一个元组列表,每个元组长度为 2 都包含用户名和主机,例如 ('alice', 'google.com')。

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

有了元组列表后,您可以循环遍历该列表,以便对每个元组进行一些计算。如果模式不包含圆括号,则 findall() 会返回找到的字符串列表,如前面的示例所示。如果模式包含单组圆括号,则 findall() 会返回与该组对应的字符串列表。(可选功能模糊:有时,模式中有圆括号 ( ) 分组,但您不想提取这些分组)。在这种情况下,应该在开头用 ?: 书写圆括号,例如 (?: ),左圆括号不会计为组结果。)

RE 工作流和调试

正则表达式模式会将很多含义打包成几个字符,但它们非常密集,以至于您需要花费大量时间调试模式。设置运行时,以便您可以运行模式并轻松输出匹配的结果,例如,对小型测试文本运行该模式并输出 findall() 的结果。如果模式不匹配,请尝试削弱模式,移除其中的某些部分,这样会得到太多匹配。如果不匹配任何值,您就无法取得任何进展,因为没有实质性的内容需要查看。一旦匹配度过高,你可以逐渐加强匹配,使其达到你想要的效果。

选项

这些 re 函数采用一些选项来修改模式匹配的行为。选项标志作为额外参数添加到 search() 或 findall() 等,如 re.search(pat, str, re.IGNORECASE)。

  • IGNORECASE -- 忽略大小写差异以进行匹配,因此“a”同时匹配“a”和“A”。
  • DOTALL - 允许点 (.) 匹配换行符,通常它匹配除换行符以外的所有内容。这可能会使您不舒服 -- 您认为 .* 可以匹配所有内容,但默认情况下,它不会超出一行的结尾。请注意,\s(空白)包含换行符,因此如果您想匹配包含换行符的空白字符,只需使用 \s*
  • 多行 -- 在由多行组成的字符串中,允许 ^ 和 $ 匹配每行的开头和结尾。通常,^/$ 将仅匹配整个字符串的开头和结尾。

贪心与非贪心(可选)

这是一个可选部分,其中展示了练习不需要的更高级的正则表达式。

假设您有包含标记的文本:<b>foo</b> 和 <i>so on</i>

假设您正尝试将每个标记与模式“(<.*>)”匹配 - 它首先匹配什么?

结果有点令人惊讶,但 .* 的贪婪方面使其将整个“<b>foo</b>”和“so on”视为一个大匹配项。<i></i>问题在于 .* 会尽可能找到它,而不是在第一个 > 处停止(也称为“贪心”)。

是正则表达式的扩展,您可以在其中添加 ?(如 .*? 或 .+?),并将其更改为非贪婪的词汇。现在,它们会尽快停止运行。因此,模式“(<.*?>)”将只有“<b>”作为第一个匹配项,“</b>”作为第二个匹配项,依此类推,依次获取每个 <..> 对。样式通常采用如下样式:先使用 .*?,后跟 .*? 运行被强制扩展的某个具体标记(在本例中为 >)。

*? 扩展源自 Perl,而包含 Perl 扩展的正则表达式称为 Perl 兼容正则表达式 - pcre。Python 支持 PCre。许多命令行 utils 等都有一个接受 pcre 模式的标志。

方括号样式是以前广泛使用的一种技术,用来编写“所有这些字符除了在 X 处停止的字符”这种想法的代码。对于上述内容,您可以编写模式,但使用 [^>]* 代替 .* 来获取所有字符,这样会跳过所有不是 > 的字符(前导 ^“反转”方括号集,因此它可以匹配中括号以外的任何字符)。

替换(可选)

re.sub(pat, Replace, str) 函数将在给定字符串中搜索模式的所有实例并予以替换。替换字符串可以包含“\1”“\2”,用于引用原始匹配文本中的“group(1)、group(2)”等文本。

以下示例会搜索所有电子邮件地址,然后更改这些地址,以保留用户 (\1),但以 yo-yo-dyne.com 作为主机。

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

演习

如要练习正则表达式,请参阅婴儿起名练习