thenullpage.com
가격표에서 4500원 같은 값의 숫자만 뽑으려 했는데 결과에 원까지 딸려 나온 적 있으신가요. 비밀번호에 대문자와 숫자가 각각 하나씩은 들어있는지 한 번에 검사하고 싶은데 방법이 안 떠오르는 경우도 많고요. 이럴 때 쓰는 게 룩어라운드(lookaround)예요. 조건은 조건대로 걸면서 결과에는 포함시키지 않는 특이한 기호라서, 알고 나면 그동안 막혀있던 패턴들이 한꺼번에 풀려요.
06.1 조건은 걸되 결과에는 안 넣을 수 있나요?
가격 문자열에서 숫자만 꺼내는 상황부터 볼게요.
import re
가격 = "커피 4500원 케이크 6800원"
re.findall(r"\d+원", 가격)
결과: ['4500원', '6800원']
뒤에 원이 붙어야 한다는 조건은 잘 걸렸는데, 그 원까지 결과에 들어와버렸어요. 조건으로만 쓰고 싶은데 패턴에 적는 순간 글자를 가져가버리는 게 문제예요. 이걸 해결하는 게 전방탐색(lookahead)이에요. 소괄호 안에 ?=를 적고 조건을 쓰면, 그 자리 뒤에 그게 있는지만 보고 아무것도 안 가져가요.
re.findall(r"\d+(?=원)", 가격)
결과: ['4500', '6800']
숫자만 깔끔하게 남았죠. 이렇게 폭이 0이라서 자리만 확인하고 지나가는 게 룩어라운드의 핵심이에요. 만약 6800 뒤에 원이 없었다면 그 숫자는 아예 안 잡혔을 테니, 조건은 조건대로 살아있는 거고요.
캡처 그룹을 쓰면 되지 않냐고 생각하실 수도 있어요. re.findall(r"(\d+)원", 가격)도 결과는 똑같이 나오거든요. 다만 치환으로 넘어가면 차이가 확 벌어져요. 그룹으로 잡으면 원까지 통째로 지워지니 다시 붙여줘야 하는데, 전방탐색으로 잡으면 원은 애초에 건드리지도 않아요.
re.sub(r"\d+(?=원)", "0", 가격)
결과: '커피 0원 케이크 0원'
06.2 반대로 뭔가 없어야 한다는 조건도 되나요?
돼요. ?= 대신 ?!를 쓰면 부정 전방탐색이 돼요. 뒤에 그게 오면 안 된다는 뜻이에요.
목록 = "4500원 2026년 300원"
re.findall(r"\d+(?![\d원])", 목록)
결과: ['2026']
원이 붙은 숫자는 전부 빠지고 연도만 남았어요. 여기서 조건에 \d까지 함께 넣은 이유가 있어요. (?!원)이라고만 두면 4500에서 한 칸 물러난 450이 통과해버리거든요. \d+가 최대한 많이 먹으려다 실패하면 한 글자씩 뱉으면서 다시 시도하는 성질 때문이에요. 부정 조건을 쓸 때는 물러설 자리까지 막아두는 습관이 필요해요.
실전에서는 금지어를 걸러낼 때 많이 써요. 아이디가 admin으로 시작하면 안 된다는 규칙을 이렇게 적어요.
패턴 = r"^(?!admin)\w+$"
bool(re.match(패턴, "admin99"))
결과: False
bool(re.match(패턴, "adam99"))
결과: True
맨 앞자리에서 admin이 오는지부터 보고, 아니라면 자리를 그대로 넘겨받은 \w+가 이어서 검사해요.
06.3 뒤가 아니라 앞을 확인할 수도 있나요?
있어요. 후방탐색(lookbehind)이라고 하고 (?<=조건)이라고 써요. 부등호가 왼쪽을 가리키니까 앞쪽을 본다고 외우시면 편해요.
결제 = "USD 120 원화 15000"
re.findall(r"(?<=USD )\d+", 결제)
결과: ['120']
USD 뒤에 붙은 금액만 잡혔고 USD 자체는 결과에 없어요. 부정형인 (?<!조건)도 똑같이 있어요.
문장 = "정가 $30 수량 12"
re.findall(r"(?<!\$)\b\d+", 문장)
결과: ['12']
달러 기호 바로 뒤에 있는 30은 걸러지고 12만 남았어요. \b를 같이 쓴 건 30에서 3만 건너뛰고 0부터 새로 잡는 걸 막기 위해서예요.
후방탐색은 치환할 때 특히 편해요. 앞에 붙은 표시는 그대로 두고 값만 갈아끼우고 싶을 때요.
re.sub(r"(?<=pw=)\w+", "****", "id=hong pw=abcd1234")
결과: 'id=hong pw=****'
06.4 조건 여러 개를 한꺼번에 검사할 수 있나요?
룩어라운드가 제일 요긴한 순간이에요. 비밀번호 규칙처럼 동시에 만족해야 하는 조건이 여러 개일 때요. 폭이 0이라 앞으로 나아가지 않으니까, 같은 자리에 조건을 여러 개 겹쳐 쓸 수 있거든요.
패턴 = r"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$"
bool(re.search(패턴, "abcdefgh"))
결과: False
bool(re.search(패턴, "Abcdefg1"))
결과: True
맨 앞자리에서 세 번을 확인해요. 어딘가에 숫자가 있는지, 소문자가 있는지, 대문자가 있는지요. 확인이 끝날 때마다 다시 맨 앞으로 돌아오기 때문에 세 조건의 순서를 바꿔 적어도 결과가 같아요. 조건을 다 통과하고 나면 그제야 .{8,}가 실제로 여덟 글자 이상을 가져가고요. 특수문자 조건을 더하고 싶으면 (?=.*[!@#])처럼 한 덩어리만 끼워 넣으면 돼요. 반대로 금지 조건도 같은 방식이에요. 공백이 들어가면 안 된다면 (?!.*\s)를 하나 더 붙이면 끝이고요.
06.5 실전에서는 어디에 제일 많이 쓰나요?
숫자에 천 단위 콤마를 찍는 작업이 대표적이에요. 하려는 건 뻔한데 정규식으로 어떻게 적을지 감이 안 오는 대표 문제이기도 하고요.
re.sub(r"\d(?=(\d{3})+$)", r"\g<0>,", "1234567")
결과: '1,234,567'
패턴을 풀어보면 뒤에 숫자가 세 개씩 딱 맞게 묶여서 끝까지 이어지는 숫자 한 글자를 찾는 거예요. 1234567에서는 1과 4가 여기에 해당해요. \g<0>는 매칭된 것 전체, 그러니까 방금 잡은 숫자 한 글자를 뜻하니까 그 뒤에 콤마만 덧붙인 셈이고요. 조건 부분은 폭이 0이라 소비되지 않으니 뒷자리 숫자들도 그대로 살아있어요. 만약 조건을 (?=(\d{3})+$)가 아니라 (\d{3})+$라고 적었다면 뒤의 여섯 자리를 통째로 먹어치워서 '1234567,'처럼 맨 끝에 콤마 하나만 달랑 붙었을 거예요.
06.6 후방탐색에서 에러가 나던데 왜 그런가요?
파이썬 re의 후방탐색에는 제약이 하나 있어요. 조건의 길이가 고정이어야 해요.
re.findall(r"(?<=USD ?)\d+", "USD120")
결과: error: look-behind requires fixed-width pattern
물음표 하나 넣었다고 에러가 났어요. 있을 수도 없을 수도 있으니 길이가 1일 수도 0일 수도 있어서예요. 같은 이유로 +나 *, {2,4}도 후방탐색 안에서는 못 써요. 해결책은 길이가 고정된 조건을 |로 나열하거나, 아예 캡처 그룹으로 바꿔서 group(1)로 꺼내는 거예요.
re.findall(r"(?<=USD )\d+|(?<=USD)\d+", "USD 120 USD90")
결과: ['120', '90']
전방탐색에는 이런 제약이 없어요. 환경마다 사정도 조금 달라서 자바스크립트는 후방탐색에 가변 길이를 허용하고, 파이썬도 pip install regex로 설치하는 regex 모듈을 쓰면 이 제약이 풀려요.
정리하면 룩어라운드는 확인만 하고 소비하지 않는 조건이에요. 뒤를 보는 (?=)와 (?!), 앞을 보는 (?<=)와 (?<!) 네 개가 전부예요. regex101에 오늘 패턴을 붙여넣으면 조건으로 쓴 부분이 하이라이트에서 쏙 빠지는 게 보이니까, 폭이 0이라는 말이 무슨 뜻인지 눈으로 바로 확인하실 수 있어요.