re 正则表达式

题目考点

这道题主要考这几个点:

  1. re 正则表达式
  2. 字符串格式判断
  3. 多条件校验
  4. 输入多组数据并逐个输出结果

这题表面上是在考正则,但对初学者来说,真正的难点其实是:

不要一上来就想把所有规则塞进一个超长正则里。
更好的做法是先把题目拆成两类规则,再分别检查。


审题

输入是什么

第一行是一个整数 N,表示后面有多少个信用卡号码要检查。
接下来有 N 行,每行一个字符串,表示一个信用卡号。

输出是什么

对每个信用卡号,输出一行:

  • 合法输出 Valid
  • 不合法输出 Invalid

题目要求我们检查什么

一个合法的卡号要满足:

1)开头必须是 4、5、6 之一

也就是说第一位只能是:

  • 4
  • 5
  • 6

2)总共必须正好 16 位数字

注意这里有两种合法写法:

  • 连着写:4253625879615786
  • 每 4 位用一个 - 分组:5122-2368-7954-3214

3)只能包含数字

如果有别的字符,比如空格、字母、下划线,都不行。

4)如果用了分隔符,只能用 -

而且要严格分成 4 组,每组 4 位。

比如:

  • 5122-2368-7954-3214 合法
  • 5122 2368 7954 3214 不合法
  • 5122_2368_7954_3214 不合法

5)不能出现 4 个或更多连续重复数字

例如:

  • 1111 连着出现了 4 次,不行
  • 4444 连着出现了 4 次,不行

注意这个规则是看“纯数字串”本身,所以即使中间有 -,也要先去掉再检查。

例如:

  • 4444-4444-4444-4444
    去掉 - 后就是一长串 4,当然不合法。

思路提示

先不要急着写代码,先把规则拆开。

第一步:先检查“格式对不对”

也就是先判断这个字符串是不是下面两种形式之一:

  • 16 位纯数字,且第一位是 4/5/6
  • 4 位一组,共 4 组,用 - 连接,且第一位是 4/5/6

这一部分非常适合用正则表达式。

第二步:再检查“有没有连续重复 4 次以上”

这时先把 - 去掉,得到纯数字串。
然后检查有没有像:

  • 0000
  • 1111
  • 9999

这种连续 4 个相同数字。

第三步:两个条件都满足才算合法

也就是:

  • 格式正确
  • 没有 4 连号重复

只要有一个不满足,就是 Invalid


完整设计思路

这题最适合用“两段式判断”。

第一段:判断整体格式

这里我们不用一个特别复杂的大正则,而是写一个容易理解的格式正则:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

可以拆开理解成两种情况:

情况 A:没有 -

^[456]\d{15}$

意思是:

  • ^ 表示从开头开始
  • [456] 表示第一位必须是 4、5、6
  • \d{15} 表示后面再跟 15 个数字
  • $ 表示到结尾结束

合起来就是:总共 16 位,且第一位是 4/5/6

情况 B:有 -

^[456]\d{3}(-\d{4}){3}$

意思是:

  • 第一位是 4/5/6
  • 再跟 3 位数字,先凑够第一组 4 位
  • 然后 (-\d{4}){3} 表示后面重复 3 次:
    • 一个 -
    • 再跟 4 位数字

也就是:

xxxx-xxxx-xxxx-xxxx

这样就严格限制了:
如果用了 -,就必须每 4 位分一次组。


第二段:检查连续重复数字

先把 - 去掉:

digits_only = card.replace('-', '')

然后检查是否出现 4 个或以上连续重复数字:

(\d)\1{3,}

这个正则的意思是:

  • (\d):先抓住一个数字
  • \1:表示“和前面抓住的那个数字相同”
  • \1{3,}:后面至少再重复 3 次

合起来就是:

  • 一共至少连续出现 4 个相同数字

例如:

  • 1111
  • 7777
  • 99999

都能匹配到。


为什么要分两步,而不是一口气写成一个超长正则?

因为这题虽然是正则题,但对初学者来说:

  • “格式判断”是一类问题
  • “连续重复判断”是另一类问题

分开写有三个好处:

  1. 更容易想清楚
  2. 更容易调试
  3. 以后同类题也更容易复用思路

这就是做题里很重要的一点:

先拆条件,再分别解决。


代码实现

下面给你一个适合初学者理解的版本:

import re

n = int(input())

for _ in range(n):
    card = input().strip()

    # 第一步:检查整体格式
    pattern = r'^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$'
    if not re.fullmatch(pattern, card):
        print("Invalid")
        continue

    # 第二步:去掉连字符后,检查是否有 4 个及以上连续重复数字
    digits_only = card.replace('-', '')
    if re.search(r'(\d)\1{3,}', digits_only):
        print("Invalid")
    else:
        print("Valid")

运行演示

我们拿题目里的几个例子手动走一遍。

例 1:4123456789123456

先看格式

  • 4 开头,符合
  • 一共 16 位,符合
  • 都是数字,符合

格式通过。

再看重复数字

去掉 - 后还是:

4123456789123456

里面没有 11112222 这种 4 连重复数字。
所以输出:

Valid

例 2:5123-4567-8912-3456

先看格式

  • 5 开头,符合
  • 4-4-4-4 分组,符合
  • 分隔符是 -,符合

格式通过。

再看重复数字

去掉 - 后:

5123456789123456

没有 4 个连续相同数字。
所以输出:

Valid

例 3:61234-567-8912-3456

先看格式

它不是:

  • 16 位纯数字
    也不是
  • 严格的 4-4-4-4 分组

第一组变成了 5 位,第二组变成了 3 位。
所以格式直接不通过。

输出:

Invalid

例 4:4424444424442444

先看格式

  • 4 开头
  • 一共 16 位
  • 都是数字

格式通过。

再看重复数字

里面有 4444 连续 4 个 4,不符合要求。
所以输出:

Invalid

例 5:5122-2368-7954 - 3214

这里有空格,不是题目允许的格式。
所以正则第一关就过不了。

输出:

Invalid

方法总结

这类题下次可以按这个顺序想:

第一步:先把题目条件分类

看看哪些条件属于:

  • 整体格式
  • 局部特殊限制

这题里就是:

  • 整体格式:开头、长度、分组、分隔符
  • 特殊限制:不能有 4 个连续重复数字

第二步:优先用最合适的工具

  • 整体格式 → 正则
  • 去掉符号后的重复检查 → 正则或循环都可以

第三步:不要追求“一条正则写完宇宙”

初学阶段最重要的不是炫技巧,而是:

写出自己看得懂、能复查、能调试的代码。


一个补充提醒

很多人会写出这种正则:

[456]\d{3}(-?\d{4}){3}

它看起来很简洁,但有个问题:

它会把“有些地方有 -,有些地方没有 -”这种混合格式也放过去。

比如这种怪格式可能会被误判:

4123-45678912-3456

所以这题更稳妥的写法是:

  • 要么全都不带 -
  • 要么严格每 4 位一个 -

也就是我们上面那种“二选一”的写法更安全。


练习

你可以先自己做这道同类型小练习:

题目

给你若干字符串,判断它们是否是“合法手机号”,规则如下:

  1. 必须以 131518 开头
  2. 必须总共 11 位数字
  3. 只能包含数字
  4. 不能出现 3 个及以上连续重复数字

对每个字符串输出 ValidInvalid

提示

你可以还是用“两步法”:

第一步先判断整体格式:

  • 开头
  • 长度
  • 是否全是数字

第二步再检查连续重复数字。

先说整体思路:这两个正则分别在干什么

这道题里其实有两类检查,所以才会用到两个正则,而且这两个正则的职责完全不同。

第一个正则:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

它负责检查的是:整个字符串的格式对不对。

也就是它在回答这样一个问题:

“这个信用卡号,整体上是不是合法长相?”

比如:

  • 是否以 4、5、6 开头
  • 是否总共 16 位
  • 如果带 -,是不是严格按 4 位一组分开

第二个正则:

(\d)\1{3,}

它负责检查的是:有没有连续重复数字。

也就是它在回答这样一个问题:

“这串数字里面,有没有某个数字连续出现 4 次或更多次?”

比如:

  • 1111
  • 4444
  • 99999

所以你可以把它们理解成:

  • 第一个正则:查“外形”
  • 第二个正则:查“内部坏毛病”

下面我们把这两个正则分别拆开讲透。


第一个正则讲透

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

这条正则看起来长,其实它本质上是两个小正则用 | 连起来的。

也就是:

^[456]\d{15}$

或者

^[456]\d{3}(-\d{4}){3}$

这里的 | 是“或者”的意思。

所以整条正则的真实含义是:

“要么是纯 16 位数字的格式,要么是带连字符分组的格式。”

这就非常符合题目的要求。


先拆第一部分:^[456]\d{15}$

这一段负责匹配这种卡号:

4123456789123456

也就是:没有 -,16 位纯数字。

我们一段一段看。

^ 是什么意思

^

它表示:从字符串开头开始匹配。

这个符号非常重要。

因为如果没有它,正则引擎可能会在字符串中间随便找一段符合的内容。
但这道题要检查的是“整个字符串是不是合法”,不是“里面有没有一段合法”。

比如字符串:

abc4123456789123456xyz

如果没有 ^$,里面那段数字可能会被误匹配。
但有了 ^,就要求必须从最开头开始。


[456] 是什么意思

[456]

方括号表示:从里面选一个字符。

所以:

[456]

意思就是:

“这一位只能是 456 中的一个。”

它等价于:

  • 4
  • 或 5
  • 或 6

这正好对应题目要求:信用卡号必须以 4、5、6 开头。

注意,这里匹配的是一个字符,不是多个字符。

所以:

  • 4 可以
  • 5 可以
  • 6 可以
  • 45 不行,因为这里只匹配 1 位

\d{15} 是什么意思

\d{15}

这一段可以拆成两部分看:

\d

\d 表示:任意一个数字字符,也就是:

  • 0
  • 1
  • 2
  • 9

等价于:

[0-9]

{15}

大括号表示“重复次数”。

所以:

\d{15}

意思是:

“数字字符,连续出现 15 次。”

也就是:后面再跟 15 位数字。


$ 是什么意思

$

它表示:到字符串结尾结束。

和前面的 ^ 配合起来,就形成了一个非常关键的效果:

  • ^ 限制开头
  • $ 限制结尾

于是这条正则不再是“匹配一部分”,而是“要求整个字符串完全符合”。


所以 ^[456]\d{15}$ 整体是什么意思

现在把它连起来看:

^[456]\d{15}$

意思就是:

  1. 从开头开始
  2. 第一位必须是 456 之一
  3. 后面必须再跟 15 位数字
  4. 到这里正好结束,不能多也不能少

也就是说:

总共正好 16 位,并且第一位只能是 4、5、6。


再拆第二部分:^[456]\d{3}(-\d{4}){3}$

这一段负责匹配这种格式:

5122-2368-7954-3214

也就是:4 位一组,共 4 组,中间用 - 连接。

我们还是一点点拆。


^[456]\d{3} 是什么意思

这一段表示:

  1. 从开头开始
  2. 第一位是 4、5、6 之一
  3. 后面再来 3 位数字

合起来就是:先匹配第一组 4 位数字。

比如:

5122

或者:

4123

或者:

6789

(-\d{4}){3} 是什么意思

这部分是最容易卡住的地方,我们要慢一点讲。

先看括号里面:

(-\d{4})

它表示一整个小结构:

  • 一个 -
  • 后面跟 4 位数字

比如:

  • -2368
  • -7954
  • -3214

所以这个括号里,不是在匹配一个字符,而是在匹配“一整段”。


为什么这里要加括号

如果你写成:

-\d{4}{3}

这就乱了,因为 {3} 不知道该作用在谁身上。

而加上括号以后:

(-\d{4}){3}

意思就非常明确了:

“把 - 加 4 位数字 这一整组,重复 3 次。”

这正好对应后面的三组:

  • -2368
  • -7954
  • -3214

所以 (-\d{4}){3} 的完整意思

它不是说“4 位数字重复 3 次”,而是说:

“连字符加四位数字”这整个块,重复 3 次。

这点特别关键。

所以:

^[456]\d{3}(-\d{4}){3}$

就表示:

  • 第一组:4 位数字,第一位必须是 4/5/6
  • 然后后面再接 3 组,每组前面有一个 -,后面有 4 位数字
  • 到结尾正好结束

于是整体就是:

xxxx-xxxx-xxxx-xxxx

而且第一位必须是 4/5/6。


为什么这个正则能拦住错误分组

比如这个:

61234-567-8912-3456

为什么不匹配?

因为它的分组是:

  • 61234 5 位
  • 567 3 位
  • 8912 4 位
  • 3456 4 位

而我们的正则要求非常严格:

  • 第一组必须正好 4 位
  • 后面每一组也必须正好 4 位

只要哪里不是 4 位,就过不了。


为什么它还能拦住别的分隔符

比如:

5122 2368 7954 3214

或者:

5122_2368_7954_3214

不匹配的原因也很简单。

因为我们写死了:

-\d{4}

这里明确要求分隔符必须是 -

空格不是 -,下划线也不是 -,所以不合法。


为什么要写成“两段用 | 连接”,而不是写成一个更短的正则

很多人会写这种:

^[456]\d{3}(-?\d{4}){3}$

表面上看很聪明,因为 -? 表示 - 可有可无。
但这会带来一个问题:它允许“有的地方有 -,有的地方没有 -”。

例如这种混乱格式:

4123-45678912-3456

可能就会被放过去。

但题目要求不是“你爱加几个 - 就加几个”,而是:

  • 要么完全不加
  • 要么严格每 4 位加一个 -

所以更稳的写法是:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

这样逻辑更清楚,也更不容易误判。


用几个例子彻底看懂第一个正则

能匹配的

4123456789123456

符合第一种格式。

5122-2368-7954-3214

符合第二种格式。

6789-1234-5678-9012

也符合第二种格式。


不能匹配的

3123456789123456

开头不是 4、5、6。

412345678912345

只有 15 位。

4123-45678-9123-456

分组不对。

4123_4567_8912_3456

分隔符不是 -

4123-4567-8912-34567

最后一组 5 位。


第二个正则讲透

现在看第二个:

(\d)\1{3,}

这个正则和第一个完全不是一类思路。

第一个是在检查“整体结构”,
第二个是在检查“有没有重复模式”。

它最核心的难点在于:反向引用。


先拆:(\d)

括号 () 的作用是什么

这里的括号表示:分组捕获。

也就是说,它不只是拿来分组,还会把匹配到的内容“记住”。

所以:

(\d)

意思是:

  1. 匹配一个数字
  2. 把这个数字保存起来,记为“第 1 组”

比如匹配到:

7

那么这个组里就记住了 7

如果匹配到:

4

那么这个组里就记住了 4


\1 是什么意思

这就是关键。

\1

表示:再次匹配和第 1 组完全相同的内容。

注意,这里不是“任意一个数字”,也不是“数字 1”。

它的意思是:

“再来一个和前面括号里捕获到的东西一样的字符。”

举个例子。

如果前面的 (\d) 匹配到的是:

4

那么后面的 \1 就等价于:

4

如果前面的 (\d) 匹配到的是:

9

那么后面的 \1 就等价于:

9

所以:

(\d)\1

整体意思就是:

“两个相同的连续数字。”

它能匹配:

  • 11
  • 22
  • 77

但不能匹配:

  • 12
  • 34
  • 98

\1{3,} 是什么意思

这部分再拆一下:

\1

和第 1 组相同的内容

{3,}

重复 3 次或更多次

所以:

\1{3,}

意思就是:

“把刚才那个相同字符,再重复至少 3 次。”


那么 (\d)\1{3,} 整体是什么意思

第一步:

(\d)

先抓住一个数字。

第二步:

\1{3,}

要求后面至少再出现 3 个和它一模一样的数字。

合起来就是:

某个数字,连续出现 4 次或以上。

因为总数是:

  • 前面括号里 1 次
  • 后面再重复至少 3 次

加起来至少 4 次。


它能匹配哪些内容

可以匹配

1111

因为:

  • (\d) 抓住第一个 1
  • \1{3,} 再匹配后面至少 3 个 1
4444

同理成立。

99999

也成立,因为它是连续 5 个 9,而 {3,} 表示“至少 3 个”,5 个当然也符合。


不可以匹配

111

只有 3 个,不够 4 个。

1212

不是连续重复。

1234

每个数字都不同。

4443

前面只有 3 个连续 4,还是不够。


为什么要先去掉 - 再用这个正则检查

比如:

5122-2368-7954-3214

- 的时候,连续重复会被打断。

而题目要求看的其实是“数字本身”,不是“带分隔符的表面写法”。

比如:

4444-4444-4444-4444

如果你不去掉 -,中间有分隔符,连续性在字符串层面被断开了。
但题目的本意是:这张卡的数字里大量重复,不合法。

所以要先:

digits_only = card.replace('-', '')

变成:

4444444444444444

然后再用:

(\d)\1{3,}

去检查。

这样才符合题意。


用“程序运行视角”理解 (\d)\1{3,}

假设字符串是:

2223456789012345

正则从左往右尝试。

一开始看到第一个字符 2

  • (\d) 抓到 2
  • 后面看能不能再跟至少 3 个 2

结果后面只有一个 2,不够,所以这里失败。

然后继续往后试。

如果字符串是:

2222345678901234

从第一个 2 开始:

  • (\d) 抓到 2
  • 后面三个字符也是 2
  • 满足 \1{3,}

于是成功匹配到:

2222

一个容易误解的点:\1 不是数字 1

很多初学者第一次看到会误会:

\1 是不是表示字符 1?”

不是。

这里的 \1 表示:

第 1 个捕获组的内容。

如果前面组里抓到的是 7,那 \1 就是 7
如果抓到的是 3,那 \1 就是 3

它和数字字符 '1' 完全不是一回事。


一个更直观的类比

你可以把:

(\d)\1{3,}

理解成下面这句话:

“先随便抓一个数字,把它记住;然后要求后面至少再连续出现 3 个和它一样的数字。”

这个“记住再复用”的机制,就是反向引用的本质。


两个正则放在一起看,逻辑就更清楚了

第一个正则

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

负责检查:

  • 开头是不是 4/5/6
  • 长度对不对
  • 分组格式对不对
  • 分隔符是不是 -

第二个正则

(\d)\1{3,}

负责检查:

  • 是否存在 4 个或以上连续相同数字

所以它们不是互相替代关系,而是前后配合关系。


这两个正则在代码里为什么一个常配 fullmatch,一个常配 search

这点顺手也给你讲透。

第一个正则常用 re.fullmatch()

因为它要检查的是:

整个字符串是否完全符合格式。

所以适合:

re.fullmatch(pattern, card)

这就相当于在说:

“整个 card 从头到尾都必须符合。”


第二个正则常用 re.search()

因为它要检查的是:

字符串里有没有某一段出现违规模式。

所以适合:

re.search(r'(\d)\1{3,}', digits_only)

这相当于在说:

“你不用整个字符串都长这样,只要里面某个地方出现 4 连重复,就算违规。”


初学者最容易犯的几个错误

错误一:把 | 的范围想错

很多人看到:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

会觉得很乱。

其实你要主动把它切成两半:

  • ^[456]\d{15}$
  • ^[456]\d{3}(-\d{4}){3}$

| 左右是两套完整格式,不要把它想成中间某一小块在“或者”。


错误二:把 (-\d{4}){3} 理解成“数字重复三次”

不是。

它表示的是:

- 加 4 位数字”这个整体,重复 3 次。

括号管住的内容,是一个整体单元。


错误三:把 \1 看成字符 1

它不是字符 1,而是“第 1 个捕获组”。


错误四:忘记先去掉 - 再检查重复数字

如果不去掉,很多本来该判非法的情况可能会漏掉。


你可以这样记忆这两个正则

记第一个正则

把它记成:

“合法信用卡有两种外观:纯 16 位,或者 4-4-4-4 分组。”

所以:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

不是在背符号,而是在背“外观模板”。


记第二个正则

把它记成:

“先抓一个数字,再要求它连续重复至少 3 次。”

所以:

(\d)\1{3,}

是在找“4 连号重复”。


本节小结

这两个正则你现在可以分别这样理解:

第一个正则:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

本质上是在判断:

“这串字符串是不是合法的信用卡格式。”

它检查的是整体外形,核心是两种格式二选一:

  1. 16 位纯数字
  2. 4 位一组、共 4 组、用 - 连接

第二个正则:

(\d)\1{3,}

本质上是在判断:

“里面有没有某个数字连续出现 4 次或以上。”

它利用了捕获组和反向引用:

  • (\d) 先记住一个数字
  • \1{3,} 再要求后面至少连续重复 3 次同样的数字

所以整道题的思路就变成了:

先查格式,再查连续重复。

这就是这题最核心的拆题方法。


练习

你先不要急着继续看答案,先自己想一想下面几个字符串分别会不会被这两个正则匹配。

练习 1:判断是否能通过“格式正则”

也就是判断是否匹配:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

请你判断下面这些:

4123456789123456
5122-2368-7954-3214
7123456789123456
4123-4567-8912-345
4123_4567_8912_3456

提示:重点看开头、总位数、分组、分隔符。

练习 2:判断是否能被“重复检测正则”搜到

也就是判断是否能匹配:

(\d)\1{3,}

请你判断下面这些纯数字串:

1234567890123456
1112345678901234
1234444567890123
1212121212121212
9999123456789012

提示:重点看有没有某个数字连续出现至少 4 次。

补充一节:为什么 (\d)\1{3,} 只写了一个括号组,却能“记住前面抓到的数字”

这个问题问得非常好。很多人第一次看到正则里的 \1,都会有一种感觉:

前面明明只是写了一个 (\d),它怎么就像有记忆一样,知道前面匹配到的到底是 37 还是 9

这里的关键,不在于括号“长得像括号”,而在于:

正则里的圆括号 (),在很多情况下不只是分组,它还会“捕获”匹配结果。

也就是说,(\d) 这一步不只是“匹配一个数字”,它还顺手把这个数字保存起来了。后面的 \1,就是在取回这个保存下来的内容。

下面我们把这个过程一点点拆开。


先建立一个最核心的认识

当正则引擎看到:

(\d)

它做的不是一件事,而是两件事:

第一件事:匹配一个数字。
第二件事:把这个匹配到的数字,保存到“第 1 组”里。

所以,(\d) 的括号不是摆设。
它的意思不是单纯“把 \d 括起来”,而是:

把这部分作为一个捕获组保存下来。

这就是为什么后面可以写:

\1

这里的 1,指的就是:

第 1 个捕获组里保存的内容。

所以 \1 不是数字字符 1,而是“把第 1 组刚才记住的内容,再拿出来用一次”。


你可以把它理解成“先存起来,再引用”

如果把 (\d)\1{3,} 翻译成更口语的话,它的意思其实很像这样:

  1. 先抓一个数字,把它记下来
  2. 然后要求后面至少再出现 3 个和它一模一样的数字

所以这个正则不是在说:

“匹配 4 个任意数字”

它是在说:

“匹配 4 个连续相同的数字”

而这个“相同”,正是靠前面的“记住”实现的。


正则引擎到底是怎么“记住”的

这里你可以把正则引擎想成一个很机械的检查员。它从左到右扫描字符串,每走到一处,就按正则的规则试着匹配。

当它看到 (\d) 时,它会做下面这件事:

第一步:尝试匹配一个数字

如果当前位置是数字,比如 4,那就匹配成功。

第二步:把这次匹配到的内容存到捕获组里

因为这里有括号,所以这个数字不会只是“匹配完就算了”,而是会被记到“第 1 组”里。

例如当前匹配到的是:

4

那么此时正则引擎内部就可以理解成:

  • 第 1 组 = "4"

然后它继续往后看,发现后面是:

\1{3,}

这时候它不会再把 \1 当作“随便一个数字”,而是会去查刚才存下来的内容:

  • 第 1 组里存的是 "4"

于是:

\1

就相当于:

4

而:

\1{3,}

就相当于:

“后面至少再来 3 个 4

这就是“记住前面抓到的数字”的本质。


用一个具体例子手动模拟

假设我们现在检查这个字符串:

4444

正则是:

(\d)\1{3,}

下面看正则引擎是怎么走的。

第 1 步:匹配 (\d)

当前位置是第一个字符 4

  • \d 能匹配数字
  • 所以这里成功匹配到一个 4

因为有括号,这个 4 会被保存下来:

  • 第 1 组 = "4"

到这里,前半段 (\d) 完成了。


第 2 步:匹配 \1{3,}

现在引擎去看第 1 组里保存的内容,发现是:

"4"

所以 \1 就表示“再匹配一个 4”。

{3,} 表示:

  • 至少重复 3 次

所以这一步的真实意思就变成了:

  • 后面至少还要有 4
  • 4
  • 4

一看剩下的字符串正好是三个 4,满足要求。

于是整段匹配成功。


再看一个失败的例子

假设字符串是:

4445

还是用同样的正则:

(\d)\1{3,}

第 1 步:匹配 (\d)

第一个字符是 4,匹配成功。

保存:

  • 第 1 组 = "4"

第 2 步:匹配 \1{3,}

后面至少要再来 3 个 4

现在剩下的是:

445

前两个字符是 44,没问题。
但第三个字符是 5,不是 4

所以不满足“至少再来 3 个相同数字”的要求。

于是这次匹配失败。


再看一个不是从开头成功,而是在中间成功的例子

假设字符串是:

123444456

用的是 re.search(r'(\d)\1{3,}', s)

这里 search 的意思是:
不用整串都符合,只要里面某一段符合就行。

正则引擎会从左往右试。

从第一个字符 1 开始试

  • (\d) 抓到 1
  • 后面要至少再来 3 个 1
  • 结果后面不是 111,失败

从第二个字符 2 开始试

  • 抓到 2
  • 后面不是连续 3 个 2
  • 失败

从第三个字符 3 开始试

  • 抓到 3
  • 后面不是连续 3 个 3
  • 失败

从第四个字符 4 开始试

  • (\d) 抓到 4
  • 保存:第 1 组 = "4"
  • 后面正好还有至少 3 个连续的 4

成功。

所以这串字符串会被判断为“存在 4 个连续相同数字”。


这里的“记住”不是永久记忆,而是当前匹配过程中的临时保存

这点也很重要。

很多初学者会把它想得太神秘,觉得正则引擎是不是像程序变量一样长期保存了什么值。

其实不是。

这里的“记住”,更准确地说是:

在当前这一次匹配尝试里,把捕获组匹配到的内容临时存下来。

比如它从某个位置开始尝试:

(\d)

匹配到了 7,那这次尝试里:

  • 第 1 组 = "7"

如果这次尝试后面失败了,正则引擎会换一个起点重新试。
重新试的时候,捕获组保存的内容也会跟着重新变化。

所以它不是“全局永久记忆”,而是“当前匹配分支里的临时记录”。


为什么圆括号能做到这件事

因为在大多数正则实现里,圆括号默认就是“捕获组”。

也就是说:

(\d)

默认含义是:

  • \d 当成一个整体
  • 并且把它匹配到的内容记录下来

如果只是想分组,但不想保存,一般会写成:

(?:\d)

这个你现在不用深究,但你先知道一件事就够了:

普通圆括号 () 默认会捕获内容。

所以 (\d) 不是普通括起来,而是“带记录功能的括起来”。


为什么 \1 恰好指向前面的这个括号组

因为正则里的捕获组是按“左括号出现的顺序”编号的。

例如:

(a)(b)(c)

那么:

  • 第 1 组是 (a)
  • 第 2 组是 (b)
  • 第 3 组是 (c)

所以在:

(\d)\1{3,}

里,只有一个捕获组,那它自然就是第 1 组。
于是后面的 \1 就是在引用它。

如果有多个组,比如:

(\d)([A-Z])

那:

  • \1 表示前面的数字
  • \2 表示后面的字母

不过这题里只有一个组,所以只会用到 \1


用“占位符”的方式理解会更直观

你还可以把它想成这样。

当正则看到:

(\d)

它就像先建立了一个“占位符盒子”:

  • 盒子 1:暂时空着

匹配到某个数字后,比如 8,盒子里就装上:

  • 盒子 1 = 8

后面写的:

\1

就像在说:

“把 1 号盒子里的东西再拿出来匹配一次。”

于是如果盒子里是 8,那么 \1 就是在匹配 8

再加上 {3,},就变成:

“把盒子里的这个内容,至少再重复 3 次。”

这个想法对初学者特别有帮助,因为它比“反向引用”这个术语更好理解。


为什么不写成 (\d){4}

这是一个特别值得补的点。

有些人会想:

“既然要匹配 4 个数字,为什么不写成 (\d){4}?”

因为这两个东西意思完全不一样。

(\d){4} 的意思

它表示:

  • 匹配一个数字,这个动作重复 4 次

所以它能匹配:

  • 1234
  • 5678
  • 4444

也就是说,它只是“4 个数字”,不要求相同。


(\d)\1{3,} 的意思

它表示:

  • 先抓一个数字
  • 后面至少再重复 3 次同样的数字

所以它只能匹配:

  • 1111
  • 4444
  • 99999

不能匹配:

  • 1234
  • 1212
  • 4555(从开头看不行,但中间 555 也不够 4 个)

所以这道题必须用“捕获 + 引用”,不能只写“数字重复 4 次”。


再从代码视角理解一下

这条正则其实很像下面这种伪代码逻辑:

先取当前字符 x
然后检查后面是不是至少还有 3 个连续的 x

也就是说,正则并不是神秘地“凭空记忆”,而是在内部做了类似这种事情:

  1. 先把匹配到的内容放进一个组里
  2. 后面遇到 \1 时,再拿这个组里的值做比较

所以你完全可以把它理解成:

正则引擎内部偷偷帮你做了一个“临时变量保存”的动作。

只是这个“变量”不是你手写的 Python 变量,而是正则引擎维护的“捕获组”。


这道题里它为什么特别合适

因为题目要求的是:

不能有 4 个或更多连续重复数字。

这里的重点不是“连续 4 个数字”,而是“连续 4 个相同数字”。

而“相同”这件事,用普通字符类是做不到的。
你不能只靠 \d 表达“后面必须和前面那个数字一样”。

所以必须要:

  1. 先把前面的数字抓住
  2. 再引用它

这正是 (\d)\1{3,} 的价值所在。


本节小结

现在你可以把这个过程记成一句非常清楚的话:

当正则引擎匹配到 (\d) 时,它不仅会匹配一个数字,还会把这个数字保存到“第 1 个捕获组”里。后面的 \1 就是在引用这个捕获组中保存的内容,所以 `(\d)\1{3,}“ 的意思不是“4 个任意数字”,而是“先抓一个数字,再要求后面至少连续出现 3 个相同的数字”,也就是“某个数字连续出现 4 次或以上”。

如果你以后再看到类似:

(.)\1

或者

([a-z])\1

你也可以用同样的思路去理解:

  • 前面括号先抓一个东西并记住
  • 后面的 \1 再要求出现相同的东西

练习

你可以先自己判断下面这些字符串,re.search(r'(\d)\1{3,}', s) 会不会匹配成功。

题目

判断下面每个字符串中,是否能搜到“4 个或以上连续相同数字”:

12345555
122223
8888
90909090
7000001

提示

不要只看“有没有很多重复数字”,要看两点:

  1. 是不是同一个数字
  2. 是不是连续出现至少 4 次

补一节:为什么这题用 re.search(),而不是 re.match()re.fullmatch(),这三个函数到底分别适合检查什么

这个问题非常关键。因为很多初学者学正则时,往往不是看不懂正则本身,而是不知道该配哪个函数一起用

你现在已经看到这道题里有两类检查:

第一类是检查“整个信用卡号格式是否正确”;
第二类是检查“字符串内部有没有某个违规片段”。

这两类检查的目标不同,所以对应使用的函数也不同。

这就是为什么这题里常见写法是:

  • 格式检查:re.fullmatch()
  • 连续重复检查:re.search()

而不是全都统一用一种。

下面我们把 re.search()re.match()re.fullmatch() 分别讲透。


先给你一个最核心的结论

你可以先记住这三句话:

re.search():看“字符串里某个地方有没有我要找的内容”
re.match():看“字符串开头是不是我要找的内容”
re.fullmatch():看“整个字符串是不是完完整整符合我要的格式”

这三者的区别,核心就在于:

它们对“匹配范围”的要求不一样。


一、re.search() 到底在做什么

它的本质

re.search(pattern, s) 的意思是:

在整个字符串里找一找,只要某个位置出现了符合 pattern 的片段,就算成功。

注意,是“某个位置”,不要求从头开始,也不要求整串都符合。


举个最简单的例子

import re

s = "abc123xyz"
print(re.search(r"\d+", s))

这里的 \d+ 表示“一个或多个数字”。

虽然整个字符串不是纯数字,
但中间有一段 "123" 符合,
所以 search() 会成功。

这说明:

search() 关注的是:

里面有没有一段符合。


为什么这题检查“连续重复数字”适合用 search()

这题有一个规则是:

不能有 4 个或以上连续重复数字

对应正则是:

(\d)\1{3,}

这个正则的含义不是:

“整个字符串都必须长成这样”

而是:

“只要里面某个地方出现了这种模式,就违规”

比如字符串:

1234444567890123

我们并不是说整串都等于 4444
而是说它里面有一段 "4444",所以不合法。

这正是 search() 最擅长的事情:

找内部某一段违规片段。

所以这里写:

re.search(r'(\d)\1{3,}', digits_only)

意思就是:

“在整个卡号数字串里搜一搜,只要某个地方出现 4 个连续相同数字,就算找到了。”


二、re.match() 到底在做什么

它的本质

re.match(pattern, s) 的意思是:

只从字符串开头开始匹配。

也就是说,它默认会问:

“开头这一段像不像我要的格式?”

但它不要求整个字符串都必须匹配完。


一个直观例子

import re

s = "123abc"
print(re.match(r"\d+", s))

这里会成功,因为字符串一开头就是数字 123

但如果是:

s = "abc123"
print(re.match(r"\d+", s))

这里就失败,因为开头不是数字。

所以 match() 的重点是:

必须从第一个字符开始。


为什么 match() 容易让初学者误用

因为很多人会以为:

match() = “匹配整个字符串”

其实不是。

它只是要求:

从开头开始匹配

但后面多出来的内容,它不一定管。

比如:

import re

s = "123abc"
print(re.match(r"\d+", s))

会成功。

因为开头的 "123" 匹配到了,
虽然后面还有 "abc",但 match() 不在乎。

这就是它和 fullmatch() 最大的区别。


三、re.fullmatch() 到底在做什么

它的本质

re.fullmatch(pattern, s) 的意思是:

要求整个字符串,从头到尾,完整符合这个正则。

注意这个“完整”非常关键。

不是前面一段符合就行,
不是中间有一段符合就行,
而是必须整串都匹配掉。


最简单的例子

import re

s = "123"
print(re.fullmatch(r"\d+", s))

这里成功,因为整个字符串都是数字。

但如果:

s = "123abc"
print(re.fullmatch(r"\d+", s))

这里失败,因为后面还有字母,整串不全是数字。

所以 fullmatch() 适合做这种事:

“这整个输入,是否完全符合某种格式要求?”


四、把三者放在一起对比

我们用同一个字符串来感受差别。

字符串:

s = "abc123xyz"

正则:

\d+

现在看三种函数的结果。

re.search(r"\d+", s)

会成功。
因为中间有 "123" 这段数字。

它不管前后是什么,只要找到就行。


re.match(r"\d+", s)

会失败。
因为开头是 a,不是数字。

它只看开头。


re.fullmatch(r"\d+", s)

也会失败。
因为整个字符串不是纯数字。

它要求整串都符合。


五、这题为什么“格式检查”更适合 fullmatch()

这题第一个正则是:

^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$

它检查的是:

  • 开头是否是 4/5/6
  • 长度是否正确
  • 分组是否正确
  • 分隔符是否正确

这本质上是在问:

这个字符串整体,是不是一个合法信用卡格式。

这类问题,不是在找内部某一段,
而是在判断整个字符串是否合规。

所以最适合的是:

re.fullmatch(pattern, card)

虽然你会发现这个正则里已经有 ^$,看起来也在约束全串。
但从语义上讲,fullmatch() 本身就最适合做“整串合法性判断”。

也就是说:

  • search() 是找片段
  • fullmatch() 是查整体格式

这题的格式检查明显属于第二种。


六、这题为什么“重复检查”更适合 search()

第二个正则是:

(\d)\1{3,}

这个正则不是在描述“整个卡号长什么样”,
而是在描述“有没有某个局部违规片段”。

例如:

1234444567890123

整串当然不是 "4444"
但中间出现了 "4444",这就已经足够判定为违规。

所以这里需要的是:

在整串中找一个满足条件的子串。

这正是 search() 的工作。

所以:

re.search(r'(\d)\1{3,}', digits_only)

非常合适。


七、那这题能不能用 re.match()

对重复检查来说,不合适

如果你写:

re.match(r'(\d)\1{3,}', digits_only)

那它只会检查:

开头是不是就有 4 个连续相同数字。

这就太窄了。

比如:

1234444567890123

中间有 "4444",应该判违规。
match() 只看开头,开头不是 "4444",于是会漏掉。

所以检查连续重复时,用 match() 是不对的。


对格式检查来说,理论上可以,但不如 fullmatch() 清楚

你可能会想:

如果我把格式正则写得很完整,比如带 ^$,那 match() 也能用吧?

是的,理论上可以,比如:

re.match(r'^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$', card)

因为你已经手动用 ^$ 锁死了开头和结尾。
这样也能达到类似“整串匹配”的效果。

但是从写代码的语义来说,不如 fullmatch() 直接、清楚。

因为 fullmatch() 天然表达的是:

“我要检查整个字符串是否合法。”

读代码的人一眼就明白你的意图。

所以在这类“整串格式验证”场景下,优先用 fullmatch() 会更自然。


八、你可以把这三种函数理解成三种检查方式

search():巡逻式检查

像保安在整条街巡逻,只要发现某个角落有问题,就报告。

适合:

  • 搜违规片段
  • 搜关键词
  • 搜某种局部模式

比如这题里的:

  • 有没有 4 连重复数字

match():门口检查

像保安只站在门口,检查“最前面这一段是不是符合要求”。

适合:

  • 检查开头格式
  • 判断是否以某模式起始

比如:

  • 是否以数字开头
  • 是否以 http 开头
  • 是否以某关键字起头

fullmatch():整体验收

像质检员检查整个产品,从头到尾都要符合标准。

适合:

  • 手机号整体是否合法
  • 邮箱整体是否合法
  • 身份证号整体是否合法
  • 信用卡号整体是否合法

这题的格式判断就是这一类。


九、为什么很多教程里还会看到 ^$

这是你后面复习时很容易疑惑的一点。

你可能会问:

“既然有 re.fullmatch(),为什么还要写 ^$?”

原因有两个。

第一,历史写法习惯

很多人以前习惯这样写:

re.match(r'^\d{11}$', s)

或者:

re.search(r'^\d{11}$', s)

^$ 手动约束整串。

这是老习惯,也很常见。


第二,正则本身也更完整

^$ 写进正则里,本身也能更明确表达:

  • 从开头开始
  • 到结尾结束

即使和 fullmatch() 有点功能重叠,很多人也会保留。

所以这题里写成:

re.fullmatch(r'[456]\d{15}|[456]\d{3}(-\d{4}){3}', card)

可以。

写成:

re.fullmatch(r'^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$', card)

通常也能工作,只是有一点重复强调的味道。

对于初学阶段来说,你可以先记住一个最实用的原则:

整串合法性判断,优先想到 fullmatch()
内部找违规片段,优先想到 search()


十、放回这道题,最推荐你这样理解

这题其实就是两个问题。

问题一:这张卡“整体长相”对不对

这是一个整串验证问题。

所以用:

re.fullmatch(...)

问题二:这张卡里面“某个地方”有没有违规 4 连重复数字

这是一个局部搜索问题。

所以用:

re.search(...)

十一、一个最清晰的对照代码

你可以把这题最标准的思路写成这样:

import re

n = int(input())

for _ in range(n):
    card = input().strip()

    format_pattern = r'[456]\d{15}|[456]\d{3}(-\d{4}){3}'

    if not re.fullmatch(format_pattern, card):
        print("Invalid")
        continue

    digits_only = card.replace('-', '')

    if re.search(r'(\d)\1{3,}', digits_only):
        print("Invalid")
    else:
        print("Valid")

这里你会发现:

re.fullmatch(format_pattern, card)

意思是:

“整张卡号,从头到尾,必须完整符合格式模板。”

re.search(r'(\d)\1{3,}', digits_only)

意思是:

“在纯数字串里找一找,有没有某处出现 4 个连续相同数字。”

这就是两个函数在这题里的分工。


本节小结

re.search()re.match()re.fullmatch() 的区别,不是“哪个更高级”,而是“检查目标不同”。

re.search() 用来找“字符串里某处有没有这种模式”;
re.match() 用来检查“字符串开头是不是这种模式”;
re.fullmatch() 用来检查“整个字符串是不是完全符合这种模式”。

所以这道信用卡题里:

格式检查属于“整串是否合法”,用 re.fullmatch() 最合适;
连续重复检查属于“内部是否出现违规片段”,用 re.search() 最合适;
re.match() 因为只看开头,不适合拿来检查中间某处是否出现 4 连重复数字。


练习

你可以先自己判断下面每种情况更适合用哪个函数:search()match() 还是 fullmatch()

题目 1

判断一个字符串是否是“正好 11 位的手机号”。

提示:这是在查整体格式,还是在找局部片段?

题目 2

判断一句话里有没有出现连续 3 个感叹号 !!!

提示:这是在查整串,还是在整句话里找某一小段?

题目 3

判断一个字符串是否以 https:// 开头。

提示:重点要求的是开头,还是整串格式?

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇