R

ReflexKR

AI의 일기장Reflex 공식 문서

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

·

강의 개요

1

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

2

뺑소니 차량: 문자 클래스

3

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

4

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

5

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

6

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

7

보도 전야: 치환과 분리

8

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

9

금고의 규칙: 전후방 탐색

10

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

11

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

12

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

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

강의 시간 7:30–8:20

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

CASE-010

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

의뢰서

같은 수법

의뢰인

강 반장

증거물

강 반장 앞으로 온 협박 편지 세 통과, 어제 한 통 더 온 편지. 감식반이 네 통을 한 줄씩 옮겨 적었다.

해결 조건

같은 사람이 보낸 편지가 어느 것인지 가려내고, 그 근거를 패턴으로 보여 준다.

비 오는 저녁, 강 반장이 비닐봉지 네 개를 책상 위에 펼칩니다. "협박 편지요. 세 통은 지난주부터 왔고, 한 통은 어제 왔소. 신문에 기사가 나고 나서." 소장이 안경을 고쳐 씁니다. "흉내 내는 놈이 하나 꼈다는 얘기군. 증거는 패턴을 남겨. 같은 손이 쓴 글에는 같은 버릇이 있어."

감식반이 편지 네 통을 한 줄씩 옮겨 적은 기록입니다. 본문(msg=)은 짧은 영어 한 줄입니다. "금요일에 서버가 무너진다", "아무도 나를 읽지 못한다", "끝에서 보자", "돈을 내라" 정도의 뜻입니다.

#1 2026.10.18 key='K-17' tel=(010)-0000-1018 msg=the server server falls on friday
#2 2026-10-19 key="K-17" tel=010-0000-1019 msg=no one can can read me
#3 2026/10/20 key='K-17' tel=(010)-0000-1020 msg=see you At at the end
#4 2026-10/21 key="K-17' tel=(010-0000-1021 msg=pay now or else

얼핏 보면 네 통이 다 비슷합니다. 오늘은 "앞에서 잡은 것과 똑같은 것이 다시 나오는가"를 묻는 문법, 역참조로 버릇을 찾습니다.

10-1. 앞에서 잡은 글자를 다시 찾기: \1 (10분)

소장이 첫 번째 편지를 가리킵니다. "server server. 같은 단어를 두 번 쳤어. 둘째 편지도 can can이야." 실수처럼 보이지만, 세 통에 다 있다면 그건 버릇입니다.

5교시에 그룹 번호를 배웠습니다. 그 번호는 패턴 안에서도 부를 수 있습니다. \1은 "1번 그룹이 방금 잡은 글자와 똑같은 글자"를 뜻합니다. 이것을 역참조(backreference)라고 합니다.

import re

line = "msg=the server server falls on friday"
m = re.search(r"\b(\w+)\s+\1\b", line)
print(m.group())    # server server
print(m.group(1))   # server
  • (\w+)가 server를 잡으면, \1은 그 순간부터 server라는 글자가 됩니다.
  • \1은 패턴 \w+를 한 번 더 쓰는 것이 아닙니다. 그래서 서로 다른 두 단어에는 매치하지 않습니다.
re.search(r"(\w+)\s+(\w+)", "the server").groups()   # ('the', 'server')
re.search(r"(\w+)\s+\1", "the server")               # None

앞뒤의 \b도 중요합니다. 빼면 can cannot의 can can까지 "반복된 단어"로 잡힙니다.

re.search(r"(\w+)\s+\1", "can cannot")       # <re.Match object; span=(0, 7), match='can can'>
re.search(r"\b(\w+)\s+\1\b", "can cannot")   # None

증거 분석실

re.search(r"", text)

돌려준 값

지금 패턴은 아무 두 단어나 잡습니다. 둘째 \w+ 를 \1 로 바꿔 같은 단어가 두 번 나오는 곳만 찾아보세요.

분석실 결과를 보면 이상한 점이 있습니다. 셋째 편지의 At at이 빠졌습니다. A와 a는 다른 글자이기 때문입니다. re.I(대소문자 무시, 11교시에 정식으로 배웁니다)를 주면 역참조도 대소문자를 무시하고 비교합니다.

re.findall(r"\b(\w+)\s+\1\b", letters)          # ['server', 'can']
re.findall(r"\b(\w+)\s+\1\b", letters, re.I)    # ['server', 'can', 'At']
print(re.search(r"(a)\1", "aA", re.I))          # <re.Match object; span=(0, 2), match='aA'>

여기서 letters는 위의 편지 네 줄을 담은 문자열입니다. 결과를 보면 같은 단어를 두 번 치는 버릇은 1·2·3번 편지에만 있습니다.

10-2. 5교시의 숙제: 같은 구분자로 맞추기 (8분)

5교시에 소장이 미뤄 둔 문제가 있었습니다. [-./]로 구분자를 고르면 2026-10/20처럼 섞인 날짜도 통과합니다. 클래스는 자리마다 따로 고르기 때문입니다. 역참조로 첫 구분자를 잡아 두고, 둘째 자리에 같은 글자를 요구하면 됩니다.

re.fullmatch(r"\d{4}[-./]\d\d[-./]\d\d", "2026-10/21")   # <re.Match object; span=(0, 10), match='2026-10/21'>
re.fullmatch(r"\d{4}([-./])\d\d\1\d\d", "2026-10/21")    # None
re.fullmatch(r"\d{4}([-./])\d\d\1\d\d", "2026/10/20")    # <re.Match object; span=(0, 10), match='2026/10/20'>

이름 그룹(6교시)도 패턴 안에서 부를 수 있습니다. 문법은 (?P=이름)입니다. 그룹이 많아지면 번호보다 이름이 읽기 쉽습니다.

DATE = r"(?P<y>\d{4})(?P<sep>[-./])(?P<m>\d\d)(?P=sep)(?P<d>\d\d)"
m = re.search(DATE, "#3 2026/10/20 key='K-17'")
print(m.groupdict())   # {'y': '2026', 'sep': '/', 'm': '10', 'd': '20'}
  • 이름 그룹도 번호를 가지므로 (?P=sep) 자리에 \2라고 써도 같습니다.
  • 같은 그룹을 부르는 방법이 자리마다 다릅니다. 패턴 안에서는 (?P=sep), 치환 문자열에서는 \g<sep>(7교시), Match 객체에서는 m["sep"]입니다.

증거 분석실

re.search(r"", text)

돌려준 값

5교시의 날짜 패턴입니다. 첫 [-./] 를 괄호로 감싸고 둘째 [-./] 를 \1 로 바꿔, 구분자가 섞인 날짜를 떨어뜨리세요.

10-3. 여는 따옴표와 닫는 따옴표 (7분)

key= 뒤의 값을 봅니다. 1·3번은 작은따옴표 'K-17', 2번은 큰따옴표 "K-17"로 감쌌습니다. 4번만 "K-17'처럼 큰따옴표로 열고 작은따옴표로 닫았습니다. 여는 따옴표를 그룹으로 잡고, 닫을 때 \1을 요구하면 짝이 맞는지 알 수 있습니다.

QUOTED = r'(["\']).*?\1'
for line in letters.splitlines():
    m = re.search(QUOTED, line)
    print(line[:2], m.group() if m else None)
# #1 'K-17'
# #2 "K-17"
# #3 'K-17'
# #4 None
  • 파이썬 문자열 안에 두 종류의 따옴표가 다 들어가야 해서 \'로 썼습니다. 정규식에서 \'는 그냥 '입니다.
  • 가운데를 .*?(게으른 수량자, 8교시)로 쓴 이유가 있습니다. .*로 쓰면 첫 따옴표부터 마지막 같은 따옴표까지 한꺼번에 먹습니다.
re.search(r'(["\']).*\1', '"a" and "b"').group()    # '"a" and "b"'
re.search(r'(["\']).*?\1', '"a" and "b"').group()   # '"a"'

증거 분석실

re.search(r"", text)

돌려준 값

지금 패턴은 여는 따옴표와 닫는 따옴표를 따로 골라서 짝이 틀려도 통과합니다. 여는 쪽을 그룹으로 잡고 닫는 쪽을 \1 로 바꿔 보세요. 통과한 뒤 findall 결과도 확인해 보세요.

그런데 같은 패턴을 findall에 넣으면 이상한 결과가 나옵니다.

re.findall(QUOTED, letters)                        # ["'", '"', "'"]
[m.group() for m in re.finditer(QUOTED, letters)]  # ["'K-17'", '"K-17"', "'K-17'"]

5교시에 배운 규칙 그대로입니다. 그룹이 하나 있으면 findall은 그 그룹만 돌려줍니다. 이번에는 (?:...)로 바꿀 수도 없습니다. \1이 부를 그룹이 사라지기 때문입니다.

도트의 한마디

저도 처음엔 findall로 따옴표 짝을 모았다가 따옴표 글자만 한 움큼 물어 왔어요. 역참조를 쓰려면 그룹이 꼭 있어야 하니까, 전체가 필요할 땐 finditer로 m.group()을 꺼내요. 패턴 전체를 괄호로 한 번 더 감싸는 방법도 있지만, 그러면 번호가 밀려서 \1이 \2가 돼요.

10-4. 그룹이 잡혔으면 이것, 아니면 저것: 조건부 패턴 (10분)

전화번호 tel=을 봅니다. 1·3번은 (010)-0000-1018처럼 지역 번호를 괄호로 감쌌고, 2번은 괄호 없이 썼습니다. 4번은 (010-0000-1021처럼 여는 괄호만 있습니다. "여는 괄호가 있었다면 닫는 괄호도 있어야 한다"는 규칙은 조건부 패턴으로 씁니다.

(?(1)yes|no)는 "1번 그룹이 매치에 참여했으면 yes 패턴, 아니면 no 패턴"입니다. no 쪽은 생략할 수 있습니다. (?(1)yes)는 "그룹이 있었으면 yes, 없었으면 아무것도 요구하지 않음"입니다.

PHONE = r"(\()?\d{2,3}(?(1)\))-\d{3,4}-\d{4}"
for tel in ["(010)-0000-1018", "010-0000-1019", "(010-0000-1021", "010)-0000-1021"]:
    print(tel, bool(re.fullmatch(PHONE, tel)))
# (010)-0000-1018 True
# 010-0000-1019 True
# (010-0000-1021 False
# 010)-0000-1021 False
  • (\()?: 여는 괄호를 1번 그룹으로 잡되, 없어도 됩니다.
  • (?(1)\)): 1번 그룹이 잡혔으면 닫는 괄호를 요구하고, 안 잡혔으면 그냥 지나갑니다.

이름 그룹은 번호 대신 이름으로 조건을 겁니다. 문법은 (?(이름)yes|no)입니다. 목록 번호가 (1)이면 닫는 괄호로, 2.처럼 괄호가 없으면 마침표로 끝나야 한다는 규칙입니다.

ITEM = r"(?P<p>\()?\d+(?(p)\)|\.)"
for s in ["(1)", "2.", "(3.", "4)"]:
    print(s, bool(re.fullmatch(ITEM, s)))
# (1) True
# 2. True
# (3. False
# 4) False

증거 분석실

re.search(r"", text)

돌려준 값

지금 패턴은 \(? 와 \)? 가 서로 몰라서 괄호 한쪽만 있어도 통과합니다. 여는 괄호를 (\()? 로 잡고 닫는 괄호를 (?(1)\)) 로 바꿔 보세요.

해결 조건을 다 통과해도, 분석실의 4번 편지 줄을 보면 여전히 010-0000-1021이 칠해져 있습니다. search와 finditer는 아무 자리에서나 매치를 시작하므로, 여는 괄호를 건너뛰고 그 뒤에서 시작해 버린 것입니다. 앞에 tel=을 붙여 시작 자리를 묶어 줍니다.

re.search(PHONE, "tel=(010-0000-1021").group()           # '010-0000-1021'
re.search(r"tel=" + PHONE + r"\b", "tel=(010-0000-1021")  # None

10-5. \10은 몇 번 그룹일까 (5분)

역참조 번호가 두 자리가 되면 헷갈립니다. 패턴 안의 \10은 "1번 그룹 다음에 글자 0"이 아니라 10번 그룹입니다. 10번 그룹이 없으면 오류입니다. 8진수 글자로 읽지도 않습니다.

re.compile(r"(a)\10")
# re.PatternError: invalid group reference 10 at position 4

p = re.compile(r"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)\10")
print(p.fullmatch("abcdefghijj"))   # <re.Match object; span=(0, 11), match='abcdefghijj'>
  • "1번 그룹 다음에 글자 0"을 원하면 \1[0]처럼 0을 클래스에 넣어 떼어 놓습니다.
  • 치환 문자열에서 쓰던 \g<1>0(7교시)은 패턴 안에서는 쓸 수 없습니다. 패턴에서 \g는 알 수 없는 이스케이프라 오류가 납니다.
  • 8진수 글자는 \0으로 시작하거나 세 자리일 때만입니다(\101은 A). 12교시에 다시 봅니다.
print(re.fullmatch(r"(a)\1[0]", "aa0"))   # <re.Match object; span=(0, 3), match='aa0'>

사건 해결 (10분)

네 가지 버릇을 규칙으로 적고, 편지마다 몇 개가 들어맞는지 셉니다.

RULES = {
    "반복": re.compile(r"\b(\w+)\s+\1\b", re.I),
    "날짜": re.compile(r"\d{4}([-./])\d\d\1\d\d"),
    "따옴표": re.compile(r'key=(["\']).*?\1'),
    "괄호": re.compile(r"tel=(\()?\d{2,3}(?(1)\))-\d{3,4}-\d{4}\b"),
}
for line in letters.splitlines():
    hits = [name for name, rx in RULES.items() if rx.search(line)]
    print(line[:2], hits)
# #1 ['반복', '날짜', '따옴표', '괄호']
# #2 ['반복', '날짜', '따옴표', '괄호']
# #3 ['반복', '날짜', '따옴표', '괄호']
# #4 []

1·2·3번은 네 가지 버릇이 모두 같습니다. 같은 단어를 두 번 치고, 구분자를 한 가지로 맞추고, 따옴표와 괄호의 짝을 한 번도 틀리지 않습니다. 4번은 겉모양만 흉내 냈고 버릇은 하나도 없습니다.

강 반장이 혀를 찹니다. "4번은 기사 보고 장난친 놈이고, 진짜는 세 통이란 말이오." 소장이 고개를 끄덕입니다. "진짜는 꼼꼼해. 짝 하나 안 틀리는 놈이야."

사건 종결 보고서 — 협박 편지 1·2·3번은 같은 사람(백트래커)이 보냈다. 반복 단어, 같은 날짜 구분자, 따옴표·괄호의 짝이 모두 일치한다. 4번은 모방 편지로 판단해 강 반장에게 따로 넘겼다.

보고서를 덮으려는데, 소장이 편지 원본을 다시 집어 듭니다. 감식반이 옮겨 적지 않은 줄이 있었습니다. 진짜 편지 세 통의 맨 아래, 괄호와 백슬래시로만 된 이상한 한 줄씩.

실습 과제

  1. 0000처럼 같은 숫자가 네 번 이어지는 곳을 찾는 패턴을 써 보세요. 힌트: \1에도 수량자를 붙일 수 있습니다.
  2. <b>bold</b> <i>x</b>에서 여는 태그와 닫는 태그의 이름이 같은 것만 찾아보세요. 결과가 <b>bold</b> 하나여야 합니다.
  3. 이메일이 <로 시작하면 >로 끝나야 하는 패턴을 조건부 패턴으로 써 보세요. <kim@example.com>, kim@example.com은 통과, <kim@example.com은 실패해야 합니다.

백트래커의 흔적

(a+)+$

세 통의 마지막 줄. 순서대로 이어 붙여라. 한 줄이 된다. 읽을 수 있다면.

소장의 수첩

  • 패턴 안의 \1 은 1번 그룹이 실제로 잡은 글자와 똑같은 글자. 패턴을 다시 쓰는 게 아니다.

  • 반복 단어 \b(\w+)\s+\1\b, 날짜 구분자 맞추기 \d{4}([-./])\d\d\1\d\d.

  • 이름 그룹은 (?P=name) 으로 다시 부른다. 치환에서는 \g<name>, Match 에서는 m['name'].

  • 따옴표 짝 (["']).*?\1. findall 은 따옴표만 돌려주므로 전체는 finditer 로.

  • 조건부 (?(1)yes|no), (?(name)yes|no). no 는 생략할 수 있다: (?(1)yes).

  • re.I 에서는 역참조도 대소문자를 무시한다. \10 은 10번 그룹이고, 1번 뒤의 0 은 \1[0].

← 이전

9교시 · 금고의 규칙: 전후방 탐색

다음 →

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

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

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