R

ReflexKR

Reflex 官方文档

看得见的正则表达式

·

课程概览

1

消失的支付记录:按原样查找文字

2

肇事逃逸车辆:字符类

3

撕破的电话簿:量词

4

虚假注册:锚点与校验

5

五花八门的日期:分组与选择

6

凌晨 3 点的访问:命名分组与 Match 对象

7

见报前夜:替换与分割

8

回溯者的挑战书:贪婪、懒惰与占有

9

保险箱的规则:前后查找

10

同样的手法:反向引用与条件模式

11

读不懂的正则:标志与编译

12

回溯者的真面目:总复习与分词器

看得见的正则表达式
第5课

时间 3:20–4:10

五花八门的日期:分组与选择

CASE-005

Grep & Co. 数据侦探事务所

委托书

五花八门的日期

委托人

网店“郑市集”的郑老板

证物

郑市集的订单记录。每个兼职店员都用不同的样子写日期。

破案条件

从所有订单里分别取出年、月、日,整理成统一的样子。

虚假注册案的第二天,郑老板又来了。这回她先叹了口气。“注册是拦住了。可我想按日期整理订单记录,才发现三个兼职店员写的日期全都不一样。”所长喝了一口咖啡。“怎么找,第一部已经学过了。现在该学怎样从找到的东西里取出来。”

订单记录如下。

#1001 2026-10-20 ship=done
#1002 2026.10.20 ship=wait
#1003 2026/10/19 ship=done
#1004 20261019 ship=done
#1005 2026-10/20 ship=wait

光找到日期还不够。得把“2026”“10”“20”分别取出来,才能整理。今天的工具是括号,也就是分组。

5-1. 用括号打包:给分组加量词(5 分钟)

第 3 课说过,量词只作用于紧挨在前面的那一块。用括号 ( ) 一包,多个字符就成了一块。

re.fullmatch(r"ha+", "hahaha")      # None
re.fullmatch(r"(ha)+", "hahaha")    # <re.Match object; span=(0, 6), match='hahaha'>

像 IP 地址这样“数字加句点”重复三次的形状,也是给分组加上量词来写。

re.search(r"(\d{1,3}\.){3}\d{1,3}", "ip=203.0.113.66")
# <re.Match object; span=(3, 15), match='203.0.113.66'>

(\d{1,3}\.){3} 是“1~3 位数字加一个句点”重复三次。最后一个数字后面没有句点,所以单独再接一个 \d{1,3}。

5-2. 选择:| 不是这个就是那个(8 分钟)

| 表示“左边或右边”。可 | 在正则里的优先级最低。不用括号拦住的话,它会把整个模式一分为二。

re.fullmatch(r"ship=done|wait", "wait")          # <re.Match object; span=(0, 4), match='wait'>
re.fullmatch(r"ship=(done|wait)", "wait")        # None
re.fullmatch(r"ship=(done|wait)", "ship=wait")   # <re.Match object; span=(0, 9), match='ship=wait'>

ship=done|wait 的意思是“ship=done 或 wait”。想要“ship= 后面跟 done 或 wait”,就只用括号包住要选择的部分。

和锚点混用时尤其要小心。^a|b$ 不是“只有 a 或 b 的字符串”,而是“^a 或 b$”。

re.findall(r"^a|b$", "a.b")         # ['a', 'b']
re.search(r"^ok|no$", "say no")     # <re.Match object; span=(4, 6), match='no'>

say no 并不以 ok 开头,却匹配上了。按本来的意思,应该写成 ^(ok|no)$。

各个选项是从左往右依次尝试的,用的是先成功的那个,而不是挑最长的那个。

re.search(r"10|100", "100")   # <re.Match object; span=(0, 2), match='10'>
re.search(r"100|10", "100")   # <re.Match object; span=(0, 3), match='100'>
  • 把长的选项放在前面。
  • 单个字符之间的选择 -|\.|/ 和字符类 [-./] 是一样的。这种时候字符类更短也更快。

证据分析室

re.search(r"", text)

返回值

ship=done|wait 是“ship=done”或“wait”。只用括号包住要选择的部分试试。

5-3. 按分组编号取出(8 分钟)

括号除了打包,还有第二个作用:它会把括号里匹配到的文字单独记下来。这叫捕获分组,按左括号出现的顺序编号为 1、2、3……

假设上面的订单记录存放在字符串 orders 里。

m = re.search(r"(\d{4})-(\d\d)-(\d\d)", orders)
print(m.group(0))               # 2026-10-20
print(m.group(1))               # 2026
print(m.group(2), m.group(3))   # 10 20
print(m.groups())               # ('2026', '10', '20')
print(m.group(1, 2))            # ('2026', '10')
print(m[1], m[0])               # 2026 2026-10-20
  • group(0)(或者直接 group())是整个匹配。从 1 号开始才是分组。
  • groups() 把 1 号到最后一个分组打包成元组返回。
  • 像 group(1, 2) 这样传入多个编号,返回的也是元组。
  • m[1] 是 m.group(1) 的简写。

括号嵌套时规则也一样:按左括号 ( 出现的顺序来数编号。

((\d{4})-(\d\d))-(\d\d)
12       3       4
m = re.search(r"((\d{4})-(\d\d))-(\d\d)", "2026-10-20")
print(m.groups())     # ('2026-10', '2026', '10', '20')

证据分析室

re.search(r"", text)

返回值

用括号分别包住年、月、日。每个分组都会染上颜色,下面一栏会出现 m.group(1)~(3)。

5-4. 不捕获的括号:(?:...)(5 分钟)

很多时候只想打包,并不需要记住。比如为了限定 | 的范围,或者为了加量词而用括号。这时就用 (?:...)。它会打包,但不领编号。

m = re.search(r"(?:\d{4})-(\d\d)", "2026-10")
print(m.group(1))                           # 10
print(re.fullmatch(r"(?:ab)+", "abab").groups())   # ()

第一个括号不领编号,所以 (\d\d) 成了 1 号。只捕获需要的部分,编号就简单,还能躲开下一节 findall 的陷阱。

5-5. findall 与分组(8 分钟)

模式里有捕获分组时,findall 的行为会变。

捕获分组数findall 返回的是
0 个整个匹配的字符串列表
1 个该分组的字符串列表
2 个以上各分组组成的元组列表
re.findall(r"\d{4}[-./]\d\d[-./]\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
re.findall(r"(\d{4})[-./]\d\d[-./]\d\d", orders)
# ['2026', '2026', '2026', '2026']
re.findall(r"(\d{4})[-./](\d\d)[-./](\d\d)", orders)
# [('2026', '10', '20'), ('2026', '10', '20'), ('2026', '10', '19'), ('2026', '10', '20')]

需要年、月、日的时候,元组列表很方便。问题在于,只是为了打包而用的括号也会变成分组。

re.findall(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)
# [('-', '-'), ('.', '.'), ('/', '/'), ('-', '/')]

想要的是日期,回来的却只有分隔符。需要整个匹配时,就把括号改成 (?:...),或者用 finditer 拿到 Match 后调用 group()。

re.findall(r"\d{4}(?:-|\.|/)\d\d(?:-|\.|/)\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
[m.group() for m in re.finditer(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)]
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']

多特的提醒

findall 的结果短得奇怪、或者冒出元组的时候,先数数模式里的括号吧。只是为了打包而写的括号,常常偷偷变成了分组。第 3 课用 findall 跑 (ha)+ 时只出来 'ha',也是这个原因哦。

证据分析室

re.search(r"", text)

返回值

findall 的结果是分隔符组成的元组。把两个括号改成 (?:,或者选 finditer,让完整的日期显示出来。

5-6. 没有匹配的分组、重复的分组(6 分钟)

分组没有参与匹配时,那个位置就是 None。比如带 ? 的分组匹配了 0 次,或者 | 选中了另一边。

m = re.fullmatch(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10")
print(m.groups())     # ('2026', '10', None, None)
print(m.group(3))     # None
print(re.search(r"(a)|(b)", "b").groups())   # (None, 'b')

给分组加上量词、让它匹配多次时,分组里只留下最后一次。

m = re.fullmatch(r"(\d)+", "2026")
print(m.group(0), m.group(1))   # 2026 6

re.findall(r"(\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['113.', '100.']
re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['203.0.113.66', '198.51.100.7']

(\d)+ 匹配了四次,分组 1 里却只剩下最后的 6。IP 地址也一样,分组里只剩第三次重复的 113.,而 findall 返回的正是这个分组。把 5-1 的 IP 模式用在 findall 上时,正确答案是 (?:...)。

破案(10 分钟)

三种分隔符 -、.、/ 用 [-./] 接住,没有分隔符的 20261019 用 ? 接住。年、月、日各自捕获。

下面的代码按韩国写法输出日期:년 是“年”,월 是“月”,일 是“日”。

DATE = r"(\d{4})[-./]?(\d\d)[-./]?(\d\d)"

for m in re.finditer(DATE, orders):
    y, mo, d = m.groups()
    print(f"{y}년 {int(mo)}월 {int(d)}일")
# 2026년 10월 20일
# 2026년 10월 20일
# 2026년 10월 19일
# 2026년 10월 19일
# 2026년 10월 20일

想把订单号也一起取出来,就再加一个分组。分组有四个,所以 findall 返回四格的元组。

re.findall(r"#(\d+) " + DATE, orders)
# [('1001', '2026', '10', '20'), ('1002', '2026', '10', '20'), ('1003', '2026', '10', '19'), ('1004', '2026', '10', '19'), ('1005', '2026', '10', '20')]

证据分析室

re.search(r"", text)

返回值

这个日期模式只认连字符。把分隔符的位置改成 [-./]?,让四种样子都能通过。

结案报告 — 已把 5 笔订单的日期拆成年、月、日整理完毕。但 #1005 录入时混用了分隔符,需要员工确认。

郑老板接过整理好的表格,指着最后一行:“#1005 是 2026-10/20 啊。这个也直接通过了?”

re.fullmatch(DATE, "2026-10/20")
# <re.Match object; span=(0, 10), match='2026-10/20'>

没错。两个 [-./]? 互相不认识。现在的模式没有办法表达“前面用了 -,后面也得用 -”。所长在笔记本上写下一行:“有办法在后面再叫出前面抓到的东西。第 10 课再说。”今天就先用人眼确认这一条,到此为止。

练习

  1. 猜猜 re.findall(r"ship=(done|wait)", orders) 的结果。改成 (?:done|wait) 后会有什么不同?
  2. re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 发货") 的 groups() 是什么?
  3. 写一个模式,用分组从时刻 03:11:02 里取出时、分、秒,再看看 m.group(1, 3) 的结果。

所长的笔记本

  • ( ) 把东西打包成一块,并记住匹配到的文字。可以像 (ab)+ 这样加量词。

  • | 的优先级最低。^a|b$ 是“^a 或 b$”。选项从左往右尝试。

  • 分组编号按左括号的顺序。group(0) 是整体,groups() 是元组,m[1] 就是 group(1)。

  • (?:...) 只打包,不领编号。

  • findall:0 个分组返回整体,1 个返回该分组,2 个以上返回元组。要整体就用 (?:...) 或 finditer。

  • 没有匹配的分组是 None,像 (\d)+ 这样重复的分组只留下最后一次。

← 上一课

第4课 · 虚假注册:锚点与校验

下一课 →

第6课 · 凌晨 3 点的访问:命名分组与 Match 对象

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex