05.重复匹配 (Python)

第五章 重复匹配

概览

  • + 匹配一个或多个字符 (字符集合).
  • * 匹配零个或多个字符 (字符集合).
  • ? 匹配零个或一个字符 (字符集合).
  • {n} 匹配n个字符 (字符集合)
  • {m, n} 至少匹配 m 个, 至多匹配 n 个字符 (字符集合).
  • {n,} 至少匹配 n 个字符 (字符集合).
  • *?, +?, {n,}? 懒惰型的重复匹配.

5.1 有多少个匹配

前面已经介绍了正则表达式的基础知识, 以及如何在 Python 编程中的应用. 但是目前学习的知识, 都有很大的局限性, 它基本只能匹配固定的字符. 所以它的作用就大打折扣了, 因为现实世界是复杂多变的, 而且正则表达式的真正威力在于用有限来描述无限. 所以, 接下来让我们来学习如何在正则表达式中进行任意的匹配操作吧!

注意 前面几篇博客, 有点照本宣科了, 纯粹是学习的记录 + 示例的 Python 代码了. 我感觉这样效果不是很好, 对于我自己来说, 我基本上书上面的内容都看了, 收获很多, 但是对于读者来说不是那么友好. 就我个人而言, 我喜欢短小精悍的博客, 用较少的篇幅, 可以讲解到知识点. 至于说, 真正的学习, 那我还是建议通过深入阅读书籍 + 代码实践才是最佳之道. 博客太长的话, 容易让人失去耐心, 不过我的水平有限, 只能说尽量朝着这个方向去做吧.

如下是一个电子邮件地址: text@text.text.

通过前面学习的知识, 写出匹配它的正则表达式: \w@\w\.\w. 这个表达式本身没有任何错误, 但是他也没有任何实际用处. 它只能匹配形如 a@b.c 的电子邮件地址. 因为 \w 只能匹配单个字符, 但是我们自己也不知道需要匹配多少个字符?

例如, 下面这些有效的电子邮件地址, 但是它们在 @ 前面的字符个数都不一样:

  • b@forta.com
  • ben@forta.com
  • bforta@forta.com

所以, 我们需要知道如何匹配多个字符, 而这可以通过几个特殊的元字符来做到.

5.1.1 匹配一个多多个字符

+ 匹配一个或多个字符 (至少一个, 不匹配零个字符的情况). 例如: a+, 匹配 a, aa, aaa

这里先来看一个简单的例子, 这个例子不是书上的, 是我临时想的一个匹配数字的例子, 它看起来更加简单一点.

import re

# 测试文本
text = """
0
10
100
1000
10000
"""

# 正则表达式
REGEXP = r'\d+'

# 编译
pattern = re.compile(REGEXP)

# 匹配, 返回值是数组
rs = pattern.findall(text)
if rs:
   print(rs)
else:
   print("No match!")

# 这里补充一个勘误, 前面我说了匹配全部, 有两个方法, 
# 一个是 findall, 另一个是 finditer, 我当时只是简单说了,
# 一个是数组, 一个是迭代器. 但是这里是有点问题的, 有一个
# 地方没有说清楚. 我在这里说一下, 前面的就不改了. 
# findall 返回的是匹配结果的数组;
# finditer 返回的是匹配对象的数组.
# 这里用上面这个例子来说明.

# 匹配, 返回值是迭代器
ms = pattern.finditer(text)
if ms:
   print("迭代器对象", ms)
   print("遍历迭代器对象: ")
   for m in ms:
       print(m)
else:
   print("No match!")

# 数组是结果的数组, 但是迭代器是匹配对象的迭代器, 不是结果的迭代器.
# 所以这一点差异是很重要的, 因为通过匹配对象我们可以做更多的操作, 
# 反而数组因为丢失了部分元信息, 导致一些操作是完成不了的. 我这里
# 使用数组, 是因为我们只是简单的搜索匹配, 所以也不需要其他的元信息了, 
# 但是之后的内容, 我们是必须要使用迭代器的.

\d+ 匹配一个或多个数字字符, 这样上面一个或者多个数字字符都是可以匹配的了.

在这里插入图片描述

我们接着看那个邮件地址匹配的例子, 这次使用 + 来匹配一个或多个字符.

import re

text = """
Send personal email to ben@forta.com. for questions
about a book use support@forta.com. Feel free to send
unsolicited email to spam@forta.com (wouldn't it be
nice if it were that simple, huh?).
"""

# 正则表达式
REGEXP = r"\w+@\w+\.\w+"

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

匹配结果

提示 + 是一个元字符, 如果需要匹配 + 本身, 就必须使用转义序列 \+.

+ 还可以用来匹配一个或多个字符集合.下面来演示这种用法, 使用相同的正则表达式, 但是测试的文本和上面有一些不同:

# 电子邮件地址
# ben@forta.com
# ben.forta@forta.com
# support@forta.com
# ben@urgent.forta.com
# spam@forta.com
import re

text = """
Send persona1 email to ben@forta.com. or 
ben.forta@forta.com. For questions about a 
book use support@forta.com. If your message 
is urgent try ben@urgent.forta.com. Feel 
free to send unsolicited email to
spam@forta.com (wouldn't it be nice if
it were that simple, huh?).
"""

# 正则表达式
REGEXP = r"\w+@\w+\.\w+"

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

总共有五个电子邮件, 匹配结果也是5个, 但是其中有两个匹配的不完整. 因为正则表达式 \w+@\w+\.\w+ 并没有考虑到 @ 之前的 . 字符, 它只允许 @ 之后的两个字符串之间出现单个 \.. 因为 \w 只能匹配字母数字字符, 无法匹配出现在字符串中间的 . 字符.

在这里, 需要匹配 \w.. 用正则表达式语言来说, 就是匹配字符集合 [\w.]. 下面是上面程序的改进版本:

import re

text = """
Send persona1 email to ben@forta.com. or 
ben.forta@forta.com. For questions about a 
book use support@forta.com. If your message 
is urgent try ben@urgent.forta.com. Feel 
free to send unsolicited email to
spam@forta.com (wouldn't it be nice if
it were that simple, huh?).
"""

# 正则表达式
REGEXP = r"[\w.]+@[\w.]+\.\w+"

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

这个新的正则表达式用了些技巧, [\w.]+ 匹配字母数字字符, 下划线和 . 的一次或多次重复出现, 而 ben.forta 完全符合这一条件. @ 字符之后也用到了 [\w.]+, 这样就可以匹配到层级更深的域 (或主机) 名.

注意 这个正则表达式的最后一部分是 \w+ 而不是 [\w.]+, 如果用了后者,会导致在第二, 第三和第四个匹配上出现问题.

注意 在字符集合里面, [\w.][\w\.] 是一样的, 一般来说, 但在字符集合里使用的时候, 像 .+ 这样的元字符将被解释为普通字符, 不需要额外转义, 但转义了也没有坏处. [\w.] 的使用效果与 [\w\.]是一样的.

5.1.2 匹配零个或多个字符

+ 匹配一个或多个字符, 但不匹配零个字符, + 最少也要匹配一个字符. 那么, 如果你想匹配一个可有可无的字符. 所以如果想要匹配零个或多个字符, 我们需要使用 *, 它的使用方式和 + 完全一样.

import re

# 测试文本
text = """
Hello .ben@forta.com is my email address.
"""

# 正则表达式
REGEXP = r'[\w.]+@[\w.]+\w+'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

5.1.3 匹配零个或 1 个字符

? 匹配某个字符 (或字符集合) 的零次或一次出现, 最多不超过一次. ? 非常适合匹配一段文本中某个特定的可选字符.

import re


import re

# 测试文本
text = """
The URL is http://www.forta.com/, to connect
securely use https://www.forta.com/ instead.
"""

# 正则表达式
REGEXP = r'http:\/\/[\w.\/]+'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

这里只能匹配到 http 开头的 URL, 无法匹配到 https 开头的. 如果使用 * 来匹配, 那么有可能匹配到 httpsssssss:// 这样开头的无效 URL. 虽然通常使用 * 也可以解决问题, 因为这样无效的 URL 很少见. 但是, 如果要真正解决问题, 我们应该使用 ? 来匹配.

那么, 正则表达式就是: https?:\/\/[\w.\/]+. 代码和上面的一样, 只是正则表达式换了.

在这里插入图片描述

这个正则表达式中 ? 的含义是: 前面的字符 (s) 要么不出现, 要么最多出现一次. 即, 它只能匹配 http:// 或者 https:// 开头的 URL.

提示 这里可以将 s 作为一个单独的字符集合, 这样可以增加可读性.

http[s]?:\/\/[\w.\/]+

同样, 使用 ? 可以解决之前匹配不同操作系统中的文件换行符问题. 在 Windows 系统上, 使用 \r\n, 在 Unix 或 Linux 系统上使用 \n.

那么正则表达式可以这样来表示: [\r]?\n[\r]\n. 这里将 \r 作为一个单独的字符集合, 这样做是为了增加可读性.

? 是一个元字符, 如果要匹配 ? 本身, 就必须使用转义序列 \?.

5.2 匹配的重复次数

让我们来回顾一下, 已经学习的这个重复匹配字符从作用:

  • + 匹配一个或多个字符或字符集合
  • * 匹配零个或多个字符或字符集合
  • ? 匹配零个或 1 个字符集合.

大家发现了什么局限性吗? 如果用一个数轴表示的话, 它们可以匹配 0 个, 1 个, 任意个字符. 但是有时我们并不希望匹配任意个字符, 而是匹配有限个确定的字符.

为了解决这个问题并对重复性匹配有更多个控制权, 正则表达式允许使用重复范围 (interval). 重复范围在 {} 之间指定.

注意 {} 是元字符.如果需要匹配自身, 就应该用 \ 对其进行转义. 但是, 即使你忘记进行转义, 大部分正则表达式实现也能正确地处理它们. 不过, 最好不要依赖这种行为. 在需要吧 {} 当作普通字符来匹配的场合, 应该对其 进行转义.

5.2.1 具体的重复匹配

要先设置具体的匹配次数, 把数字写在 {} 之间即可. 例如: {3} 意味着匹配前一个字符 (或字符集合) 3次.

下面来看一个匹配 RGB 值的例子, 这是前面已经演示的例子, 当时的正则表达式写法是:

#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]

其实前面, 已经可以看出来了, 这个正则虽然很长, 但是后面的部分都是重复的, 所以我们其实只需要这样写就可以了: #[0-9A-Fa-f]{6}. 至于演示代码, 这里就不演示了, 想了解这个示例的读者, 可以去参考之前的内容.

5.2.2 区间范围

{} 语法还可以用来为重复匹配次数设定一个区间范围, 也就是匹配的最小次数和最大次数. 它的形式为: {m, n}. 即匹配次数最少为 m, 最多为n.

下面这个例子中, 将使用一个这样的正则表达式来检查日期的格式:

这里列出的日期格式是一些由于用户可能通过表单字段输入的值, 这些值必须向进行验证, 确保格式正确. \的{1, 2} 匹配一个或两个数字字符 (匹配天数和月份); \d{2,4} 匹配年份; [-\/] (请注意, 这个 \/其实是一个 \ 和一个 /) 匹配日期分隔符 -/. 下面的例子, 总共匹配了 3 个日期值, 但 2/2/2 不在匹配结果里, 因为它的年份太短了.

import re

# 测试文本
text = """"
4/6/17
10-6-2018
2/2/2
01-01-01
"""

# 正则表达式
REGEXP = r'\d{1,2}[-\/]\d{1,2}[-\/]\d{2,4}'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

注意, 上面这个例子里的模式并不能验证日期的有效性, 诸如 54/67/9999 之类的无效日期也能通过这一个测试. 它只能用来检查日期值的格式是否正确 (这一环节通常安排在日期有效性验证之前).

注意 重复范围也可以从 0 开始. 比如, {0,3 表示重复次数可以是 0, 1, 2 或 3. 所以, 其实 ? 等价于 {0,3}.

5.2.3 匹配 “至少重复多少次”

重复范围的最后一种用法是指定至少要匹配多少次 (不指定最大匹配次数). 比如说, {3,} 表示至少重复 3 次, 换句话说, 就是 “重复 3 次或更多次”.

下面来看一个例子, 这里使用一个正则表达式把所有金额大于或等于 100 美元的订单找出来:

import re

# 测试文本
text = """"
1001: $496.80
1002: $1290.69
1003: $26.43
1004: $613.42
1005: $7.61
1006: $414.90
1007: $25.00
"""

# 正则表达式
REGEXP = r'\d+: \$\d{3,}\.\d{2}'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

这个例子里第一列是订单号, 第二列是订单金额. 这里使用的正则表达式首先使用 \d+ 来匹配订单号. 然后是 \$\d{3,}\.\d{2} 用来匹配金额部分, 其中 \$ 匹配 $, \d{3,} 匹配至少 3 位数字, \. 匹配 ., \d{2} 匹配小数点后面的 2 位数字. 该模式从所有订单中正确匹配到了 4 个符合要求的订单.

提示 在使用重复范围的时候一定要小心. 如果你遗漏了花括号里面的逗号, 那么模式的含义将从至少匹配 n 次变成只匹配 n 次.

注意 + 在功能上等价于 {1,}

5.3 防止过度匹配

当我们使用 *+ 时, 它们的匹配是没有上限的, 即如果可能就会一直匹配下去, 这样会造成过度匹配的现象.

之前的例子都是经过了精心挑选的, 不存在过度匹配的问题. 下面, 我们来看一个会导致过度匹配的例子, 这里的任务是使用正则表达式匹配 <b> 标签中的文本 (可能是为了替换格式).

import re

# 测试文本
text = """"
This offer is not available to customers
living in <b>AK</b> and <b>HI</b>.
"""

# 正则表达式
REGEXP = r'<[Bb]>.*<\/[Bb]>'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

<[Bb]> 匹配其实 <b> 标签 (大小写均可), <\/[Bb]> 匹配闭合 </b> 标签 (也是大小写均可). 但是这个模式只找到了一个匹配, 而不是预期的两个. 因为匹配标签中间的内容使用了 .* 它会尝试尽可能长的匹配, 所以只会在最后一个 </b> 处停止匹配.

那么这是为什么呢? 因为 +, * 都是所谓的 “贪婪型” (greedy) 元字符, 其匹配行为是多多益善而不是适可而止. 它们会尽可能地从一段文本的开头一直匹配到末尾, 而不是碰到第一个匹配时就停止. 这是有意设计的, 量词就是贪婪的.

注意 +, *? 也叫做 “量词” (quantifier). 如果阅读英文文档的话, 会遇到这个词.

所以, 如果我们不需要这种 “贪婪行为” 的时候该怎么办呢? 答案是使用这些量词的 “懒惰型” (lazy) 版本 (之所以称之为 “懒惰型” 是因为其匹配尽可能少的字符, 而非尽可能多地去匹配). 懒惰型量词的写法是在贪婪型量词后面加上一个 ?. 下面来列出贪婪型量词及其对应的懒惰型版本.

贪婪型量词懒惰型量词
**?
++?
{n,}{n,}?

还是上面那个匹配 <b> 标签例子, 来让我们看一下吧.

import re

# 测试文本
text = """"
This offer is not available to customers
living in <b>AK</b> and <b>HI</b>.
"""

# 正则表达式
REGEXP = r'<[Bb]>.+?<\/[Bb]>'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

在这里插入图片描述

问题解决了. 因为使用了懒惰型的 *?, 第一个匹配将仅限于 <b>AK</b>, <b>HI</b> 则成为了第二个匹配.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值