R

ReflexKR

Reflex 官方文档

看得见的正则表达式

·

课程概览

1

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

2

肇事逃逸车辆:字符类

3

撕破的电话簿:量词

4

虚假注册:锚点与校验

5

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

6

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

7

见报前夜:替换与分割

8

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

9

保险箱的规则:前后查找

10

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

11

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

12

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

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

时间 6:40–7:30

保险箱的规则:前后查找

CASE-009

Grep & Co. 数据侦探事务所

委托书

保险箱的规则

委托人

韩星所长

证物

事务所那台老旧的保险箱、六个新密码候选、保险箱里的账本。

破案条件

用一个模式检查新密码规则(8 个字符以上,数字、小写字母、特殊字符各至少一个),并给账本金额加上千位分隔逗号。

所长在角落那台旧保险箱前蹲了下来。“回溯者提保险箱,只是想吓唬人。不过密码今天就换。这回要老老实实按规则来。”

新密码的规则有三条,顺序不限。

규칙 1. 8자 이상
규칙 2. 숫자, 소문자, 특수문자가 각각 하나 이상
규칙 3. 이 모든 걸 정규식 하나로 검사한다

후보:
vault2026!
vault2026
VAULT2026!
v!2
longpassword!
safe#2026box

所长的便条是用韩文写的:规则 1,8 个字符以上;规则 2,数字、小写字母、特殊字符各至少一个;规则 3,只用一个正则检查全部条件。下面的“후보”是“候选”。

你想起卡片上的那句话:“前提是你们能在同一个位置把它们全部确认。”今天要学的语法正是这个。

9-1. 检查的不是字符,而是位置(6 分钟)

回想一下第 4 课的 ^、$、\b。它们一个字符也不吃,只检查“这里是不是开头”“这里是不是单词边界”这样的位置。这类匹配叫作零宽(zero-width)匹配。

re.findall(r"\b", "hi yo")      # ['', '', '', '']
re.sub(r"\b", "|", "hi yo")     # '|hi| |yo|'

\b 找到的是四个空字符串,也就是四个位置。今天要学的前后查找,就是把这种“位置检查”换成你自己想要的条件的语法。

9-2. 检查后面跟着什么:(?=...)(8 分钟)

X(?=Y) 寻找“后面跟着 Y 的 X”。Y 只用来检查,不算进匹配里。这叫作向前查找(lookahead)。

re.findall(r"\d+(?=원)", "3000원짜리 5개, 합계 15000원")   # ['3000', '15000']

这句韩文的意思是“3000 元的 5 个,合计 15000 元”,원 是韩元,개 是“个”。5개 里的 5 后面没有 원,所以被排除了;找到的数字里也不含 원。想找“以元结尾的数字”却只取出数字本身时,它再合适不过。

证据分析室

re.search(r"", text)

返回值

只写 \d+ 的话,连个数 5 也会被抓到。请加上向前查找,只留下后面跟着 원 的数字。

9-3. 后面不能跟的东西:(?!...)(6 分钟)

反过来,X(?!Y) 是“后面不跟着 Y 的 X”,叫作否定向前查找。

re.findall(r"\b(?!tmp)\w+", "tmp_log report tmpfile final")   # ['report', 'final']
re.findall(r"\d{3}(?!\d)", "12345 678")                       # ['345', '678']
  • 第一行是“不以 tmp 开头的单词”。在单词边界的紧后面检查接下来是不是 tmp。
  • 第二行是“后面再没有数字的三位数”,也就是一串数字的最后三位。

9-4. 多条规则就把向前查找接起来(8 分钟)

像密码规则这种“这个要有、那个要有、另一个也要有”的条件,就把向前查找并排接起来写。

pw = r"(?=.*\d)(?=.*[a-z])(?=.*[^\w\s]).{8,}"
片段检查的内容
(?=.*\d)某处有数字
(?=.*[a-z])某处有小写字母
(?=.*[^\w\s])某处有特殊字符(不是字母、数字、下划线、空白的字符)
.{8,}真正吃下 8 个以上的字符

三个向前查找都在**同一个位置(最开头)**检查。它们不吃字符,所以一个查完位置不变,下一个条件又从开头开始。因此接在一起就成了“并且(AND)”。

cands = ["vault2026!", "vault2026", "VAULT2026!", "v!2", "longpassword!", "safe#2026box"]
[c for c in cands if re.fullmatch(pw, c)]   # ['vault2026!', 'safe#2026box']

证据分析室

re.search(r"", text)

返回值

.{8,} 只看长度。请用向前查找把数字、小写字母、特殊字符的条件加在前面。

多特的提醒

向前查找只是往前偷瞄一眼,脚可不动哦。就像我蹲在窗台上往外看,从来不跳出去。所以不管瞄多少次,我都还在原地。

9-5. 前面有过什么:(?<=...) (?<!...)(8 分钟)

把方向反过来,就是向后查找(lookbehind)。(?<=Y)X 是“前面有 Y 的 X”,(?<!Y)X 是“前面没有 Y 的 X”。

re.findall(r"(?<=\$)\d+", "$30 and 40 and $5")      # ['30', '5']
re.findall(r"(?<!\$)\b\d+", "$30 and 40 and $5")    # ['40']

向后查找有一个限制:Python 的 re 要求往回看的长度是固定的。

re.compile(r"(?<=a+)b")      # PatternError: look-behind requires fixed-width pattern
re.compile(r"(?<=ab|cd)e")   # 没问题:两个选项都是 2 个字符
re.compile(r"(?<=a|bc)d")    # PatternError: look-behind requires fixed-width pattern

就算有多个选项,只要长度都一样就行。长度不同的话,就把向后查找拆成两个,像 (?:(?<=a)|(?<=bc))d 这样分开写。

证据分析室

re.search(r"", text)

返回值

\$\d+ 会把 $ 也一起取出来。请把 \$ 挪进向后查找,让返回值变成 ['30', '5']。

9-6. 保险箱的账本:千位分隔逗号(4 分钟)

保险箱里账本上的数字像 1234567 这样挤在一起,很难读。找到该加逗号的位置,在那里插入逗号就行。既然是找位置,前后查找最合适。

re.sub(r"(?<=\d)(?=(?:\d{3})+(?!\d))", ",", "1234567 and 89 and 1000")
# '1,234,567 and 89 and 1,000'

读法是这样的:“前面是数字((?<=\d)),后面接着一个以上的三位数字组((?:\d{3})+),并且数字到那里就结束((?!\d))的位置。”这个位置不是字符,所以 sub 不会删掉任何字符,只是把逗号塞进去。

在向前查找里放一个分组,还能找出相互重叠的匹配。诀窍是每次只把位置挪一格,再用分组取出那个位置上的两个字符。

re.findall(r"(?=(\w\w))", "abc")   # ['ab', 'bc']

证据分析室

re.search(r"", text)

返回值

替换字符串是一个逗号。模式找的必须是位置而不是字符,数字才不会消失。

破案(10 分钟)

完成检查器,把候选筛一遍。

pw = re.compile(r"(?=.*\d)(?=.*[a-z])(?=.*[^\w\s]).{8,}")
for c in cands:
    print(f"{c:<14} {'통과' if pw.fullmatch(c) else '탈락'}")
# vault2026!     통과
# vault2026      탈락
# VAULT2026!     탈락
# v!2            탈락
# longpassword!  탈락
# safe#2026box   통과

통과 是“通过”,탈락 是“淘汰”。

通过的两个,所长一个也没选。“写进候选名单的那一刻,它就不是密码了。”说完,她避开所有人的视线,亲手定了一个符合规则的新密码,只把检查器放进事务所的公用工具箱。

结案报告 — 做出一个用三个向前查找检查保险箱密码规则的工具。六个候选中只有两个符合规则。密码已由所长另行更换。账本金额已加上千位分隔逗号。

练习

  1. 给规则加上“至少一个大写字母”。哪些候选能活下来?
  2. 用向前查找,从 "price: 1200won, tax: 120won" 中只取出 won 前面的数字。
  3. 猜猜 re.findall(r"(?<!\d)\d{2}(?!\d)", "1 22 333 44") 的结果。为什么 333 的一部分不会出现?

回溯者的踪迹

(a+)+$

保险箱就先放着吧。不过,我给警局的姜队长寄了三封信。你们能证明这三封出自同一个人之手吗?习惯会留在字里行间。

所长的笔记本

  • 前后查找是零宽匹配。不吃字符,只检查位置(和 ^ $ \b 同类)。

  • X(?=Y) 后面跟着 Y 的 X,X(?!Y) 后面不跟 Y 的 X。

  • (?<=Y)X 前面有 Y 的 X,(?<!Y)X 前面没有 Y 的 X。向后查找只能是固定长度。

  • 向前查找接在一起就是 AND:(?=.*\d)(?=.*[a-z])(?=.*[^\w\s]).{8,}

  • 找到位置再 sub,就能不删字符只插入:千位分隔逗号。

  • 像 (?=(\w\w)) 这样在向前查找里放分组,可以取出相互重叠的匹配。

← 上一课

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

下一课 →

第10课 · 同样的手法:反向引用与条件模式

订阅新课时和新课程通知

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

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

问答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex