CASE-002
Grep & Co. 数据侦探事务所
委托书
委托人
辖区警察局 姜队长
证物
一份目击者证词和地下停车场的进出记录。目击者只看到了车牌的一部分。
破案条件
从停车记录里提取出所有车牌,再把范围缩小到与目击证词相符的那一辆车。
傍晚,姜队长抖着湿漉漉的雨伞推门进来。“昨晚 22 点 45 分左右,地下停车场出口发生了肇事逃逸。目击者没看全车牌。”所长的手没离开键盘:“没看全,就把看到的那部分写成模式。证据会留下模式。”
目击者证词如下(停车场在首尔,证词原文是韩语,所以保留原样):
- 앞 두 자리는 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证词的每一句,都是“这个位置上是这几个之一”的条件。今天要学的字符类,正是用来写这种条件的语法。
方括号 [...] 表示里面写的字符中的任意一个。目击者说“가、거、고 其中之一”,那这个位置就写成 [가거고]。
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[가거고] 不是三个字符,而是一个字符。[가거고] 和 [고거가] 是同一个字符类。. 是“任意一个字符”,那方括号就是“挑出来的字符中的一个”。证据分析室
re.search(r"", text)
返回值
十个数字全写成 [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] 是一个小写字母或数字。[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] 这样写两个范围就好。
在方括号里的最前面写 ^,意思就反过来了。[^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)
返回值
常用的字符类可以用反斜杠加一个英文字母来简写。写成大写,意思就反过来。
| 写法 | 含义 | 大致等同的字符类 |
|---|---|---|
\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 这样的简写]:字符类的结束。不过放在最前面时就是字符本身^:放在最前面时表示取反-:范围。不过放在最前面或最后面时就是字符本身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 不会被抓到。位置规则要是记不清,干脆加上反斜杠,读起来反而更清楚。
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] 代替 \d。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 课还会再整理一次。
假设停车记录存放在字符串 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。同样的东西写了四遍。而且现在的车牌,前面也有三位数的。”下一课就来学怎样把这个长模式缩短。
B1、B2 这样的出入口名称。B[12] 和 B\d 有什么不同?[^\d\s] 会找到什么?先在上面的摄像头记录 12가3457 I2가3457 12가34S7 1Z거3407 上猜一猜,再运行看看。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]。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。