R

ReflexKR

Reflex 官方文档

看得见的正则表达式

·

课程概览

1

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

2

肇事逃逸车辆:字符类

3

撕破的电话簿:量词

4

虚假注册:锚点与校验

5

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

6

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

7

见报前夜:替换与分割

8

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

9

保险箱的规则:前后查找

10

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

11

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

12

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

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

时间 0:50–1:40

肇事逃逸车辆:字符类

CASE-002

Grep & Co. 数据侦探事务所

委托书

肇事逃逸车辆

委托人

辖区警察局 姜队长

证物

一份目击者证词和地下停车场的进出记录。目击者只看到了车牌的一部分。

破案条件

从停车记录里提取出所有车牌,再把范围缩小到与目击证词相符的那一辆车。

傍晚,姜队长抖着湿漉漉的雨伞推门进来。“昨晚 22 点 45 分左右,地下停车场出口发生了肇事逃逸。目击者没看全车牌。”所长的手没离开键盘:“没看全,就把看到的那部分写成模式。证据会留下模式。”

目击者证词如下(停车场在首尔,证词原文是韩语,所以保留原样):

- 앞 두 자리는 12 아니면 17이었어요.
- 가운데 한글은 가, 거, 고 중 하나였어요.
- 마지막 숫자는 7이었어요.
- 차는 출구(OUT) 쪽으로 빠져나갔어요.

翻译过来就是:

  • 前两位不是 12 就是 17。
  • 中间的韩文字是 가、거、고 其中之一。
  • 最后一位数字是 7。
  • 车是从出口(OUT)开走的。

韩国车牌的格式是“数字 + 一个韩文字 + 数字”,比如 12가3457。下面是停车场的进出记录。摄像头偶尔会把车牌读错。

2026-10-19 22:41 IN  B1 12가3457
2026-10-19 22:43 IN  B1 34나1207
2026-10-19 22:47 OUT B2 17거3407
2026-10-19 22:52 IN  B1 12-3457
2026-10-19 22:58 OUT B2 56다9081
2026-10-19 23:05 IN  B1 12a3457

证词的每一句,都是“这个位置上是这几个之一”的条件。今天要学的字符类,正是用来写这种条件的语法。

2-1. 方括号:其中的一个字符(5 分钟)

方括号 [...] 表示里面写的字符中的任意一个。目击者说“가、거、고 其中之一”,那这个位置就写成 [가거고]。

import re

re.findall("12[가거고]3457", "12가3457 12거3457 12나3457")   # ['12가3457', '12거3457']
re.findall(r"[ab]c", "ac bc cc")                            # ['ac', 'bc']
re.search(r"[abc]", "xyz")                                  # None
  • 方括号里不管写几个字符,都只占一个位置。[가거고] 不是三个字符,而是一个字符。
  • 顺序无所谓。[가거고] 和 [고거가] 是同一个字符类。
  • 如果说第 1 课的 . 是“任意一个字符”,那方括号就是“挑出来的字符中的一个”。

证据分析室

re.search(r"", text)

返回值

把中间的字改成 [가거고]。가、거、고 要能通过,나 要被筛掉。

2-2. 范围:[0-9]、[a-z]、[가-힣](8 分钟)

十个数字全写成 [0123456789] 太麻烦了。在方括号里的两个字符之间放一个 -,就构成了范围。[0-9] 是 0 到 9,[a-z] 是全部英文小写字母。

re.findall(r"[0-9]", "B1")            # ['1']
re.findall(r"[a-z0-9]", "B1 b2")      # ['1', 'b', '2']
re.findall(r"[a-zA-Z]", "a1Z")        # ['a', 'Z']
  • 多个范围可以连着写。[a-z0-9] 是一个小写字母或数字。
  • 范围的依据是字符的码位(Unicode 编号)顺序。所以 [a-z] 里不包含大写字母。

韩文的完整音节字在 Unicode 里从 가(U+AC00)到 힣(U+D7A3),共 11,172 个字连续排列。所以一个 [가-힣] 就能抓住所有完整的韩文音节字。

print(hex(ord("가")), hex(ord("힣")))   # 0xac00 0xd7a3
re.findall(r"[가-힣]", "ㄱ ㅏ 가")       # ['가']

ㄱ、ㅏ 这种单独的字母(字母表里的辅音、元音)在另一个区段,不在 [가-힣] 里。车牌中间的字总是完整的音节字,所以这个范围就够用了。

范围要是写反了,在构建模式的那一刻就会报错。

re.compile("[a-Z]")   # re.PatternError: bad character range a-Z at position 1

证据分析室

re.search(r"", text)

返回值

把中间的“가”改成 [가-힣],找出全部四个车牌。摄像头读错的两行要被筛掉。

多特的提醒

范围只能从码位小的写到码位大的哦。想一次抓住大小写而写成 [a-Z],就会报错。a 是 97 号,Z 是 90 号,顺序反了嘛。大小写都要的话,像 [a-zA-Z] 这样写两个范围就好。

2-3. 取反:[^...] 除了这些(7 分钟)

在方括号里的最前面写 ^,意思就反过来了。[^0-9] 是一个不是数字的字符。

re.findall(r"[^0-9]", "12가34")     # ['가']
re.findall(r"[0-9^]", "2^3")        # ['2', '^', '3']
re.findall(r"[^0-9]", "1\n2")       # ['\n']
  • ^ 只有放在最前面才表示取反。像 [0-9^] 这样放在后面,它就只是 ^ 这个字符。
  • [^0-9] 照样要占一个字符。它不是“没有数字的位置”,而是“一个不是数字的字符”。
  • 换行符也不是数字,所以会被 [^0-9] 抓到。

停车场的摄像头偶尔会把数字错读成英文字母:把 1 读成 I,把 5 读成 S,把 2 读成 Z。找出既不是数字、也不是韩文、也不是空格的字符,读错的位置就暴露出来了。

多特的提醒

我翻不过窗户(换行符),可 [^0-9] 翻得过去哦。因为“不是数字的字符”里也包括换行符。在多行记录里用 [^...] 的时候,一定要确认它会不会连换行符一起抓走。

证据分析室

re.search(r"", text)

返回值

[^0-9] 连韩文和空格都会抓到。在方括号里再加上 가-힣 和一个空格,只留下读错的字符。

2-4. 简写:\d \w \s(10 分钟)

常用的字符类可以用反斜杠加一个英文字母来简写。写成大写,意思就反过来。

写法含义大致等同的字符类
\d一个数字[0-9]
\D一个非数字字符[^0-9]
\w一个字母、数字或下划线(包括韩文、汉字)比 [a-zA-Z0-9_] 宽
\W一个不属于 \w 的字符
\s一个空白字符(空格、制表符、换行符等)比 [ \t\n\r\f\v] 宽
\S一个非空白字符
re.findall(r"\d", "B1 12가")       # ['1', '1', '2']
re.findall(r"\D", "12가-3")        # ['가', '-']
re.findall(r"\w", "a_1 가!")       # ['a', '_', '1', '가']
re.findall(r"\W", "a_1 가!")       # [' ', '!']
re.findall(r"\s", "a b\tc\nd")     # [' ', '\t', '\n']
re.findall(r"\S", "a b\tc")        # ['a', 'b', 'c']

简写可以和其他字符一起放进方括号里。[\d,.] 是“数字、逗号或句点”中的一个字符。

re.findall(r"\d[\d,.]\d", "1,2 3.4 5-6 789")   # ['1,2', '3.4', '789']

现在用简写来写车牌。中间写成 \D(非数字)看起来很省事,可摄像头读错的 12-3457 和 12a3457 也会通过。中间应该是“一个韩文字”,而不是“任意一个非数字字符”。

证据分析室

re.search(r"", text)

返回值

\D 连连字符和英文字母都会放行。把中间的 \D 改成 [가-힣],只留下真正的车牌。

2-5. 方括号里规则会变(5 分钟)

在方括号里,大部分元字符都失去了力量。[.*+?] 只是句点、星号、加号、问号中的一个字符而已,不用加反斜杠。

方括号里仍然特殊的字符只剩四个。

  • \:转义,以及 \d 这样的简写
  • ]:字符类的结束。不过放在最前面时就是字符本身
  • ^:放在最前面时表示取反
  • -:范围。不过放在最前面或最后面时就是字符本身
re.findall(r"[.*+?]", "a.b*c+d?")    # ['.', '*', '+', '?']
re.findall(r"[]a]", "a]b")           # ['a', ']']
re.findall(r"[-+]", "3-2+1")         # ['-', '+']
re.findall(r"[a\-z]", "a-mz")        # ['a', '-', 'z']
re.findall(r"[\]\\]", r"a]b\c")      # [']', '\\']

[a\-z] 不是范围,而是 a、-、z 三个字符。所以 m 不会被抓到。位置规则要是记不清,干脆加上反斜杠,读起来反而更清楚。

2-6. Unicode,以及要避开的叠写符号(5 分钟)

Python 的 str 模式默认就是 Unicode。所以简写的范围比你想的要宽。\w 能抓到韩文,前面已经见过了。\d 连阿拉伯-印度数字 ٣(U+0663,数字 3)也当作数字。\s 也能抓到全角空格(U+3000),这在中文、韩文文档里很常见。

re.findall(r"\d", "3٣")          # ['3', '٣']
print(int("٣"))                  # 3
re.findall(r"\s", "a\u3000b")    # ['\u3000']
  • 只想要 0~9 的话,就用 [0-9] 代替 \d。
  • 把简写收窄到 ASCII 范围的 re.ASCII 标志,第 11 课再学。

最后,在方括号里像 [[、--、&&、~~、|| 这样把同一个符号叠着写,会出现 FutureWarning。因为 Python 把这些位置预留了下来,打算将来用于字符类之间的集合运算(并集、交集等)。

import warnings
warnings.simplefilter("always")

re.compile(r"[[a]")      # FutureWarning: Possible nested set at position 1
re.compile(r"[a&&b]")    # FutureWarning: Possible set intersection at position 2

现在还能用,但将来含义可能会变。真的需要这些字符时,就像 [\[a]、[a&\&b] 这样转义其中一个。第 12 课还会再整理一次。

破案(10 分钟)

假设停车记录存放在字符串 parking 里。先把真正的车牌全部提取出来。摄像头读错的两行会被排除。

plates = re.findall(r"\d\d[가-힣]\d\d\d\d", parking)
print(plates)   # ['12가3457', '34나1207', '17거3407', '56다9081']

现在把目击证词一位一位地翻译成字符类:前两位是 1[27],中间是 [가거고],最后是 7。

print(re.findall(r"1[27][가거고]\d\d\d7", parking))         # ['12가3457', '17거3407']
print(re.findall(r"OUT B\d 1[27][가거고]\d\d\d7", parking))  # ['OUT B2 17거3407']

候选有两个,但 12가3457 是事故前开进来的,没有开出去的记录。事故 2 分钟后从出口开走的车,只有 17거3407 一辆。

结案报告 — 22:47 从 B2 出口驶离的 17거3407 与目击证词全部吻合。已请姜队长查询该车辆。

姜队长走后,所长用手指点了点屏幕上的模式。“\d\d[가-힣]\d\d\d\d。同样的东西写了四遍。而且现在的车牌,前面也有三位数的。”下一课就来学怎样把这个长模式缩短。

练习

  1. 写一个模式,只找出停车记录里 B1、B2 这样的出入口名称。B[12] 和 B\d 有什么不同?
  2. [^\d\s] 会找到什么?先在上面的摄像头记录 12가3457 I2가3457 12가34S7 1Z거3407 上猜一猜,再运行看看。
  3. 用简写写一个模式,找出像时刻 22:41 这样“两位:两位”的形状。

所长的笔记本

  • [abc] 是 a、b、c 中的一个字符。不管放几个字符,都只占一个位置。

  • [a-z0-9] 这样的范围按码位顺序。韩文完整音节是 [가-힣]。[a-Z] 会报错。

  • [^...] 只有 ^ 在最前面时才是取反。换行符也会被抓到。

  • \d \w \s,以及与之相反的 \D \W \S。可以像 [\d,.] 这样混进字符类里。

  • 字符类里 . * + ? 都是字符本身。特殊的只有 \ ] ^ - 这四个。

  • str 模式的 \d \w \s 覆盖整个 Unicode。只要 0~9 就写 [0-9]。

← 上一课

第1课 · 消失的支付记录:按原样查找文字

下一课 →

第3课 · 撕破的电话簿:量词

订阅新课时和新课程通知

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

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

问答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex