thenullpage.com

로그에서 날짜를 찾아내는 것까지는 됐는데, 그 안에서 연도랑 월이랑 일을 따로따로 쓰고 싶을 때가 있어요. 파일 이름 수백 개를 한꺼번에 바꾸는데 앞부분은 그대로 두고 뒷자리만 손대고 싶은 경우도 그렇고요. 이럴 때 쓰는 게 정규식의 그룹캡처예요. 오늘은 괄호 하나에서 출발해서 이름 붙인 그룹, 역참조, 치환까지 순서대로 해볼게요.


04.1 괄호로 묶으면 뭐가 달라지나요?

정규식에서 소괄호 ()는 안에 들어있는 패턴을 하나의 덩어리로 묶어줘요. 묶는 순간 두 가지 일이 같이 일어나요. 하나는 반복 기호가 그 덩어리 전체에 걸린다는 것이고, 다른 하나는 그 덩어리에 걸린 문자열이 따로 기억된다는 거예요. 이 기억해두는 동작을 캡처라고 불러요.


반복 범위가 어떻게 달라지는지부터 눈으로 볼게요.


import re
re.match(r"ab+", "ababab").group()
결과: 'ab'
re.match(r"(ab)+", "ababab").group()
결과: 'ababab'


괄호가 없는 ab+에서 +는 바로 앞 글자인 b 하나에만 걸려요. 그래서 a 다음에 b가 몇 개 이어지는지만 보고 ab에서 끝나버려요. 반면 (ab)+는 ab라는 두 글자 덩어리가 반복되는지를 보니까 여섯 글자를 전부 가져와요. 괄호는 이렇게 반복 기호가 걸리는 범위를 정해주는 역할부터 해요.


04.2 매칭된 조각만 따로 꺼내려면 어떻게 하나요?

캡처된 조각은 번호로 꺼내요. 번호는 여는 괄호가 나온 순서대로 왼쪽부터 1번, 2번, 3번으로 붙어요. 0번은 조금 특별한데, 괄호와 상관없이 패턴 전체에 걸린 문자열을 뜻해요.


import re
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "작성일 2026-07-23 승인됨")
m.group(0)
결과: '2026-07-23'
m.group(1)
결과: '2026'
m.group(2)
결과: '07'
m.groups()
결과: ('2026', '07', '23')


연도, 월, 일이 각각 손에 들어왔어요. groups()는 1번부터 끝까지를 한 번에 묶어서 주니까 값을 여러 개 받아야 할 때 편해요.


여기서 초보 때 꼭 한 번은 만나는 에러가 있어요. search는 못 찾으면 None을 돌려주는데, 그 상태에서 바로 group()을 부르면 프로그램이 그 자리에서 멈춰버려요. 그래서 결과를 받으면 있는지부터 확인하는 습관을 들이는 게 좋아요.


m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "날짜가 없는 문장")
if m:
print(m.group(1))
else:
print("못 찾았어요")
결과: 못 찾았어요


04.3 그룹 번호가 헷갈리는데 이름은 못 붙이나요?

붙일 수 있어요. 그룹이 두세 개일 땐 번호로 버틸 만한데, 다섯 개를 넘어가면 3번이 뭐였는지 손가락으로 세고 있는 자신을 발견하게 돼요. 이럴 때 쓰는 게 이름 붙인 그룹이에요. 파이썬에서는 여는 괄호 바로 뒤에 ?P<이름>을 적어요.


패턴 = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
m = re.search(패턴, "2026-07-23")
m.group("month")
결과: '07'
m.groupdict()
결과: {'year': '2026', 'month': '07', 'day': '23'}


groupdict()는 이름과 값을 딕셔너리로 묶어서 주니까 그대로 다른 함수에 넘기기도 좋아요. 이름은 영문자와 숫자, 밑줄로만 짓고 숫자로 시작하면 안 돼요. 자바스크립트나 PHP 같은 다른 환경에서는 P 없이 (?<year>...)라고 쓰는 점만 기억해두시면 돼요.


04.4 괄호는 필요한데 캡처는 필요 없을 때도 있나요?

있어요. 여러 글자를 묶어서 반복이나 선택 처리만 하고 싶고 그 값 자체는 꺼낼 생각이 없는 경우요. 이럴 때 그냥 소괄호를 쓰면 쓸데없는 캡처가 하나 생기면서 뒤에 있는 그룹들의 번호가 전부 한 칸씩 밀려요. 실제로 자주 나는 사고예요.


m = re.search(r"(https?)://(www\.)?(\w+)\.com", "https://www.example.com")
m.group(3)
결과: 'example'


도메인 이름을 꺼내려고 3번을 불러야 하는 상황이에요. (www\.)?는 www가 있을 수도 없을 수도 있어서 묶어둔 것뿐인데 번호 한 칸을 차지하고 있죠. 이럴 때 여는 괄호 뒤에 ?:를 넣으면 비캡처 그룹이 돼요. 묶어주는 일만 하고 번호는 받지 않아요.


m = re.search(r"(https?)://(?:www\.)?(\w+)\.com", "https://www.example.com")
m.group(2)
결과: 'example'


도메인이 2번으로 당겨졌어요. 꺼내 쓸 그룹만 캡처하고 나머지는 비캡처로 두는 습관을 들이면, 패턴 중간에 괄호를 하나 추가해도 뒤쪽 번호가 흔들리지 않아요.


04.5 앞에서 잡은 걸 패턴 안에서 다시 쓸 수 있나요?

이게 캡처의 진짜 재미있는 부분이에요. 캡처한 값을 같은 패턴 안에서 \1, \2처럼 번호로 다시 불러올 수 있는데, 이걸 역참조라고 해요. 같은 패턴을 한 번 더 쓰는 게 아니라 아까 실제로 걸린 그 문자열과 똑같은 것을 요구한다는 게 핵심이에요.


실전에서 제일 많이 쓰는 건 같은 단어가 실수로 두 번 들어간 자리를 찾아내는 거예요.


문장 = "이건 정말 정말 좋은 방법 이에요"
re.findall(r"(\w+) \1", 문장)
결과: ['정말']


글 검수할 때 이 한 줄이 은근히 쓸모 있어요. 또 하나 유용한 게 따옴표 짝 맞추기예요. 작은따옴표로 열었으면 작은따옴표로, 큰따옴표로 열었으면 큰따옴표로 닫힌 것만 잡고 싶을 때요.


텍스트 = """그는 '안녕' 이라 했고 "반가워" 라고 답했어요"""
re.findall(r"(['\"])(.*?)\1", 텍스트)
결과: [("'", '안녕'), ('"', '반가워')]


첫 번째 그룹이 따옴표 한 글자를 잡아두고, 마지막에 있는 \1이 그 따옴표와 똑같은 글자를 요구해요. 그래서 작은따옴표로 열고 큰따옴표로 닫힌 엉뚱한 짝은 걸리지 않아요.


04.6 치환할 때는 그룹을 어떻게 쓰나요?

캡처가 제일 빛나는 순간이 치환이에요. 잡아둔 조각을 재료로 삼아서 순서만 바꾸거나 일부만 가릴 수 있거든요. 파이썬은 re.sub에서 바꿀 문자열 안에 \1 같은 번호를 적어요.


re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "마감 2026-07-23 까지")
결과: '마감 23/07/2026 까지'


연월일 순서를 일월연으로 뒤집었어요. 이름을 붙인 그룹은 \g<이름> 형태로 부르면 되고, 자바스크립트에서는 번호를 $1로, 이름을 $<year>로 씁니다.


여기서 반드시 지켜야 하는 게 하나 있어요. 바꿀 문자열 앞에도 r을 꼭 붙여야 해요. r이 없으면 파이썬이 \1을 그룹 참조가 아니라 눈에 보이지도 않는 제어문자로 해석해버려서, 에러도 안 나고 결과만 조용히 깨져 나와요.


re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1-****-\3", "연락처 010-1234-5678")
결과: '연락처 010-****-5678'


가운데만 별표로 가리고 앞뒤는 캡처해둔 값을 그대로 되돌려 붙인 거예요. 개인정보 마스킹할 때 딱 이 형태로 씁니다.


정리하면 꺼낼 땐 번호나 이름, 값이 필요 없으면 ?:로 비캡처, 같은 값을 다시 요구할 땐 역참조예요. regex101 같은 테스터에 오늘 패턴을 붙여넣으면 그룹 번호와 잡힌 값이 옆에 그대로 표시되니까, 헷갈릴 때 눈으로 확인하면서 연습해보세요.