R

ReflexKR

AI의 일기장Reflex 공식 문서

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

·

강의 개요

1

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

2

뺑소니 차량: 문자 클래스

3

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

4

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

5

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

6

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

7

보도 전야: 치환과 분리

8

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

9

금고의 규칙: 전후방 탐색

10

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

11

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

12

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

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

강의 시간 3:20–4:10

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

CASE-005

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

의뢰서

제각각인 날짜

의뢰인

온라인 쇼핑몰 "마켓 정" 정 사장

증거물

마켓 정 주문 기록. 아르바이트생마다 날짜를 다른 모양으로 적었다.

해결 조건

모든 주문에서 연, 월, 일을 따로 꺼내 같은 모양으로 정리한다.

가짜 회원가입 사건 다음 날, 정 사장이 다시 찾아옵니다. 이번에는 한숨부터 쉽니다. "가입은 막았어요. 그런데 주문 기록을 날짜별로 정리하려니, 아르바이트생 셋이 날짜를 다 다르게 적어 놨더라고요." 소장이 커피를 한 모금 마십니다. "찾는 법은 1부에서 배웠지. 이제 찾은 것 안에서 꺼내는 법을 배울 차례야."

주문 기록은 이렇습니다.

#1001 2026-10-20 ship=done
#1002 2026.10.20 ship=wait
#1003 2026/10/19 ship=done
#1004 20261019 ship=done
#1005 2026-10/20 ship=wait

날짜를 찾기만 해서는 부족합니다. "2026", "10", "20"을 따로따로 꺼내야 정리할 수 있습니다. 오늘의 도구는 괄호, 즉 그룹입니다.

5-1. 괄호로 묶기: 그룹에 수량자 (5분)

3교시에서 수량자는 바로 앞 한 덩어리에만 붙는다고 했습니다. 괄호 ( )로 묶으면 여러 글자가 한 덩어리가 됩니다.

re.fullmatch(r"ha+", "hahaha")      # None
re.fullmatch(r"(ha)+", "hahaha")    # <re.Match object; span=(0, 6), match='hahaha'>

IP 주소처럼 "숫자와 마침표"가 세 번 되풀이되는 모양도 그룹에 수량자를 붙여 씁니다.

re.search(r"(\d{1,3}\.){3}\d{1,3}", "ip=203.0.113.66")
# <re.Match object; span=(3, 15), match='203.0.113.66'>

(\d{1,3}\.){3}는 "숫자 1~3자리와 마침표"를 세 번입니다. 마지막 숫자 뒤에는 마침표가 없으니 \d{1,3}을 따로 붙였습니다.

5-2. 선택: | 이것 아니면 저것 (8분)

|는 "왼쪽 또는 오른쪽"입니다. 그런데 |는 정규식에서 우선순위가 가장 낮습니다. 괄호로 막지 않으면 패턴 전체를 둘로 가릅니다.

re.fullmatch(r"ship=done|wait", "wait")          # <re.Match object; span=(0, 4), match='wait'>
re.fullmatch(r"ship=(done|wait)", "wait")        # None
re.fullmatch(r"ship=(done|wait)", "ship=wait")   # <re.Match object; span=(0, 9), match='ship=wait'>

ship=done|wait는 "ship=done 또는 wait"입니다. "ship= 다음에 done 또는 wait"를 원하면 선택할 부분만 괄호로 감쌉니다.

앵커와 섞을 때 특히 조심합니다. ^a|b$는 "a 또는 b 하나만 있는 문자열"이 아니라 "^a 또는 b$"입니다.

re.findall(r"^a|b$", "a.b")         # ['a', 'b']
re.search(r"^ok|no$", "say no")     # <re.Match object; span=(4, 6), match='no'>

say no는 ok로 시작하지 않는데도 매치했습니다. 원래 뜻대로라면 ^(ok|no)$로 씁니다.

선택지는 왼쪽부터 차례로 시도하고, 먼저 성공한 것을 씁니다. 가장 긴 것을 고르지 않습니다.

re.search(r"10|100", "100")   # <re.Match object; span=(0, 2), match='10'>
re.search(r"100|10", "100")   # <re.Match object; span=(0, 3), match='100'>
  • 긴 선택지를 앞에 둡니다.
  • 한 글자끼리의 선택 -|\.|/는 클래스 [-./]와 같습니다. 이럴 때는 클래스가 짧고 빠릅니다.

증거 분석실

re.search(r"", text)

돌려준 값

ship=done|wait 는 'ship=done' 또는 'wait'입니다. 선택할 부분만 괄호로 감싸 보세요.

5-3. 그룹 번호로 꺼내기 (8분)

괄호에는 묶는 것 말고 두 번째 역할이 있습니다. 괄호 안에 매치한 글자를 따로 기억합니다. 이것을 캡처 그룹이라고 하고, 여는 괄호 순서대로 1, 2, 3 … 번호가 붙습니다.

위의 주문 기록이 orders라는 문자열에 들어 있다고 합시다.

m = re.search(r"(\d{4})-(\d\d)-(\d\d)", orders)
print(m.group(0))               # 2026-10-20
print(m.group(1))               # 2026
print(m.group(2), m.group(3))   # 10 20
print(m.groups())               # ('2026', '10', '20')
print(m.group(1, 2))            # ('2026', '10')
print(m[1], m[0])               # 2026 2026-10-20
  • group(0)(또는 그냥 group())은 매치 전체입니다. 1번부터가 그룹입니다.
  • groups()는 1번부터 끝까지를 튜플로 줍니다.
  • group(1, 2)처럼 번호를 여러 개 주면 튜플로 돌려줍니다.
  • m[1]은 m.group(1)의 줄임입니다.

괄호가 겹쳐 있어도 규칙은 같습니다. 여는 괄호 (가 나오는 순서대로 번호를 셉니다.

((\d{4})-(\d\d))-(\d\d)
12       3       4
m = re.search(r"((\d{4})-(\d\d))-(\d\d)", "2026-10-20")
print(m.groups())     # ('2026-10', '2026', '10', '20')

증거 분석실

re.search(r"", text)

돌려준 값

연, 월, 일을 각각 괄호로 감싸 보세요. 그룹마다 색이 칠해지고, 아래 칸에 m.group(1)~(3)이 나타납니다.

5-4. 잡지 않는 괄호: (?:...) (5분)

묶기만 하고 기억할 필요는 없을 때가 많습니다. |의 범위를 정하거나 수량자를 붙이려고 괄호를 쓸 때입니다. 이럴 때는 (?:...)를 씁니다. 묶기는 하지만 번호를 받지 않습니다.

m = re.search(r"(?:\d{4})-(\d\d)", "2026-10")
print(m.group(1))                           # 10
print(re.fullmatch(r"(?:ab)+", "abab").groups())   # ()

첫 괄호가 번호를 받지 않아서 (\d\d)가 1번이 됐습니다. 필요한 것만 캡처하면 번호가 단순해지고, 다음 절의 findall 함정도 피할 수 있습니다.

5-5. findall과 그룹 (8분)

findall은 패턴에 캡처 그룹이 있으면 동작이 바뀝니다.

캡처 그룹 수findall이 돌려주는 것
0개매치 전체 문자열의 리스트
1개그 그룹의 문자열 리스트
2개 이상그룹들의 튜플 리스트
re.findall(r"\d{4}[-./]\d\d[-./]\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
re.findall(r"(\d{4})[-./]\d\d[-./]\d\d", orders)
# ['2026', '2026', '2026', '2026']
re.findall(r"(\d{4})[-./](\d\d)[-./](\d\d)", orders)
# [('2026', '10', '20'), ('2026', '10', '20'), ('2026', '10', '19'), ('2026', '10', '20')]

연·월·일이 필요할 때는 튜플 리스트가 편리합니다. 문제는 묶으려고만 쓴 괄호도 그룹이 된다는 점입니다.

re.findall(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)
# [('-', '-'), ('.', '.'), ('/', '/'), ('-', '/')]

날짜를 원했는데 구분자만 돌아왔습니다. 매치 전체가 필요하면 괄호를 (?:...)로 바꾸거나, finditer로 Match를 받아 group()을 씁니다.

re.findall(r"\d{4}(?:-|\.|/)\d\d(?:-|\.|/)\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
[m.group() for m in re.finditer(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)]
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']

도트의 한마디

findall 결과가 이상하게 짧거나 튜플이 나오면, 먼저 패턴의 괄호를 세어 보세요. 묶으려고만 쓴 괄호가 몰래 그룹이 돼 있을 때가 많아요. 3교시에서 (ha)+ 를 findall로 돌렸을 때 'ha'만 나온 것도 이것 때문이에요.

증거 분석실

re.search(r"", text)

돌려준 값

findall 결과가 구분자 튜플입니다. 두 괄호를 (?: 로 바꾸거나, finditer를 골라 날짜 전체가 나오게 해 보세요.

5-6. 매치되지 않은 그룹, 반복된 그룹 (6분)

그룹이 매치에 참여하지 않으면 그 자리는 None입니다. ?가 붙은 그룹이 0번 매치했거나, |의 다른 쪽이 선택된 경우입니다.

m = re.fullmatch(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10")
print(m.groups())     # ('2026', '10', None, None)
print(m.group(3))     # None
print(re.search(r"(a)|(b)", "b").groups())   # (None, 'b')

그룹에 수량자를 붙여 여러 번 매치하면, 그룹에는 마지막 한 번만 남습니다.

m = re.fullmatch(r"(\d)+", "2026")
print(m.group(0), m.group(1))   # 2026 6

re.findall(r"(\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['113.', '100.']
re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['203.0.113.66', '198.51.100.7']

(\d)+는 네 번 매치했지만 그룹 1에는 마지막 6만 남았습니다. IP 주소도 그룹에는 세 번째 반복인 113.만 남았고, findall은 그 그룹을 돌려줬습니다. 5-1의 IP 패턴을 findall에 쓸 때는 (?:...)가 정답입니다.

사건 해결 (10분)

세 가지 구분자 -, ., /는 [-./]로, 구분자 없는 20261019는 ?로 받습니다. 연·월·일은 각각 캡처합니다.

DATE = r"(\d{4})[-./]?(\d\d)[-./]?(\d\d)"

for m in re.finditer(DATE, orders):
    y, mo, d = m.groups()
    print(f"{y}년 {int(mo)}월 {int(d)}일")
# 2026년 10월 20일
# 2026년 10월 20일
# 2026년 10월 19일
# 2026년 10월 19일
# 2026년 10월 20일

주문 번호까지 함께 꺼내려면 그룹을 하나 더 둡니다. 그룹이 넷이니 findall은 네 칸짜리 튜플을 돌려줍니다.

re.findall(r"#(\d+) " + DATE, orders)
# [('1001', '2026', '10', '20'), ('1002', '2026', '10', '20'), ('1003', '2026', '10', '19'), ('1004', '2026', '10', '19'), ('1005', '2026', '10', '20')]

증거 분석실

re.search(r"", text)

돌려준 값

하이픈만 받는 날짜 패턴입니다. 구분자 자리를 [-./]? 로 바꿔 네 가지 모양을 모두 받아 보세요.

사건 종결 보고서 — 주문 5건의 날짜를 연·월·일로 나눠 정리함. 단, #1005는 구분자가 섞인 채 입력되어 있어 직원 확인이 필요함.

정리된 표를 넘겨받은 정 사장이 마지막 줄을 가리킵니다. "#1005는 2026-10/20인데요. 이것도 그냥 통과했네요?"

re.fullmatch(DATE, "2026-10/20")
# <re.Match object; span=(0, 10), match='2026-10/20'>

맞습니다. [-./]? 두 개는 서로를 모릅니다. 앞에서 -를 썼으면 뒤에도 -를 쓰라고 말할 방법이 지금 패턴에는 없습니다. 소장이 수첩에 한 줄을 적습니다. "앞에서 잡은 걸 뒤에서 다시 부르는 방법이 있어. 그건 10교시에." 오늘은 이 한 건을 사람 눈으로 확인하는 것으로 마칩니다.

실습 과제

  1. re.findall(r"ship=(done|wait)", orders)의 결과를 예상해 보세요. (?:done|wait)로 바꾸면 어떻게 달라질까요?
  2. re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 배송")에서 groups()는 무엇일까요?
  3. 시각 03:11:02에서 시, 분, 초를 그룹으로 꺼내는 패턴을 쓰고, m.group(1, 3)의 결과를 확인해 보세요.

소장의 수첩

  • ( ) 는 묶어서 한 덩어리로 만들고, 매치한 글자를 기억한다. (ab)+ 처럼 수량자를 붙인다.

  • | 는 우선순위가 가장 낮다. ^a|b$ 는 '^a 또는 b$'. 선택지는 왼쪽부터 시도한다.

  • 그룹 번호는 여는 괄호 순서. group(0) 은 전체, groups() 는 튜플, m[1] 은 group(1).

  • (?:...) 는 묶기만 하고 번호를 받지 않는다.

  • findall: 그룹 0개면 전체, 1개면 그 그룹, 2개 이상이면 튜플. 전체가 필요하면 (?:...) 나 finditer.

  • 매치되지 않은 그룹은 None, (\d)+ 처럼 반복된 그룹은 마지막 한 번만 남는다.

← 이전

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

다음 →

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

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

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