CASE-002
그렙 앤 코 데이터 탐정 사무소
의뢰서
의뢰인
관할 경찰서 강 반장
증거물
목격자 진술 한 장과 지하 주차장 출입 기록. 목격자는 번호판을 일부만 봤다.
해결 조건
주차 기록에서 번호판을 모두 뽑고, 목격자 진술에 맞는 차량을 하나로 좁힌다.
저녁 무렵, 강 반장이 젖은 우산을 털며 사무소 문을 밀고 들어옵니다. "어젯밤 22시 45분쯤, 지하 주차장 출구에서 뺑소니야. 목격자가 번호판을 다 못 봤대." 소장은 키보드에서 손을 떼지 않고 대답합니다. "다 못 봤으면, 본 만큼만 패턴으로 적으면 돼. 증거는 패턴을 남겨."
목격자 진술은 이렇습니다.
- 앞 두 자리는 12 아니면 17이었어요.
- 가운데 한글은 가, 거, 고 중 하나였어요.
- 마지막 숫자는 7이었어요.
- 차는 출구(OUT) 쪽으로 빠져나갔어요.주차장 출입 기록입니다. 카메라가 번호판을 가끔 잘못 읽습니다.
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]에 대문자는 들어가지 않습니다.한글 완성형 글자는 유니코드에서 가(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은 잡히지 않습니다. 위치 규칙이 헷갈리면 그냥 백슬래시를 붙이는 편이 읽기 쉽습니다.
파이썬의 str 패턴은 기본이 유니코드입니다. 그래서 줄임 표기가 생각보다 넓습니다. \w가 한글을 잡는 것은 이미 봤습니다. \d는 아라비아-인도 숫자 ٣(U+0663, 숫자 3)도 숫자로 봅니다. \s는 한국어 문서에 자주 섞이는 전각 공백(U+3000)도 잡습니다.
re.findall(r"\d", "3٣") # ['3', '٣']
print(int("٣")) # 3
re.findall(r"\s", "a\u3000b") # ['\u3000']\d 대신 [0-9]를 씁니다.re.ASCII 플래그는 11교시에 배웁니다.마지막으로, 대괄호 안에서 [[, --, &&, ~~, ||처럼 같은 기호를 겹쳐 쓰면 FutureWarning이 납니다. 파이썬이 나중에 클래스끼리의 집합 연산(합집합, 교집합 등)에 쓰려고 남겨 둔 자리이기 때문입니다.
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 는 유니코드 전체. 0~9만 원하면 [0-9].
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.