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_]。请注意,虽然“字词”是此名称的助记符,它只匹配单个字词字符,而非整个字词。\W(大写 W)匹配任何非文字字符。
  • \b -- 字词和非字词之间的边界
  • \s --(小写 s)匹配单个空白字符 - 空格、换行符、回车符、制表符,格式为 [ \n\r\t\f]。\S(大写 S)匹配任何非空白字符。
  • \t,\n,\r -- 制表符,换行符,return
  • \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() 可能是 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)

包含文件的 findall

对于文件,您可能习惯编写一个循环来迭代文件的行,然后可以在每行上调用 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 和 Groups

括号 ( ) 组机制可与 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></i>

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

结果有点出乎意料,但 .* 贪婪的方面导致它匹配整个“<b>foo</b>”依此类推”<i></i>必将大相径庭问题是,.* 会尽可能走远,而不是在第一个 >(也称为“贪婪”)。

正则表达式有一个在末尾添加 ? 符号的扩展,例如 .*?或 .+?,将其更改为非贪心搜索。现在它们会尽快停下来。因此模式“(<.*?>)”只能得到“<b>”作为第一个匹配项,以“</b>”表示作为第二个匹配项,以此类推,获取每个 <..>。样式通常是使用 .*?后面紧跟着将 .*?运行时间被强制延长

*?扩展源自 Perl,而包含 Perl 扩展的正则表达式称为 Perl 兼容正则表达式 (pcre)。Python 包含 pcre 支持。许多命令行实用程序等都有一个用于接受 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

锻炼

要练习使用正则表达式,请参阅婴儿取名练习