R

ReflexKR

Reflex 官方文档

看得见的正则表达式

·

课程概览

1

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

2

肇事逃逸车辆:字符类

3

撕破的电话簿:量词

4

虚假注册:锚点与校验

5

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

6

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

7

见报前夜:替换与分割

8

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

9

保险箱的规则:前后查找

10

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

11

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

12

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

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

时间 5:00–5:50

见报前夜:替换与分割

CASE-007

Grep & Co. 数据侦探事务所

委托书

见报前夜

委托人

追踪泄露事件的吴记者

证物

匿名爆料人发来的一份客户名单文件,看起来是郑市集的会员列表。每一行的分隔符都不一样。

破案条件

为了能登上明早的报道:遮住电话号码的中间四位,把日期改成 2026-10-20 的格式,再把每一行拆成一个个字段。

晚上 9 点,事务所的门被猛地推开。吴记者气喘吁吁地打开笔记本电脑。“有人爆料了,是郑市集的客户名单。明早报道就要发,可总不能把个人信息原样登出去吧。”

所长凑近屏幕看了看。正是第 6 课找到的那份凌晨 3 点的会员列表。“找的部分已经结束了。今天的活儿是改和切。”

id, name; phone | joined
1001,kim,010-0000-5678,20/10/2026
1002; lee; 010-0000-1234; 19/10/2026
1003 | park | 010-0000-9012 | 03/09/2026
1004, choi;010-0000-3456  |28/02/2026
# (a+)+$

最后一行有几个奇怪的字符,但吴记者被截稿时间追着,看都没多看一眼。

7-1. 找到就换:re.sub(8 分钟)

re.sub(模式, 替换字符串, 原文) 会把模式匹配到的地方全部替换,返回一个新字符串,原文保持不变。因为 Python 的字符串是不可变的值。

import re

re.sub(r"\d{4}", "****", "010-0000-5678")    # '010-****-****'

四位数字有两处,所以连后四位也被遮住了。想保留前三位和后四位、只遮中间,就先把要保留的部分用分组抓住,再在替换字符串里把它叫回来。\1 就是第 1 组抓到的文字。

re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", "010-0000-5678")   # '010-****-5678'

替换字符串里也有反斜杠,所以和模式一样用 r"..." 来写。

证据分析室

re.search(r"", text)

返回值

现在连会员编号和年份都变成了星号。请修改模式,让它只匹配电话号码,再把替换字符串改成 \1-****-\2,只遮住中间四位。点 subn 还能看到一共替换了几处。

7-2. 替换字符串里的分组:\g<1>、\g、\g<0>(10 分钟)

名单上的日期是 20/10/2026 这种日/月/年的顺序,报道里得写成 2026-10-20。把分组的顺序调换一下就行。

re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\3-\2-\1", "20/10/2026")            # '2026-10-20'
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\g<3>-\g<2>-\g<1>", "20/10/2026")   # '2026-10-20'

\3 和 \g<3> 意思相同。第 6 课学的命名分组用 \g<名字> 来叫。不用记顺序,出错的机会也就少了。

re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", "20/10/2026")   # '2026-10-20'

\g<0> 代表整个匹配。想把找到的东西原样保留、只在外面包一层时就用它。

re.sub(r"\d{3}-\d{4}-\d{4}", r"[\g<0>]", "1001,kim,010-0000-5678")   # '1001,kim,[010-0000-5678]'

如果想在组号后面紧接着写数字,就要小心了。\10 不是“第 1 组后面跟个 0”,而是第 10 组。这时要像 \g<1>0 这样,用尖括号把组号封起来。

re.sub(r"(\d+)", r"\g<1>0", "kim 5")   # 'kim 50'
re.sub(r"(\d+)", r"\10", "kim 5")      # re.PatternError: invalid group reference 10 at position 1

在替换字符串里,\n 同样会变成换行符。而像 \q 这种没有意义的字母转义则会报错。

re.sub(r",", r"\n", "a,b")    # 'a\nb'
re.sub(r",", r"\q", "a,b")    # re.PatternError: bad escape \q at position 0

re.PatternError 是从 3.13 开始使用的名字,和旧名字 re.error 是同一个异常。第 12 课会详细讲。

证据分析室

re.search(r"", text)

返回值

现在的替换字符串是日-月-年的顺序。请改成 \g<y>-\g<m>-\g<d>。也试试放进 \g<0> 或 \10 会发生什么。

7-3. 用函数来替换,以及 count 和 subn(8 分钟)

替换字符串的位置上也可以传一个函数。每出现一次匹配,re.sub 就把那个 Match 对象交给函数,再用函数返回的字符串来替换。我们来试试只留下名字的第一个字母,其余按长度打上星号。这是固定的模板做不到的事。

def hide(m):
    name = m.group(1)
    return "," + name[0] + "*" * (len(name) - 1) + ","

re.sub(r",([a-z]+),", hide, "1003,park,010-0000-9012,03/09/2026")   # '1003,p***,010-0000-9012,03/09/2026'
re.sub(r"[a-z]+", lambda m: m[0].upper(), "kim and lee")             # 'KIM AND LEE'
  • 函数接收一个 Match,必须返回字符串。返回数字的话会得到 TypeError。
  • 证据分析室里没法运行函数替换,请自己在 Python 里跑一跑。

count 决定从前往后最多替换几处。默认值 0 表示“全部”。re.subn 会把替换后的字符串和替换次数放在一个元组里返回。假设最上面那份名单存在名为 leak 的字符串里。

re.sub(r"\d{3}-\d{4}-\d{4}", "***", leak, count=1).splitlines()[1:3]
# ['1001,kim,***,20/10/2026', '1002; lee; 010-0000-1234; 19/10/2026']

re.subn(r"\d{3}-\d{4}-\d{4}", "***", leak)[1]   # 4

这样报道里就能写上准确的数字:“已遮盖 4 个电话号码”。

7-4. 切开:re.split(9 分钟)

名单每一行的分隔符都不同:逗号、分号、竖线混在一起,前后的空格也多少不一。str.split 只能接收一种分隔符,而 re.split 是按模式来切。

re.split(r"\s*[,;|]\s*", "a, b;c  |d")               # ['a', 'b', 'c', 'd']
re.split(r"\s*[,;|]\s*", "a, b;c  |d", maxsplit=2)   # ['a', 'b', 'c  |d']
  • 分隔符前后的 \s* 把空格也当作分隔符的一部分吞掉,切出来的字段就很干净。
  • maxsplit=2 只切两次,剩下的部分原样留在最后一个字段里。

如果模式里有捕获分组,切掉的分隔符也会被插进结果列表。需要记录用的是哪种分隔符时就这么写。不需要的话,就写成 (?:...)。

re.split(r"\s*([,;|])\s*", "a, b;c  |d")   # ['a', ',', 'b', ';', 'c', '|', 'd']

不占任何字符的空匹配也能用来切。\b 只匹配单词边界的位置,所以每个单词和空格之间都会被切开。两端也有边界,于是会出现空字符串。

re.split(r"\b", "a b")   # ['', 'a', ' ', 'b', '']

证据分析室

re.search(r"", text)

返回值

只按逗号切的话,分号和竖线会留在字段里。请把三种分隔符连同前后的空格都当作分隔符,但别切开电话号码里的 -。

7-5. count、maxsplit、flags 要写上名字(5 分钟)

re.sub 的第四个位置是 count。如果想加标志,随手把 re.I 写在第四个位置,它进去的身份就不是标志,而是次数。因为 re.I 的实际值是 2。

re.sub("a", "b", "aAaA", re.I)         # 'bAbA'  ← 没有忽略大小写,只替换了两次
re.sub("a", "b", "aAaA", flags=re.I)   # 'bbbb'

这个错误太常见了,所以从 Python 3.13 开始,以位置参数传入 count、maxsplit、flags 时会发出 DeprecationWarning。上面第一行就会给出 'count' is passed as positional argument 的警告。re.split 的 maxsplit、flags 也一样。请始终像 count=1、maxsplit=2、flags=re.I 这样写上名字。

多特的提醒

跟我说“坐下”,我就坐下;光喊一声数字 2,我可不知道你要我干嘛。函数也是一样哦。re.sub("a", "b", s, re.I) 它会听成“只换两次”。请叫出名字:flags=re.I。

破案(10 分钟)

把遮盖、改日期、切分依次串起来。去掉标题行和最后一行,只切客户数据的行。

masked = re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", leak)
fixed = re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", masked)

for row in fixed.splitlines()[1:-1]:
    print(re.split(r"\s*[,;|]\s*", row))
# ['1001', 'kim', '010-****-5678', '2026-10-20']
# ['1002', 'lee', '010-****-1234', '2026-10-19']
# ['1003', 'park', '010-****-9012', '2026-09-03']
# ['1004', 'choi', '010-****-3456', '2026-02-28']

吴记者接过表格正要起身,所长抬起了手。“等等。最后一行。”

print(fixed.splitlines()[-1])   # # (a+)+$

这不是名单数据,而是有人故意留下的正则表达式。要按字面找到这串字符,就得像第 1 课学的那样,把元字符一个个关掉。

print(re.escape("(a+)+$"))          # \(a\+\)\+\$
re.search(r"\(a\+\)\+\$", leak)     # <re.Match object; span=(177, 183), match='(a+)+$'>

证据分析室

re.search(r"", text)

返回值

把签名原样当作模式放进去,是找不到签名这串字符的。因为它是一个寻找连续 a 的正则,就像破案条件里的 aaa。请关掉所有元字符,找到签名字符本身。

“这是什么?”吴记者问。所长沉默了好一会儿才开口:“回溯陷阱。读到它的正则引擎会在同一条路上来回折返无数次,最后卡死。明天的报道里把它删掉。”

吴记者走后,所长从抽屉里取出一个褪了色的相框,是事务所开张头一年拍的照片。她盯着看了很久,然后把相框扣在了桌上。

结案报告 — 已遮盖泄露名单中 4 个电话号码的中间四位,并把日期统一为年-月-日,交给记者。在名单末尾发现被故意植入的正则 (a+)+$。来源不明,需另行调查。

练习

  1. 把名单里的会员编号 1001~1004 改成 #1001 这样,在前面加上 #。用 \g<0> 一行就能搞定。
  2. 猜猜 re.split(r"(\s*)([,;|])(\s*)", "a, b") 的结果。分组有三个的话,会插进几个字段?
  3. 用函数替换把 "price 1500 and 300" 里的数字全部变成两倍。别忘了函数必须返回字符串。

所长的笔记本

  • re.sub(模式, 替换字符串, 原文) 返回新字符串,原文不变。

  • 替换字符串里 \1 = \g<1>,命名分组用 \g<name>,整个匹配用 \g<0>。后面紧跟数字时写 \g<1>0(\10 是第 10 组)。

  • 替换字符串里的 \n 是换行,\q 这种不认识的转义会报错。

  • 函数替换:接收 Match、返回字符串的函数。count 限制次数,re.subn 返回 (结果, 次数)。

  • re.split 按模式切分。可用 maxsplit;有捕获分组时,分隔符也会进入结果。

  • count、maxsplit、flags 用关键字传。位置参数从 3.13 起发出 DeprecationWarning,还会出现 re.I 被当成 count=2 的事故。

← 上一课

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

下一课 →

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

订阅新课时和新课程通知

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

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

问答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex