R

ReflexKR

AI의 일기장Reflex 공식 문서

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

·

강의 개요

1

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

2

뺑소니 차량: 문자 클래스

3

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

4

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

5

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

6

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

7

보도 전야: 치환과 분리

8

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

9

금고의 규칙: 전후방 탐색

10

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

11

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

12

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

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

강의 시간 5:00–5:50

보도 전야: 치환과 분리

CASE-007

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

의뢰서

보도 전야

의뢰인

유출 사건 취재 기자 오 기자

증거물

익명의 제보자가 보낸 고객 명단 파일. 마켓 정 회원 목록으로 보인다. 구분자가 줄마다 제각각이다.

해결 조건

내일 아침 기사에 실을 수 있게 전화번호 가운데를 가리고, 날짜를 2026-10-20 꼴로 바꾸고, 줄마다 칸을 나눈다.

밤 9시, 사무소 문이 벌컥 열립니다. 오 기자가 숨을 몰아쉬며 노트북을 펼칩니다. "제보가 들어왔습니다. 마켓 정 고객 명단이에요. 내일 아침 기사가 나가야 하는데, 개인정보를 그대로 실을 수는 없잖아요."

소장이 화면을 들여다봅니다. 6교시에 찾은 새벽 3시의 그 회원 목록입니다. "찾기는 끝났어. 오늘은 바꾸고, 자르는 날이야."

id, name; phone | joined
1001,kim,010-0000-5678,20/10/2026
1002; lee; 010-0000-1234; 19/10/2026
1003 | park | 010-0000-9012 | 03/09/2026
1004, choi;010-0000-3456  |28/02/2026
# (a+)+$

맨 끝 줄에 이상한 글자가 보이지만, 오 기자는 마감에 쫓겨 그냥 넘어갑니다.

7-1. 찾아서 바꾸기: re.sub (8분)

re.sub(패턴, 바꿀 문자열, 원본)은 패턴에 매치된 곳을 모두 바꾼 새 문자열을 돌려줍니다. 원본은 그대로 둡니다. 파이썬 문자열은 바뀌지 않는 값이기 때문입니다.

import re

re.sub(r"\d{4}", "****", "010-0000-5678")    # '010-****-****'

네 자리 숫자가 두 군데라서 뒷자리까지 가려졌습니다. 앞 세 자리와 뒤 네 자리는 남기고 가운데만 가리려면, 남길 부분을 그룹으로 잡아 두고 바꿀 문자열에서 다시 불러냅니다. \1은 1번 그룹이 잡은 글자입니다.

re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", "010-0000-5678")   # '010-****-5678'

바꿀 문자열에도 백슬래시가 들어가니 패턴처럼 r"..."로 씁니다.

증거 분석실

re.search(r"", text)

돌려준 값

지금은 회원 번호와 연도까지 별표가 됩니다. 전화번호만 매치하도록 패턴을 고치고, 바꿀 문자열을 \1-****-\2 로 바꿔 가운데만 가리세요. subn을 누르면 몇 군데를 바꿨는지도 나옵니다.

7-2. 바꿀 문자열 안의 그룹: \g<1>, \g, \g<0> (10분)

명단의 날짜는 20/10/2026처럼 일/월/연 순서입니다. 기사에는 2026-10-20으로 실어야 합니다. 그룹 순서를 바꿔 끼우면 됩니다.

re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\3-\2-\1", "20/10/2026")            # '2026-10-20'
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\g<3>-\g<2>-\g<1>", "20/10/2026")   # '2026-10-20'

\3과 \g<3>은 같은 뜻입니다. 6교시의 이름 그룹은 \g<이름>으로 부릅니다. 순서를 외울 필요가 없어 실수가 줄어듭니다.

re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", "20/10/2026")   # '2026-10-20'

\g<0>은 매치 전체입니다. 찾은 것을 지우지 않고 감싸기만 할 때 씁니다.

re.sub(r"\d{3}-\d{4}-\d{4}", r"[\g<0>]", "1001,kim,010-0000-5678")   # '1001,kim,[010-0000-5678]'

그룹 번호 바로 뒤에 숫자를 붙이고 싶을 때는 조심해야 합니다. \10은 "1번 그룹 뒤에 0"이 아니라 10번 그룹입니다. 이럴 때 \g<1>0처럼 꺾쇠로 번호를 닫아 줍니다.

re.sub(r"(\d+)", r"\g<1>0", "kim 5")   # 'kim 50'
re.sub(r"(\d+)", r"\10", "kim 5")      # re.PatternError: invalid group reference 10 at position 1

바꿀 문자열에서도 \n은 줄바꿈 글자가 됩니다. 반면 \q처럼 뜻이 없는 영문자 이스케이프는 오류입니다.

re.sub(r",", r"\n", "a,b")    # 'a\nb'
re.sub(r",", r"\q", "a,b")    # re.PatternError: bad escape \q at position 0

re.PatternError는 3.13부터 쓰는 이름이고, 예전 이름 re.error와 같은 예외입니다. 12교시에 자세히 다룹니다.

증거 분석실

re.search(r"", text)

돌려준 값

바꿀 문자열이 일-월-연 순서로 되어 있습니다. \g<y>-\g<m>-\g<d> 로 고쳐 보세요. \g<0> 이나 \10 을 넣으면 어떻게 되는지도 확인해 보세요.

7-3. 함수로 바꾸기, 그리고 count와 subn (8분)

바꿀 문자열 자리에 함수를 넘길 수도 있습니다. re.sub은 매치가 나올 때마다 그 Match 객체를 함수에 넣고, 함수가 돌려준 문자열로 바꿉니다. 이름의 첫 글자만 남기고 나머지 길이만큼 별표를 찍어 봅시다. 고정된 틀로는 할 수 없는 일입니다.

def hide(m):
    name = m.group(1)
    return "," + name[0] + "*" * (len(name) - 1) + ","

re.sub(r",([a-z]+),", hide, "1003,park,010-0000-9012,03/09/2026")   # '1003,p***,010-0000-9012,03/09/2026'
re.sub(r"[a-z]+", lambda m: m[0].upper(), "kim and lee")             # 'KIM AND LEE'
  • 함수는 Match 하나를 받아 문자열을 돌려줘야 합니다. 숫자를 돌려주면 TypeError입니다.
  • 증거 분석실에서는 함수 치환을 실행할 수 없습니다. 직접 파이썬에서 돌려 보세요.

count는 앞에서부터 몇 군데까지만 바꿀지 정합니다. 기본값 0은 "전부"입니다. re.subn은 바꾼 문자열과 바꾼 횟수를 튜플로 돌려줍니다. 맨 위 명단이 leak이라는 문자열에 들어 있다고 합시다.

re.sub(r"\d{3}-\d{4}-\d{4}", "***", leak, count=1).splitlines()[1:3]
# ['1001,kim,***,20/10/2026', '1002; lee; 010-0000-1234; 19/10/2026']

re.subn(r"\d{3}-\d{4}-\d{4}", "***", leak)[1]   # 4

기사에는 "전화번호 4건을 가렸다"라고 정확한 숫자를 적을 수 있습니다.

7-4. 자르기: re.split (9분)

명단은 줄마다 구분자가 다릅니다. 쉼표, 세미콜론, 세로 막대가 섞였고 앞뒤 빈칸도 제각각입니다. str.split은 구분자를 하나만 받습니다. re.split은 패턴으로 자릅니다.

re.split(r"\s*[,;|]\s*", "a, b;c  |d")               # ['a', 'b', 'c', 'd']
re.split(r"\s*[,;|]\s*", "a, b;c  |d", maxsplit=2)   # ['a', 'b', 'c  |d']
  • 구분자 앞뒤의 \s*가 빈칸까지 구분자로 삼켜서 칸이 깨끗하게 나옵니다.
  • maxsplit=2는 두 번까지만 자르고, 나머지는 마지막 칸에 그대로 남깁니다.

패턴에 캡처 그룹이 있으면, 잘라 낸 구분자도 결과 리스트에 끼워 넣습니다. 어떤 구분자였는지 기록해야 할 때 씁니다. 필요 없다면 (?:...)로 씁니다.

re.split(r"\s*([,;|])\s*", "a, b;c  |d")   # ['a', ',', 'b', ';', 'c', '|', 'd']

글자를 차지하지 않는 빈 매치로도 자를 수 있습니다. \b는 단어 경계 자리에만 매치하니, 단어와 빈칸 사이마다 잘립니다. 양 끝에도 경계가 있어서 빈 문자열이 생깁니다.

re.split(r"\b", "a b")   # ['', 'a', ' ', 'b', '']

증거 분석실

re.search(r"", text)

돌려준 값

쉼표로만 자르면 세미콜론과 세로 막대가 칸 안에 남습니다. 세 구분자와 앞뒤 빈칸을 모두 구분자로 삼되, 전화번호의 - 는 자르지 마세요.

7-5. count, maxsplit, flags는 이름을 붙여서 (5분)

re.sub의 네 번째 자리는 count입니다. 플래그를 넣으려고 무심코 re.I를 네 번째에 쓰면, 플래그가 아니라 횟수로 들어갑니다. re.I의 실제 값이 2이기 때문입니다.

re.sub("a", "b", "aAaA", re.I)         # 'bAbA'  ← 대소문자 무시는 안 되고, 두 번만 바뀜
re.sub("a", "b", "aAaA", flags=re.I)   # 'bbbb'

이 실수가 워낙 흔해서 파이썬 3.13부터는 count, maxsplit, flags를 위치 인자로 넘기면 DeprecationWarning을 냅니다. 위 첫 줄은 'count' is passed as positional argument라는 경고를 냅니다. re.split의 maxsplit, flags도 같습니다. 언제나 count=1, maxsplit=2, flags=re.I처럼 이름을 붙여 쓰세요.

도트의 한마디

저도 '앉아'라고 하면 앉고, 그냥 숫자 2를 부르면 뭘 하라는 건지 몰라요. 함수도 똑같아요. re.sub("a", "b", s, re.I)는 '두 번만 바꿔'로 알아들어요. flags=re.I 라고 이름을 불러 주세요.

사건 해결 (10분)

가리기, 날짜 바꾸기, 자르기를 차례로 잇습니다. 마지막 줄과 머리줄은 빼고 고객 줄만 자릅니다.

masked = re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", leak)
fixed = re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", masked)

for row in fixed.splitlines()[1:-1]:
    print(re.split(r"\s*[,;|]\s*", row))
# ['1001', 'kim', '010-****-5678', '2026-10-20']
# ['1002', 'lee', '010-****-1234', '2026-10-19']
# ['1003', 'park', '010-****-9012', '2026-09-03']
# ['1004', 'choi', '010-****-3456', '2026-02-28']

오 기자가 표를 받아 들고 일어서려는데, 소장이 손을 듭니다. "잠깐. 맨 끝 줄."

print(fixed.splitlines()[-1])   # # (a+)+$

명단 데이터가 아닙니다. 누군가 일부러 남긴 정규식입니다. 이 글자를 그대로 찾으려면 1교시에 배운 대로 메타문자를 하나하나 꺼야 합니다.

print(re.escape("(a+)+$"))          # \(a\+\)\+\$
re.search(r"\(a\+\)\+\$", leak)     # <re.Match object; span=(177, 183), match='(a+)+$'>

증거 분석실

re.search(r"", text)

돌려준 값

서명을 패턴 그대로 넣으면 서명 글자를 찾지 못합니다. 해결 조건의 aaa처럼 a가 이어진 글자를 찾는 정규식이기 때문입니다. 메타문자를 모두 꺼서 서명 글자 자체를 찾으세요.

"이게 뭡니까?" 오 기자가 묻습니다. 소장은 한참 말이 없다가 대답합니다. "되돌아가기 함정. 이걸 읽는 정규식 엔진은 같은 길을 수없이 되돌아가다 멈춰 버려. 내일 기사에는 빼."

오 기자가 떠난 뒤, 소장은 서랍에서 빛바랜 사진 액자 하나를 꺼냅니다. 사무소를 연 첫해의 사진입니다. 한참을 들여다보다가, 액자를 책상 위에 엎어 둡니다.

사건 종결 보고서 — 유출 명단 4건의 전화번호 가운데 자리를 가리고 날짜를 연-월-일로 통일해 기자에게 전달함. 명단 끝에서 의도적으로 심은 정규식 (a+)+$를 발견함. 출처 미상. 별도 조사 필요.

실습 과제

  1. 명단의 회원 번호 1001~1004를 #1001처럼 앞에 #을 붙여 바꿔 보세요. \g<0>을 쓰면 한 줄로 됩니다.
  2. re.split(r"(\s*)([,;|])(\s*)", "a, b")의 결과를 예상해 보세요. 그룹이 세 개면 몇 개의 칸이 끼어들까요?
  3. 함수 치환으로 "price 1500 and 300"의 숫자를 모두 두 배로 바꿔 보세요. 함수가 문자열을 돌려줘야 한다는 점을 잊지 마세요.

소장의 수첩

  • re.sub(패턴, 바꿀 문자열, 원본) 은 새 문자열을 돌려준다. 원본은 그대로.

  • 바꿀 문자열에서 \1 = \g<1>, 이름은 \g<name>, 전체는 \g<0>. 숫자가 이어지면 \g<1>0 (\10 은 10번 그룹).

  • 바꿀 문자열의 \n 은 줄바꿈, \q 처럼 모르는 이스케이프는 오류.

  • 함수 치환: Match 를 받아 문자열을 돌려주는 함수. count 로 횟수 제한, re.subn 은 (결과, 횟수).

  • re.split 은 패턴으로 자른다. maxsplit, 캡처 그룹이 있으면 구분자도 결과에 들어간다.

  • count, maxsplit, flags 는 키워드로. 위치 인자는 3.13부터 DeprecationWarning, re.I 가 count=2 로 들어가는 사고.

← 이전

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

다음 →

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

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

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