thenullpage.com
전화번호가 그대로 찍힌 로그를 보고 뒷자리만 가려야겠다 싶으셨던 적 있으신가요. 2026-07-28을 28/07/2026으로 뒤집는 일도 자르고 붙이는 코드로 하면 금세 길어지고요. 찾는 것까지는 정규식으로 해결했는데 그다음 바꾸는 자리에서 막히는 경우가 참 많아요. 치환은 sub 함수 하나가 전부인 것처럼 보이지만, 바꿀 내용을 문자열이 아니라 함수로도 넘길 수 있다는 걸 알고 나면 할 수 있는 일이 확 늘어나요.
09.1 sub은 어떻게 쓰나요?
찾을 패턴, 바꿀 내용, 대상 문자열 순으로 세 개를 넘기면 돼요.
import re
문장 = "사과 3개, 사과 5개"
re.sub(r"사과", "배", 문장)
결과: '배 3개, 배 5개'
print(문장)
결과: 사과 3개, 사과 5개
바꿨는데 원본은 그대로죠. 파이썬 문자열은 값을 고칠 수 없는 자료라 sub은 언제나 새 문자열을 만들어서 돌려줘요. 결과를 변수에 받아두지 않으면 아무 일도 안 일어난 것처럼 보이니 여기서 한 번씩 헤매요.
앞에서 몇 개까지만 바꾸고 싶으면 count를 붙여요.
re.sub(r"사과", "배", 문장, count=1)
결과: '배 3개, 사과 5개'
참고로 찾을 게 기호 하나 없는 고정 문자열뿐이라면 문자열.replace가 더 빠르고 읽기도 쉬워요. 정규식은 패턴이 필요할 때 꺼내는 도구예요.
09.2 찾은 부분을 살려서 바꿀 수 있나요?
괄호로 잡아두면 바꿀 문자열 안에서 번호로 다시 불러 쓸 수 있어요.
날짜 = "2026-07-28"
re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", 날짜)
결과: '28/07/2026'
괄호 순서대로 1번, 2번, 3번이 붙고 바꿀 문자열에서는 이 번호로 순서를 마음대로 섞을 수 있어요. 잘라서 이어 붙이던 코드가 한 줄로 줄었죠.
그룹에 이름을 붙여뒀다면 \g<이름> 형태로 불러요.
re.sub(r"(?P<year>\d{4})-(?P<month>\d{2})", r"\g<month>월 \g<year>년", "2026-07")
결과: '07월 2026년'
번호도 \g<1>처럼 적을 수 있는데, 이 형태가 꼭 필요한 순간이 있어요. 그룹 바로 뒤에 숫자를 붙일 때예요.
re.sub(r"(\d+)", r"\10", "3")
결과: error: invalid group reference 10
re.sub(r"(\d+)", r"\g<1>0", "3")
결과: '30'
1번 그룹 뒤에 0을 붙이려던 건데 엔진이 \10을 통째로 10번 그룹이라고 읽어버렸어요. 꺾쇠로 감싸주면 어디까지가 번호인지 분명해져요.
09.3 바꿀 문자열 앞에도 r을 붙여야 하나요?
붙이는 게 안전해요. 빼먹으면 조용히 이상한 게 들어가거든요.
re.sub(r"(\d+)", "\1번", "5")
결과: '\x01번'
보이는 글자가 아니라 제어문자가 박혔어요. 파이썬이 정규식 엔진에 넘기기도 전에 "\1"을 그런 문자로 해석해버린 탓이에요. 앞에 r을 붙이면 백슬래시가 그대로 전달돼서 '5번'이 제대로 나와요.
진짜 백슬래시 문자를 넣고 싶을 때도 규칙이 있어요.
re.sub(r"이름", r"\\사용자", "이름 폴더")
결과: \사용자 폴더
두 개를 적어야 하나가 나와요. 바꿀 문자열도 엔진이 한 번 훑고 지나가기 때문이에요. 윈도우 경로를 넣다가 여기서 자주 막혀요.
넣을 값이 사용자 입력처럼 무엇이 들어올지 모르는 변수라면 아예 이 검사를 안 거치게 하는 방법도 있어요. 09.4에서 볼 함수 치환으로 넘기면 돌려준 글자가 그대로 박히거든요. 백슬래시가 섞여 있어도 사고가 안 나요.
09.4 바꿀 값을 계산해야 하면요?
문자열 자리에 함수를 넣으면 돼요. 이게 sub의 진짜 무기예요.
def 두배(m):
return str(int(m.group()) * 2)
re.sub(r"\d+", 두배, "사과 3개 배 5개")
결과: '사과 6개 배 10개'
엔진이 한 군데 찾을 때마다 그 함수를 부르고, 함수가 돌려준 문자열을 그 자리에 끼워 넣어요. 함수가 받는 m은 매치 객체라 m.group(1)처럼 그룹도 그대로 꺼내 쓸 수 있고요. 다만 돌려주는 값은 반드시 문자열이어야 해서 숫자는 str로 감싸야 해요.
내용이 짧으면 lambda로도 충분해요.
re.sub(r"\d+", lambda m: "#" * len(m.group()), "a12 b345")
결과: 'a## b###'
숫자 길이만큼 우물 정을 찍었어요. 바꿀 내용이 찾은 내용에 따라 달라지는 일이라 문자열로는 흉내 낼 수 없죠.
09.5 어떤 건 바꾸고 어떤 건 놔둘 수 있나요?
함수 안에서 조건을 걸고, 안 바꿀 자리는 잡은 것을 그대로 돌려주면 돼요.
def 처리(m):
수 = int(m.group())
if 수 >= 10:
return "많음"
return m.group()
re.sub(r"\d+", 처리, "3 12 7 20")
결과: '3 많음 7 많음'
10 이상만 바뀌고 나머지 숫자는 원래대로 남았어요. 원래 글자를 그대로 돌려주면 결과적으로 그 자리는 손대지 않은 셈이 되거든요. 사전에 있는 단어인지 확인한다든가, 패턴만으로는 도저히 표현 못 하는 조건도 여기서 다 처리할 수 있어요.
09.6 몇 개나 바뀌었는지 알 수 있나요?
subn을 쓰면 바뀐 문자열과 횟수를 같이 줘요.
re.subn(r"\s+", " ", "a b c")
결과: ('a b c', 2)
바뀐 게 있을 때만 파일을 저장하는 스크립트에서 요긴해요. 횟수가 0이면 건드릴 게 없다는 뜻이니 그냥 넘기면 되니까요.
같은 패턴을 여러 번 쓸 거라면 미리 만들어두고 점을 찍어 부르는 편이 깔끔해요.
모음 = re.compile(r"[aeiou]")
모음.sub("*", "banana")
결과: 'b*n*n*'
09.7 실전에서는 어떤 치환을 많이 하나요?
첫째는 공백 정리예요. 사용자가 어딘가에서 복사해 붙인 글에는 연속 공백과 줄바꿈이 잔뜩 섞여 있거든요.
re.sub(r"\s+", " ", " 많은 공백 ").strip()
결과: '많은 공백'
둘째는 개인정보 마스킹이고요.
전화 = "010-1234-5678"
re.sub(r"(\d{3})-(\d{4})-(\d{4})", lambda m: m.group(1) + "-****-" + m.group(3), 전화)
결과: '010-****-5678'
앞뒤 자리는 그룹으로 살리고 가운데만 별표로 덮었어요. 이 경우는 r"\1-****-\3"이라고 문자열로 적어도 결과가 같은데, 가린 자릿수에 맞춰 별표 개수를 정하고 싶다면 함수 쪽이 편해요.
셋째는 이름 규칙 바꾸기예요.
re.sub(r"(?<!^)([A-Z])", r"_\1", "userNameValue").lower()
결과: 'user_name_value'
맨 앞이 아닌 대문자 앞에만 밑줄을 넣고 전체를 소문자로 내렸어요. 표기 규칙이 다른 코드를 옮길 때 이 한 줄이 수백 군데를 처리해줘요.
정리하면 치환에서 챙길 건 네 가지예요. 잡은 자리는 그룹 번호로 다시 부르기, 바꿀 문자열에도 r 붙이기, 몇 개 바꿨는지 궁금하면 subn, 그리고 계산이나 조건이 끼면 문자열 대신 함수 넘기기. 특히 함수 치환은 처음엔 낯설어도 한 번 써보면 계속 손이 가니 꼭 직접 쳐보세요.