
题目考点
这道题主要考这几个点:
re正则表达式- 字符串格式判断
- 多条件校验
- 输入多组数据并逐个输出结果
这题表面上是在考正则,但对初学者来说,真正的难点其实是:
不要一上来就想把所有规则塞进一个超长正则里。
更好的做法是先把题目拆成两类规则,再分别检查。
审题
输入是什么
第一行是一个整数 N,表示后面有多少个信用卡号码要检查。
接下来有 N 行,每行一个字符串,表示一个信用卡号。
输出是什么
对每个信用卡号,输出一行:
- 合法输出
Valid - 不合法输出
Invalid
题目要求我们检查什么
一个合法的卡号要满足:
1)开头必须是 4、5、6 之一
也就是说第一位只能是:
456
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 次以上”
这时先把 - 去掉,得到纯数字串。
然后检查有没有像:
000011119999
这种连续 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 个相同数字
例如:
1111777799999
都能匹配到。
为什么要分两步,而不是一口气写成一个超长正则?
因为这题虽然是正则题,但对初学者来说:
- “格式判断”是一类问题
- “连续重复判断”是另一类问题
分开写有三个好处:
- 更容易想清楚
- 更容易调试
- 以后同类题也更容易复用思路
这就是做题里很重要的一点:
先拆条件,再分别解决。
代码实现
下面给你一个适合初学者理解的版本:
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
里面没有 1111、2222 这种 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 位一个
-
也就是我们上面那种“二选一”的写法更安全。
练习
你可以先自己做这道同类型小练习:
题目
给你若干字符串,判断它们是否是“合法手机号”,规则如下:
- 必须以
13、15、18开头 - 必须总共 11 位数字
- 只能包含数字
- 不能出现 3 个及以上连续重复数字
对每个字符串输出 Valid 或 Invalid。
提示
你可以还是用“两步法”:
第一步先判断整体格式:
- 开头
- 长度
- 是否全是数字
第二步再检查连续重复数字。
先说整体思路:这两个正则分别在干什么
这道题里其实有两类检查,所以才会用到两个正则,而且这两个正则的职责完全不同。
第一个正则:
^[456]\d{15}$|^[456]\d{3}(-\d{4}){3}$
它负责检查的是:整个字符串的格式对不对。
也就是它在回答这样一个问题:
“这个信用卡号,整体上是不是合法长相?”
比如:
- 是否以 4、5、6 开头
- 是否总共 16 位
- 如果带
-,是不是严格按 4 位一组分开
第二个正则:
(\d)\1{3,}
它负责检查的是:有没有连续重复数字。
也就是它在回答这样一个问题:
“这串数字里面,有没有某个数字连续出现 4 次或更多次?”
比如:
1111444499999
所以你可以把它们理解成:
- 第一个正则:查“外形”
- 第二个正则:查“内部坏毛病”
下面我们把这两个正则分别拆开讲透。
第一个正则讲透
^[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]
意思就是:
“这一位只能是 4、5、6 中的一个。”
它等价于:
- 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}$
意思就是:
- 从开头开始
- 第一位必须是
4、5、6之一 - 后面必须再跟 15 位数字
- 到这里正好结束,不能多也不能少
也就是说:
总共正好 16 位,并且第一位只能是 4、5、6。
再拆第二部分:^[456]\d{3}(-\d{4}){3}$
这一段负责匹配这种格式:
5122-2368-7954-3214
也就是:4 位一组,共 4 组,中间用 - 连接。
我们还是一点点拆。
^[456]\d{3} 是什么意思
这一段表示:
- 从开头开始
- 第一位是 4、5、6 之一
- 后面再来 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
为什么不匹配?
因为它的分组是:
612345 位5673 位89124 位34564 位
而我们的正则要求非常严格:
- 第一组必须正好 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 组”
比如匹配到:
7
那么这个组里就记住了 7。
如果匹配到:
4
那么这个组里就记住了 4。
\1 是什么意思
这就是关键。
\1
表示:再次匹配和第 1 组完全相同的内容。
注意,这里不是“任意一个数字”,也不是“数字 1”。
它的意思是:
“再来一个和前面括号里捕获到的东西一样的字符。”
举个例子。
如果前面的 (\d) 匹配到的是:
4
那么后面的 \1 就等价于:
4
如果前面的 (\d) 匹配到的是:
9
那么后面的 \1 就等价于:
9
所以:
(\d)\1
整体意思就是:
“两个相同的连续数字。”
它能匹配:
112277
但不能匹配:
123498
\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}$
本质上是在判断:
“这串字符串是不是合法的信用卡格式。”
它检查的是整体外形,核心是两种格式二选一:
- 16 位纯数字
- 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),它怎么就像有记忆一样,知道前面匹配到的到底是 3、7 还是 9?
这里的关键,不在于括号“长得像括号”,而在于:
正则里的圆括号 (),在很多情况下不只是分组,它还会“捕获”匹配结果。
也就是说,(\d) 这一步不只是“匹配一个数字”,它还顺手把这个数字保存起来了。后面的 \1,就是在取回这个保存下来的内容。
下面我们把这个过程一点点拆开。
先建立一个最核心的认识
当正则引擎看到:
(\d)
它做的不是一件事,而是两件事:
第一件事:匹配一个数字。
第二件事:把这个匹配到的数字,保存到“第 1 组”里。
所以,(\d) 的括号不是摆设。
它的意思不是单纯“把 \d 括起来”,而是:
把这部分作为一个捕获组保存下来。
这就是为什么后面可以写:
\1
这里的 1,指的就是:
第 1 个捕获组里保存的内容。
所以 \1 不是数字字符 1,而是“把第 1 组刚才记住的内容,再拿出来用一次”。
你可以把它理解成“先存起来,再引用”
如果把 (\d)\1{3,} 翻译成更口语的话,它的意思其实很像这样:
- 先抓一个数字,把它记下来
- 然后要求后面至少再出现 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 44
一看剩下的字符串正好是三个 4,满足要求。
于是整段匹配成功。
再看一个失败的例子
假设字符串是:
4445
还是用同样的正则:
(\d)\1{3,}
第 1 步:匹配 (\d)
第一个字符是 4,匹配成功。
保存:
- 第 1 组 =
"4"
第 2 步:匹配 \1{3,}
后面至少要再来 3 个 4。
现在剩下的是:
445
前两个字符是 4、4,没问题。
但第三个字符是 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 次
所以它能匹配:
123456784444
也就是说,它只是“4 个数字”,不要求相同。
(\d)\1{3,} 的意思
它表示:
- 先抓一个数字
- 后面至少再重复 3 次同样的数字
所以它只能匹配:
1111444499999
不能匹配:
123412124555(从开头看不行,但中间555也不够 4 个)
所以这道题必须用“捕获 + 引用”,不能只写“数字重复 4 次”。
再从代码视角理解一下
这条正则其实很像下面这种伪代码逻辑:
先取当前字符 x
然后检查后面是不是至少还有 3 个连续的 x
也就是说,正则并不是神秘地“凭空记忆”,而是在内部做了类似这种事情:
- 先把匹配到的内容放进一个组里
- 后面遇到
\1时,再拿这个组里的值做比较
所以你完全可以把它理解成:
正则引擎内部偷偷帮你做了一个“临时变量保存”的动作。
只是这个“变量”不是你手写的 Python 变量,而是正则引擎维护的“捕获组”。
这道题里它为什么特别合适
因为题目要求的是:
不能有 4 个或更多连续重复数字。
这里的重点不是“连续 4 个数字”,而是“连续 4 个相同数字”。
而“相同”这件事,用普通字符类是做不到的。
你不能只靠 \d 表达“后面必须和前面那个数字一样”。
所以必须要:
- 先把前面的数字抓住
- 再引用它
这正是 (\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
提示
不要只看“有没有很多重复数字”,要看两点:
- 是不是同一个数字
- 是不是连续出现至少 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:// 开头。
提示:重点要求的是开头,还是整串格式?



