正则表达式最全教程(含示例代码)

正好我最近想接触下爬虫,打算整理一下正则表达式,后面会用到。
本文基于python的howto-regex 文档,总结如下:
另外,官方指南:https://docs.python.org/zh-cn/3/library/re.html#module-re

1.概述

使用正则表达式可以为匹配的可能字符串集指定规则;可以判断此字符串是否与模式匹配等问题。还可以使用正则表达式修改字符串或以各种方式将其拆分。

正则表达式模式被编译成一系列字节码,然后由用C 编写的匹配引擎执行。对于高级用途,可能需要特别注意引擎如何执行给定的正则,并将正则写入以某种方式生成运行速度更快的字节码。

2.简单模式

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

元字符的完整列表如下:

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

这些元字符有的可以用来表示匹配字符,有的可以用来表示字符重复次数,下面依次介绍这些元字符。

2.1 匹配字符

1.[ ]:用于指定字符类,里面放你希望匹配的一组字符。既可以单独列出字符,也可以通过给出两个字符并用’-'标记将他们分开来表示一系列字符。
例如:

  • [a-z]:用来匹配小写字符
  • [abc]:可以匹配a或b或c,这与[a-c]
  • 相同。
  • 字符类中的元字符是不生效的,例如:[akm$]将匹配a或k或m或$,$通常是一个元字符,但是在字符类中它不生效

2. ^:用于匹配字符类未列出的字符。通过包含^作为作为字符类的第一个字符来表示。
例如:

  • [^5]:匹配除5之外的任何字符
  • 如果该字符出现在字符类的其他位置,则没有特殊含义
  • [5^]:匹配5或者 ^

3.‘\’字符:后面可以跟各种字符,以指示各种特殊序列

  • 它也可以用于转义所有元字符。如果你需要匹配 [ 或 \ ,可以在他们的前面加上一个反斜杠来转义他们的特殊含义。即 \[ 或 \\
  • 一些以\开头的特殊序列表示预定义的字符集,例如字符集、字母集或任何非空格的集合。
  • \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_]。

4. ’ . '字符:用来匹配除换行符之外的任何内容。但是有一个可选模式(re.DOTALL)可以匹配换行符

2.2. 重复

匹配不同的字符集是正则表达式可以做的第一件事情,另一个功能是可以指定正则表达式的某些部分必须重复一定次数。

5. ’ * '字符:它指定前一个字符可以匹配零次或多次。

  • 例如:ca*t 将匹配’ ct ’ (0 个’a’ 字符),’ cat ’ (1 个’a’ ),’ caaat ’ (3 个’a’ 字符),等等
  • 类似 * 这样的重复是贪婪的;当重复正则时,匹配引擎将尝试尽可能多地重复它。如果模式的后续部分不
    匹配,则匹配引擎将回退并以较少的重复次数再次尝试

6. ’ + '字符:它指定前一个字符可以匹配一次或多次。

  • ca+t 将匹配’cat’ (1 个’a’),‘caaat’ (3 个’a’),但不会匹配’ct’。

7. ’ ? '字符:它指定前一个字符可以匹配一次或零次。

  • 可以把它想象成是可选的。例如,home-?brew 匹配’homebrew’ 或’home-brew’。

7. ’ ?{m, n}'字符:这个是最复杂的重复限定符是,其中m 和n 是十进制整数。这个限定符意味着必须至少重复m 次,最多重复n 次。

  • 例如,a/{1,3}b 将匹配’a/b’ ,‘a//b’ 和’a///b’ 。它不匹配没有斜线的’ab’,或者有四个的’ab’。
  • 可以省略m 或n; 在这种情况下,将假定缺失值的合理值。省略m 被解释为0 下限,而省略n 则为无穷大
    的上限。
  • 可以看出{0,} 与* 相同,{1,} 相当于+ ,{0,1} 和? 相同。建议最好使用* ,+ 或? ,因为他们更短更易阅读。

3.使用正则表达式

有了上面的了解,下面介绍在python中实际使用它们,re模块提供了正则表达式引擎的接口,允许你将正则编译成对象,然后用它们进行匹配。

3.1 编译正则表达式

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

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

re.compile() 也接受一个可选的flags 参数,用于启用各种特殊功能和语法变体。后面会介绍可用的设置,但现在只举一个例子

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

正则作为字符串传递给re.compile() 。正则被处理为字符串,因为正则表达式不是核心Python语言的一部分,并且没有创建用于表达它们的特殊语法。

3.2 反斜杠灾难

将正则放在字符串中可以使Python 语言更简单,但有一个缺点就是反斜杠灾难。
正则表达式使用反斜杠字符(’\’) 来表示特殊形式或允许使用特殊字符而不调用它们的特殊含义。

  • 假设你想要编写一个与字符串\section 相匹配的正则。要找出在程序代码中写入的内容,从要匹配的字符串开始,接下来,必须通过在反斜杠前面添加反斜杠从而产生字符串’\\section’,但是,要将其表示为Python 字符串文字,必须再次转义两个反斜杠,即’\\\\section’。
    在这里插入图片描述
  • 简而言之,要匹配文字反斜杠,必须将’\\’ 写为正则字符串,因为正则表达式必须是\,并且每个反斜杠必须表示为\\
  • 解决方案是使用Python 的原始字符串表示法来表示正则表达式;在前缀为’r’ 的字符串中,反斜杠不以任何特殊的方式处理,因此r"\n" 是一个包含’\’ 和’n’ 的双字符字符串,而"\n" 是一个包含换行符的单字符字符串。正则表达式通常使用这种原始字符串表示法用Python 代码编写。
  • 此外,在正则表达式中有效但在Python 字符串文字中无效的特殊转义序列会导致DeprecationWarning,最终变为SyntaxError。这意味着如果未使用原始字符串表示法或转义反斜杠,序列将无效。
    在这里插入图片描述

3.3 应用匹配

当编译正则表达式之后就得到了模式对象,下面介绍最终的模式对象的方法和属性:

方法/属性目的
match()从字符串的开头匹配。没有找到匹配,返回None,成功则返回匹配对象的实例,包含匹配相关的信息:起始和终止位置、匹配的子串等的。
search()扫描字符串,查找此正则匹配的任何位置。没有找到匹配,返回None,成功则返回匹配对象的实例,包含匹配相关的信息:起始和终止位置、匹配的子串等的。
findall()找到正则匹配的所有子字符串,并将它们作为列表返回
finditer()找到正则匹配的所有子字符串,并将它们返回为一个iterator

示例:

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

你可以尝试匹配正则[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()返回包含匹配(start, end)位置的元组
>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回正则匹配的子字符串。start() 和end() 返回匹配的起始和结束索引。span() 在单个元组
中返回开始和结束索引。由于match() 方法只检查正则是否在字符串的开头匹配,所以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

>>> 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)

3.4 模块级别函数

  • 另外,你不必创建模式对象并调用其方法;re 模块还提供了顶级函数match(),search(),findall(),sub()
    等等。这些函数采用与相应模式方法相同的参数,并将正则字符串作为第一个参数添加,并仍然返回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 '>
  • 本质上,这些函数只是为你创建一个模式对象,并在其上调用适当的方法。它们还将编译对象存储在缓存中,因此使用相同的未来调用将不需要一次又一次地解析该模式
  • 你是否应该使用这些模块级函数,还是应该自己获取模式并调用其方法?如果你正在循环中访问正则表达式,预编译它将节省一些函数调用。在循环之外,由于有内部缓存,没有太大区别。
    所以在循环之内不建议使用模块级别的函数

3.5 编译标志

  • 编译标志允许你修改正则表达式的工作方式。标志在re 模块中有两个名称,长名称如IGNORECASE 和一个简短的单字母形式,例如I。(如果你熟悉Perl 的模式修饰符,则单字母形式使用和其相同的字母;例如,re.VERBOSE 的缩写形式为re.X。)

  • 多个标志可以通过按位或运算来指定它们;例如,re.I | re.M 设置I 和M 标志。

  • 下面是可用标志表,以及每个标志的更详细说明。

标志意义
ASCII, A使几个转义如\w、\b、\s 和\d 匹配仅与具有相应特征属性的ASCII 字符匹配。
DOTALL, S使. 匹配任何字符,包括换行符。
IGNORECASE, I进行大小写不敏感匹配。
LOCALE, L进行区域设置感知匹配。
MULTILINE, M多行匹配,影响^ 和$。
VERBOSE, X(为’ 扩展’)启用详细的正则,可以更清晰,更容易理解。

IGNORECASE, I

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

LOCALE, L

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

MULTILINE, M

  • (^ 和$ 还没有解释;它们将在以下部分介绍更多元字符。)
  • 通常^ 只匹配字符串的开头,而$ 只匹配字符串的结尾,紧接在字符串末尾的换行符(如果有的话)之前。当指定了这个标志时,^ 匹配字符串的开头和字符串中每一行的开头,紧跟在每个换行符之后。
    类似地,$ 元字符匹配字符串的结尾和每行的结尾(紧接在每个换行符之前)。

DOTALL, S

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

ASCII, A

  • 使\w、\W、\b、\B、\s 和\S 执行仅ASCII 匹配而不是完整匹配Unicode 匹配。这仅对Unicode 模式
    有意义,并且对于字节模式将被忽略。

VERBOSE, X

  • 此标志允许你编写更易读的正则表达式,方法是为您提供更灵活的格式化方式。
  • 指定此标志后,将忽略正则字符串中的空格,除非空格位于字符类中或前面带有未转义的反斜杠;这使你可以更清楚地组织和缩进正则。
  • 此标志还允许你将注释放在正则中,引擎将忽略该注释;注释标记为’#’ 既不是在字符类中,也不是在未转义的反斜杠之前
  • 例如,这里的正则使用re.VERBOSE;看看阅读有多容易:
    在这里插入图片描述
    如果没有详细设置,正则将如下所示:
    在这里插入图片描述
    在上面的例子中,Python 的字符串文字的自动连接已被用于将正则分解为更小的部分,但它仍然比使用re.VERBOSE 版本更难理解。

4 更多模式能力

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

4.1 更多元字符

要讨论的其余一些元字符是零宽度断言。它们不会使解析引擎在字符串中前进一个字符;相反,它们根本不占用任何字符,只是成功或失败。

例如,\b 是一个断言,指明当前位置位于字边界;这个位置根本不会被\b改变。这意味着永远不应重复零宽度断言,因为如果它们在给定位置匹配一次,它们显然可以无限次匹配

1.| 或者“or”运算符

  • 如果A 和B 是正则表达式,A|B 将匹配任何与A 或B 匹配的字符串。
  • | 具有非常低的优先级,以便在交替使用多字符字符串时使其合理地工作。Crow|Servo 将匹配’Crow’ 或’Servo’,而不是’Cro’、‘w’ 或’S’ 和’ervo’。
  • 要匹配字面’|’,请使用|,或将其括在字符类中,如[|]

2.^:当该字符不在字符类中,表示在行的开头匹配

  • 除非设置了MULTILINE 标志,否则只会在字符串的开头匹配。在MULTILINE 模式下,这也在字符串中的每个换行符后立即匹配。
    例如,如果你希望仅在行的开头匹配单词From,则要使用的正则^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
  • 要匹配字面’^’,使用 \^

3.$ 匹配行的末尾,定义为字符串的结尾,或者后跟换行符的任何位置

>>> 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='}'>
  • 匹配字面’$’,使用$ 或者将其包含在一个字符类中,例如[$]。

4.\A 仅匹配字符串的开头

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

5. \Z 只匹配字符串尾

6. \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 转换为退格,你的正则不会按照你的预期匹配。以下示例与我们之前的正则看起来相同,但省略了正则字符串前面的’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 的字符串文字兼容。

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

4.2 分组

通常,你需要获取更多信息,而不仅仅是正则是否匹配。正则表达式通常用于通过将正则分成几个子组来解析字符串,这些子组匹配不同的感兴趣组件。例如,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 始终存在;它表示整个正则,所以匹配对象方法都将组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 的字符串文字也使用反斜杠后跟数字以允许在字符串中包含任意字符,因此正则中引入后向引用时务必使用原始字符串。
    例如,以下正则检测字符串中的双字
>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

解释一下:上面的模式指的是:首先找到一个前后是空格的单词,然后后向引用子组1,也就是子组1的内容和前面单词的相同,最后加空格。只有’the the’满足条件。

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

4.3 非捕获和命名组

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

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

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

Python 支持一些Perl 的扩展,并增加了新的扩展语法用于Perl 的扩展语法。如果在问号之后的第一个字符为P,即表明其为Python 专属的扩展。

现在已经了解了一般的扩展语法,回到简化复杂正则中组处理的功能。

有时会想要使用组来表示正则表达式的一部分,但是对检索组的内容不感兴趣。可以通过使用非捕获组来显式表达这个事实: (?: … ),你可以用任何其他正则表达式替换 ‘…’

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()
  • 除了你无法检索组匹配内容的事实外,非捕获组的行为与捕获组完全相同;你可以在里面放任何东西,用重复元字符重复它,比如*,然后把它嵌入其他组(捕获或不捕获)
  • (?:…) 在修改现有模式时特别有用,因为你可以添加新组而不更改所有其他组的编号方式。值得一提的是,捕获和非捕获组之间的搜索没有性能差异;两种形式没有一种更快。

更重要的功能是命名组:不是通过数字引用它们,而是可以通过名称引用组。

  • 命名组的语法是Python 特定的扩展之一: (?P<name>...)。name 显然是该组的名称。命名组的行为与捕获组完全相同,并且还将名称与组关联。
  • 处理捕获组的匹配对象方法都接受按编号引用组的整数或包含所需组名的字符串。命名组仍然是给定的数字,因此你可以通过两种方式检索有关组的信息
>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'
  • 此外,你可以通过groupdict() 将命名分组提取为一个字典:
>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}
  • 命名组很有用,因为它们允许你使用容易记住的名称,而不必记住数字。这是来自imaplib 模块的示例正则
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\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'

4.4 前向断言

另一个零宽度断言是前向断言。前向断言以正面和负面形式提供,如下所示:

  • (?=⋯) 正向前向断言。如果包含的正则表达式,由… 表示,在当前位置成功匹配,则成功,否则失败。但是,一旦尝试了包含的表达式,匹配的引擎就不会前进;模式其余的部分会在在断言开始的地方尝试。

  • (?!⋯) 负向前向断言。这与正向前向断言相反;如果包含的表达式在字符串中的当前位置不匹配,则成功。

例子说明:

更具体一些,考虑一个简单的模式来匹配文件名并将其拆分为基本名称和扩展名,用. 分隔。例如,在news.rc 中,news 是基本名称,rc 是文件名的扩展名。

与此匹配的模式非常简单: .*[.].*$

请注意,. 需要特别处理,因为它是元字符,所以它在字符类中只能匹配特定字符。还要注意尾随的$;添加此项以确保扩展名中的所有其余字符串都必须包含在扩展名中。这个正则表达式匹配foo.bar、autoexec.bat、sendmail.cf 和printers.conf。

现在,考虑使更复杂一点的问题;如果你想匹配扩展名不是bat 的文件名怎么办?一些错误的尝试:
.*[.][^b].*$ 上面的第一次尝试试图通过要求扩展名的第一个字符不是b 来排除bat。这是错误的,因为模式也与foo.bar 不匹配。

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

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$在这次尝试中,第二个和第三个字母都是可选的,以便允许匹配的扩展名短于三个字符,例如sendmail.cf。

模式现在变得非常复杂,这使得它难以阅读和理解。更糟糕的是,如果问题发生变化并且你想要将bat 和
exe 排除为扩展,那么该模式将变得更加复杂和混乱。

负面前向消除了所有这些困扰:
.*[.](?!bat$)[^.]*$ 负向前向意味着:如果表达式bat 此时不匹配,请尝试其余的模式;如果bat$匹配,整个模式将失败。尾随的$ 是必需的,以确保允许像sample.batch 这样的扩展只以bat 开头的文件能通过。[^.]* 确保当文件名中有多个点时,模式有效。

现在很容易排除另一个文件扩展名;只需在断言中添加它作为替代。以下模块排除以bat 或exe:.*[.](?!bat$|exe$)[^.]*$

5 修改字符串

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

方法属性目的
split()将字符串拆分为一个列表,在正则匹配的任何地方将其拆分
sub()找到正则匹配的所有子字符串,并用不同的字符串替换它们
subn()与sub() 相同,但返回新字符串和替换次数

模式的split() 方法在正则匹配的任何地方拆分字符串,返回一个片段列表。它类似于split() 字符串方法,但在分隔符的分隔符中提供了更多的通用性;字符串的split() 仅支持按空格或固定字符串进行拆分。当然,还有一个模块级re.split() 函数

.split(string[, maxsplit=0 ])
通过正则表达式的匹配拆分字符串。如果在正则中使用捕获括号,则它们的内容也将作为结果列表的一部分返回。如果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().']

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

>>> 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.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.']

5.2 搜索和替换

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

.sub(replacement, string[, count=0 ])
返回通过替换replacement 替换string 中正则的最左边非重叠出现而获得的字符串。如果未找到模式,则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,被替换为正则中相应组
匹配的子字符串。这使你可以在生成的替换字符串中合并原始文本的部分内容。

这个例子匹配单词section 后跟一个用{,} 括起来的字符串,并将section 改为subsection

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

还有一种语法用于引用由(?P…) 语法定义的命名组。\g 将使用名为name 的组匹配的子字符串,\g 使用相应的组号。因此\g<2> 等同于\2,但在诸如\g<2>0 之类的替换字符串中并不模糊。( \20 将被解释为对组20 的引用,而不是对组2 的引用,后跟字面字符’0’。) 以下替换都是等效的,但使用所有三种变体替换字符串。

>>> 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’,其中的(?i)表示扩展的忽略大小写的表示方法。

6 常见问题

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

6.1 使用字符串方法

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

一个例子:用另一个固定字符串替换一个固定字符串;例如,你可以用deed 替换word 。re.sub()看起来像是用于此的函数,但请考虑replace() 方法。

注意replace() 也会替换单词里面的word ,把swordfish 变成sdeedfish ,但简单的正则word 也会这样做。(为了避免对单词的部分进行替换,模式必须是bword\b,以便要求word 在任何一方都有一个单词边界。这使得工作超出了replace() 的能力。)

另一个常见任务是从字符串中删除单个字符的每个匹配项或将其替换为另一个字符。你可以用re.sub(’\n’, ’ ', S) 之类的东西来做这件事,但是translate() 能够完成这两项任务,并且比任何正则表达式都快。

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

6.2 match()和search()

match()仅仅从字符串start=0处去进行匹配,如果要匹配的对象start不为0,则不会匹配到。

>>> print(re.match('super', 'superstition').span())
(0, 5)
>>> print(re.match('super', 'insuperable'))
None

search() 将向前扫描字符串,报告它找到的第一个匹配项

>>> print(re.search('super', 'superstition').span())
(0, 5)
>>> print(re.search('super', 'insuperable').span())
(2, 7)

有时可能会想继续使用re.match() ,只需在你的正则前面添加.* 。但是建议使用re.search()直接解决这个问题。

这主要是因为:正则表达式编译器对正则进行一些分析,以加快寻找匹配的过程。其中一个分析可以确定匹配的第一个特征必须是什么;例如,以Crow 开头的模式必须与’C’ 匹配。分析让引擎快速扫描字符串,寻找起始字符,只在找到’C’ 时尝试完全匹配。添加.* 会使这个优化失效,需要扫描到字符串的末尾,然后回溯以找到正则的其余部分的匹配。

6.3 贪婪与非贪婪

当重复一个正则表达式时,就像在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>

正则匹配’<’ 中的’’ 和.* 消耗字符串的其余部分。正则中还有更多的剩余东西,并且> 在字符
串的末尾不能匹配,所以正则表达式引擎必须逐个字符地回溯,直到它找到匹配> 。最终匹配从’‘中的’<’ 扩展到’’ 中的’>’ ,而这并不是你想要的结果。

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

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

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

6.4 使用re.VERBOSE

到目前为止,你可能已经注意到正则表达式是一种非常紧凑的表示法,但它们并不是非常易读。具有中等复杂度的正则可能会成为反斜杠、括号和元字符的冗长集合,使其难以阅读和理解。

对于这样的正则,在编译正则表达式时指定re.VERBOSE 标志可能会有所帮助,因为re.VERBOSE允许你更清楚地格式化正则表达式

re.VERBOSE 标志有几种效果。

  • 正则表达式中的不是在字符类中的空格将被忽略。这意味着表达式如dog | cat 等同于不太可读的dog|cat ,但[a b] 仍将匹配字符’a’ 、‘b’ 或空格。
  • 此外,你还可以在正则中放置注释;注释从# 字符扩展到下一个换行符。当与三引号字符串一起使用时,这使正则的格式更加整齐:
    在这里插入图片描述
    相比下面这种写法这更具有可读性:
pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

肝了很长时间,终于肝完了。

都看到这了,确定不三连?

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
正则表达式善于处理文本,对匹配、搜索和替换等操作都有意想不到的作用。正因如此,正则表达式现在是作为程序员七种基本技能之一*,因此学习和使用它在工作中都能达到很高的效率。 正则表达式应用于程序设计语言中,首次是出现在 Perl 语言,这也让 Perl 奠定了正则表达式旗手的地位。现在,它已经深入到了所有的程序设计语言中,在程序设计语言中,正则表达式可以说是标准配置了。 Java 中从 JDK 1.4 开始增加了对正则表达式的支持,至此正则表达式成为了 Java 中的基本类库,使用时不需要再导入第三方的类库了。Java 正则表达式的语法来源于象征着正则表达式标准的 Perl 语言,但也不是完全相同的,具体的可以参看 Pattern 类的 API 文档说明。 我在一次偶然中发现了位于 java.sun.com 站点上的 Java Tutorial,也在那里看到了关于 Java 的正则表达式教程,感觉它不同于其他的正则表达式教程,文中以大量的匹配实例来进行说明。为了能让 Java 学习者能更好地使用正则表达式,就将其完整地译出了。该教程中所介绍的正则表达式应用仅仅是最为简单的(并没有完全地涉及到 Pattern 类支持的所有正则表达式语法,也没有涉及到高级的应用),适合于从未接触过或者是尚未完全明白正则表达式基础的学习者。在学习完该教程后,应该对正则表达式有了初步的了解,并能熟练地运用 java.util.regex 包中的关于正则表达式的类库,为今后学习更高级的正则表达式技术奠定良好的基础。 教程中所有的源代码都在 src 目录下,可以直接编译运行。由于当前版本的 Java Tutorial 是基于 JDK 6.0 的,因此其中的示例程序也用到了 JDK 6.0 中的新增类库,但正则表达式在 JDK 1.4 就已经存在了,为了方便大家使用,改写了部分的源代码,源代码类名中后缀为"V4"的表示用于 JDK 1.4 或以上版本,"V5"的表示用于 JDK 5.0 或以上版本,没有这些后缀的类在各个版本中均可以正常使用。 由于译者的水平和技术能力有限,译稿虽经多次校对,难免有疏漏之处,敬请大家批评和指正。若有发现不妥之处,请发送邮件至 [email protected],我会在 blog 中进行勘误,谢谢!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的贝塔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值