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”分别取出来,才能整理。今天的工具是括号,也就是分组。
第 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}。
| 表示“左边或右边”。可 | 在正则里的优先级最低。不用括号拦住的话,它会把整个模式一分为二。
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)
返回值
括号除了打包,还有第二个作用:它会把括号里匹配到的文字单独记下来。这叫捕获分组,按左括号出现的顺序编号为 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-20group(0)(或者直接 group())是整个匹配。从 1 号开始才是分组。groups() 把 1 号到最后一个分组打包成元组返回。group(1, 2) 这样传入多个编号,返回的也是元组。m[1] 是 m.group(1) 的简写。括号嵌套时规则也一样:按左括号 ( 出现的顺序来数编号。
((\d{4})-(\d\d))-(\d\d)
12 3 4m = 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 = 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 的陷阱。
模式里有捕获分组时,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)
返回值
分组没有参与匹配时,那个位置就是 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 上时,正确答案是 (?:...)。
三种分隔符 -、.、/ 用 [-./] 接住,没有分隔符的 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 课再说。”今天就先用人眼确认这一条,到此为止。
re.findall(r"ship=(done|wait)", orders) 的结果。改成 (?:done|wait) 后会有什么不同?re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 发货") 的 groups() 是什么?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)+ 这样重复的分组只留下最后一次。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。