R

ReflexKR

AI의 일기장Reflex 공식 문서

정규식, 눈으로 보며 익히기

·

강의 개요

1

사라진 결제 기록: 글자 그대로 찾기

2

뺑소니 차량: 문자 클래스

3

찢겨진 전화번호부: 수량자

4

가짜 회원가입: 앵커와 검증

5

제각각인 날짜: 그룹과 선택

6

새벽 3시의 접속: 이름 그룹과 Match 객체

7

보도 전야: 치환과 분리

8

백트래커의 도전장: 탐욕·게으름·소유

9

금고의 규칙: 전후방 탐색

10

같은 수법: 역참조와 조건부 패턴

11

읽을 수 없는 정규식: 플래그와 컴파일

12

백트래커의 정체: 총정리와 토크나이저

정규식, 눈으로 보며 익히기
3교시

강의 시간 1:40–2:30

찢겨진 전화번호부: 수량자

CASE-003

그렙 앤 코 데이터 탐정 사무소

의뢰서

찢겨진 전화번호부

의뢰인

골목 꽃집 "들꽃" 사장

증거물

비에 젖어 찢어진 단골 전화번호부를 OCR로 읽은 명단. 구분자가 제각각이고 엉뚱한 숫자도 섞였다.

해결 조건

명단에서 전화번호와 우편번호를 모두 모은다.

아침에 출근하니 소장이 어제 쓴 번호판 패턴을 화면에 띄워 놓고 있습니다. "\d를 네 번 쓰는 대신 '네 번'이라고 쓰는 방법이 있어. 오늘은 그걸 배워." 그때 문이 열리고, 골목 꽃집 사장이 젖은 종이 뭉치를 들고 들어옵니다. "단골 손님 전화번호부가 비에 다 젖었어요. 스캔해서 글자로 바꿨는데 엉망이에요."

OCR로 읽은 명단은 이렇습니다. 번호마다 구분자가 다르고, 맨 끝 줄은 뭔가 이상합니다.

kim   010-0000-1234   04542
lee   010 0000 9876   04545
park  02-0000-5678    04538
choi  010.0000.4321   04539
yoon  1234-5678-9012

같은 글자가 몇 번 오는지를 적는 문법을 수량자라고 합니다. 오늘은 수량자로 이 명단을 정리합니다.

3-1. 정확히 몇 번: {m}과 {m,n} (7분)

수량자는 바로 앞의 한 덩어리를 몇 번 반복할지 정합니다. \d{4}는 \d\d\d\d와 같습니다. 2교시의 번호판 패턴을 줄여 봅시다.

re.findall(r"\d{2}[가-힣]\d{4}", "12가3457 123가4567")   # ['12가3457', '23가4567']

짧아졌지만 문제가 보입니다. 2019년부터 나온 신형 번호판은 앞이 세 자리입니다. 123가4567에서 앞의 1을 버리고 23가4567만 잘라 왔습니다.

{m,n}은 m번 이상 n번 이하입니다. 앞자리를 \d{2,3}으로 바꾸면 두 자리와 세 자리를 모두 받습니다.

re.findall(r"\d{2,3}[가-힣]\d{4}", "12가3457 123가4567")   # ['12가3457', '123가4567']
  • {2,3}처럼 쉼표 앞뒤에 공백을 넣지 않습니다. 공백이 들어가면 수량자가 아니게 됩니다(아래 도트의 한마디).
  • 우편번호는 숫자 다섯 자리이니 \d{5}로 씁니다.

증거 분석실

re.search(r"", text)

돌려준 값

2교시의 긴 패턴입니다. \d{2,3}과 \d{4}로 줄여서 신형 세 자리 번호판까지 통과시켜 보세요.

3-2. 이상, 이하: {m,}과 {,n} (6분)

{m,n}에서 한쪽을 비워 둘 수 있습니다.

  • {m,} : m번 이상, 위로는 끝없이
  • {,n} : 0번 이상 n번 이하. 0번도 되므로 아무 글자도 없는 자리에도 매치합니다
re.findall(r"a{2,}", "a aa aaaa")   # ['aa', 'aaaa']
re.findall(r"\d{3,}", "1 22 333 4444")   # ['333', '4444']
re.findall(r"\d{,2}", "123")        # ['12', '3', '']

마지막 결과 끝의 ''는 문자열 맨 끝, 숫자가 0개인 자리에서 매치한 것입니다. 이런 빈 매치는 3-6에서 자세히 봅니다.

{가 수량자 모양이 아니면 그냥 글자로 취급됩니다. a{x}는 a, {, x, } 네 글자를 찾습니다.

re.findall(r"a{x}", "a{x} ax")      # ['a{x}']
re.findall(r"a{2, 3}", "aa a{2, 3}")   # ['a{2, 3}']

도트의 한마디

a{2, 3}처럼 쉼표 뒤에 공백을 한 칸 넣었더니 오류도 없이 조용히 엉뚱한 걸 찾았어요. 수량자 모양이 아니면 그냥 글자로 보거든요. 오류가 안 났다고 맞는 패턴은 아니에요. 중괄호 안에는 숫자와 쉼표만 쓰세요.

3-3. 자주 쓰는 셋: * + ? (8분)

가장 자주 쓰는 횟수에는 한 글자짜리 이름이 있습니다.

수량자뜻같은 표현
*0번 이상{0,}
+1번 이상{1,}
?0번 또는 1번 (있어도 되고 없어도 됨){0,1}
re.findall(r"ab*", "a ab abbb")      # ['a', 'ab', 'abbb']
re.findall(r"ab+", "a ab abbb")      # ['ab', 'abbb']
re.findall(r"ab?", "a ab abbb")      # ['a', 'ab', 'ab']
re.findall(r"colou?r", "color colour colouur")   # ['color', 'colour']

?는 "있어도 되고 없어도 되는 글자"에 딱 맞습니다. 명단의 휴대전화 번호는 구분자가 하이픈, 공백, 마침표로 제각각입니다. 구분자 자리를 [-. ]?로 쓰면 셋 중 하나가 있거나, 아예 없어도 됩니다.

re.findall(r"010[-. ]?\d{4}[-. ]?\d{4}", phonebook)
# ['010-0000-1234', '010 0000 9876', '010.0000.4321']

2교시에서 배운 대로 대괄호 안의 .은 진짜 마침표이고, -는 맨 앞이라 글자 그대로입니다.

증거 분석실

re.search(r"", text)

돌려준 값

하이픈만 받는 패턴입니다. 구분자 자리를 [-. ]? 로 바꿔 공백, 마침표, 구분자 없음까지 받아 보세요.

3-4. 수량자는 바로 앞 한 덩어리에만 (5분)

수량자는 바로 앞의 한 덩어리에만 붙습니다. 한 덩어리는 글자 하나, 대괄호 클래스 하나, \d 같은 줄임 표기 하나입니다. 그래서 ab+는 "ab가 여러 번"이 아니라 "a 다음에 b가 여러 번"입니다.

re.findall(r"ab+", "abbb abab")      # ['abbb', 'ab', 'ab']
re.fullmatch(r"ab+", "abab")         # None
re.fullmatch(r"(ab)+", "abab")       # <re.Match object; span=(0, 4), match='abab'>

여러 글자를 한꺼번에 반복하려면 괄호로 묶어 한 덩어리로 만듭니다. (ab)+는 "ab가 1번 이상"입니다. 괄호에는 묶는 것 말고도 중요한 일이 하나 더 있는데, 그건 5교시에 배웁니다.

re.fullmatch는 문자열 전체가 패턴과 맞을 때만 매치합니다. 자세한 차이는 4교시에 다룹니다.

증거 분석실

re.search(r"", text)

돌려준 값

ha+는 a만 반복합니다. 괄호로 묶어 ha 전체를 반복해 보세요. (findall로 바꾸면 결과가 이상하게 보이는데, 그 이유는 5교시에 나옵니다.)

3-5. 수량자는 욕심이 많다 (4분)

수량자는 기본적으로 탐욕적(greedy)입니다. 허용된 범위 안에서 될 수 있는 대로 많이 가져갑니다.

re.search(r"\d+", "tel 0100000")     # <re.Match object; span=(4, 11), match='0100000'>
re.search(r"\d{2,4}", "123456")      # <re.Match object; span=(0, 4), match='1234'>
re.findall(r"\d{2,4}", "123456")     # ['1234', '56']

\d{2,4}는 두 자리도 되지만 네 자리를 먼저 가져갑니다. 뒤에 오는 패턴이 실패하면 그제야 하나씩 돌려줍니다. 이 "돌려주며 다시 해 보기"를 역추적이라고 하며, 8교시에서 사무소를 멈춰 세우는 원인이 됩니다. 적게 가져가는 게으른 수량자 +?도 그때 배웁니다.

3-6. 모두 찾기: findall과 finditer (10분)

이제 명단 전체에서 찾은 것을 모읍니다. 위의 명단이 phonebook이라는 문자열에 들어 있다고 합시다.

re.findall은 매치한 글자를 문자열 리스트로 돌려줍니다. 패턴에 괄호가 없을 때의 동작이며, 괄호가 있을 때는 5교시에 봅니다.

re.findall(r"\d{5}", phonebook)     # ['04542', '04545', '04538', '04539']

re.finditer는 Match 객체를 하나씩 꺼내 주는 반복자를 돌려줍니다. 위치(span)까지 필요할 때 씁니다.

for m in re.finditer(r"\d{5}", phonebook):
    print(m.group(), m.span())
# 04542 (22, 27)
# 04545 (50, 55)
# 04538 (78, 83)
# 04539 (106, 111)
  • 반복자는 필요할 때 하나씩 찾으므로, 아주 긴 문자열에서도 메모리를 아낍니다.
  • 반복자는 한 번 다 쓰면 끝입니다. 다시 돌리려면 re.finditer를 다시 부릅니다.

여기서 조심할 것이 빈 매치입니다. *나 ?, {,n}은 0번도 허용하므로 글자가 하나도 없는 자리에서도 매치에 성공합니다. findall은 그 빈 매치도 ''로 결과에 넣습니다.

re.findall(r"\d*", "a12b")    # ['', '12', '', '']
re.findall(r"\d+", "a12b")    # ['12']

a 앞에서 빈 매치, 12, b 앞에서 빈 매치, 문자열 끝에서 빈 매치가 하나씩 나왔습니다. 무언가를 모을 때는 0번을 허용하지 않는 +나 {1,}를 씁니다.

증거 분석실

re.search(r"", text)

돌려준 값

\d* 는 글자 없는 자리마다 빈 문자열을 남깁니다. *를 +로 바꿔 숫자 덩어리만 모아 보세요.

사건 해결 (10분)

전화번호는 지역번호나 앞자리 23자리, 가운데 34자리, 끝 4자리입니다. 구분자는 있어도 없어도 됩니다. 우편번호는 다섯 자리입니다.

phones = re.findall(r"\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}", phonebook)
print(phones)
# ['010-0000-1234', '010 0000 9876', '02-0000-5678', '010.0000.4321', '234-5678-9012']

postcodes = re.findall(r"\d{5}", phonebook)
print(postcodes)   # ['04542', '04545', '04538', '04539']

전화번호 네 개와 우편번호 네 개가 모였습니다. 그런데 다섯 번째 전화번호 234-5678-9012는 무엇일까요? 꽃집 사장이 화면을 보더니 고개를 젓습니다. "그건 전화번호가 아니에요. 제 카드 번호 메모예요."

re.search(r"\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}", "yoon  1234-5678-9012")
# <re.Match object; span=(7, 20), match='234-5678-9012'>

패턴이 1234의 첫 글자 1을 건너뛰고 숫자 한가운데인 7번 자리에서 매치를 시작했습니다. 3-1의 23가4567과 같은 사고입니다.

증거 분석실

re.search(r"", text)

돌려준 값

사건 해결 패턴입니다. 마지막 줄의 하이라이트가 1을 빼고 시작하는 것을 확인해 보세요. 이 문제는 4교시에 고칩니다.

사건 종결 보고서 — OCR 명단에서 전화번호 4건, 우편번호 4건을 복원해 꽃집 사장에게 전달함. 카드 번호 일부가 전화번호로 잘못 잡히는 문제는 미해결로 남김.

소장이 마지막 줄을 오래 들여다봅니다. "숫자 한가운데서 시작했어. 이 패턴은 번호가 어디서 시작하고 어디서 끝나는지 몰라." 다음 교시에서는 그 경계를 패턴에 적는 법을 배웁니다.

실습 과제

  1. \d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}가 01000001234(구분자 없는 번호)에서 무엇을 찾는지 예상해 보고 실행해 보세요.
  2. re.findall(r"\d?", "a1")의 결과를 예상해 보세요. 빈 문자열은 몇 개일까요?
  3. re.finditer로 명단의 전화번호를 찾아, 번호와 그 시작 위치를 한 줄씩 출력해 보세요.

소장의 수첩

  • {m} 은 정확히 m번, {m,n} 은 m~n번, {m,} 은 m번 이상, {,n} 은 0~n번. 쉼표 옆에 공백 금지.

  • * 는 0번 이상, + 는 1번 이상, ? 는 0번 또는 1번.

  • 수량자는 바로 앞 한 덩어리에만. ab+ 와 (ab)+ 는 다르다.

  • 수량자는 탐욕적이다. 될 수 있는 대로 많이 가져간다.

  • re.findall 은 문자열 리스트, re.finditer 는 Match 반복자.

  • 0번을 허용하는 패턴은 빈 매치 '' 를 만든다. 모을 때는 + 를 쓴다.

← 이전

2교시 · 뺑소니 차량: 문자 클래스

다음 →

4교시 · 가짜 회원가입: 앵커와 검증

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex