오늘 배울 룩어라운드(주변을 엿보기)는 정규식에서 가장 신기하면서도 실전에서 자주 쓰는 기술이에요. 한마디로 "이 뒤에 무엇이 오는지", "이 앞에 무엇이 있는지"를 조건으로만 확인하고, 정작 그 부분은 결과에 포함하지 않는 거예요. 예를 들어 5000원에서 "뒤에 원이 붙은 숫자"만 골라내되 숫자만 가져오고 원은 빼는, 그런 야무진 일을 해줘요. 말로만 들으면 아리송하니 예제로 바로 보여드릴게요. 완전 처음이어도 따라 치면 "아, 이래서 편하구나" 하실 거예요.
15.1 룩어라운드가 뭐예요?
지금까지 배운 정규식은 매치되는 글자를 먹으면서(소비하면서) 지나갔어요. \d+가 5000을 잡으면 그 5000은 결과에 들어가죠. 그런데 룩어라운드는 조건은 확인하되 그 부분을 안 먹어요. "여기 뒤에 이런 게 있나?"만 슬쩍 보고, 있으면 통과, 없으면 탈락시켜요. 확인만 하고 지나가니 결과에는 안 남는 거예요.
왜 이게 좋은지 예로 감을 잡아볼게요. 만약 룩어라운드 없이 \d+원이라고 쓰면 매치 결과에 원까지 딸려 들어와요. 그럼 나중에 원을 또 잘라내야 하죠. 반면 룩어라운드로 "뒤에 원이 있는지"만 확인하면, 조건은 맞추면서도 결과에는 숫자만 남아요. 이렇게 "조건은 걸되 결과는 깨끗하게"가 룩어라운드의 핵심 매력이에요. 조건에 쓰인 글자는 다음 매치를 찾을 때 다시 검사 대상이 될 수 있어서, 겹쳐서 확인하는 일도 가능해져요.
종류는 크게 둘로 나뉘어요. 뒤쪽을 보는 전방탐색과 앞쪽을 보는 후방탐색이에요. 각각 "있어야 함"(긍정)과 "없어야 함"(부정) 버전이 있고요. 표기를 정리하면 전방 긍정은 (?=...), 전방 부정은 (?!...), 후방 긍정은 (?<=...), 후방 부정은 (?<!...)예요. 물음표와 등호, 느낌표의 조합이라 처음엔 헷갈리는데, 예제로 하나씩 만나보면 자연스럽게 익어요.
15.2 뒤에 특정 글자가 오는 것만 (전방탐색)
![[정규식 15] 앞뒤를 엿보는 룩어라운드](https://img.thenullpage.com/posts/6599/6599_1_a68f17.webp)
상황을 볼게요. 5000원처럼 뒤에 원이 붙은 숫자만, 그것도 숫자 부분만 뽑고 싶어요. 패턴은 이래요.
\d+(?=원)
\d+는 숫자 덩어리를 잡는 부분이고, 그 뒤의 (?=원)이 전방탐색이에요. "이 숫자 바로 뒤에 원이 있어야 해. 단, 그 원은 결과에 안 넣어"라는 뜻이에요. 대상은 가격 5000원 그리고 300개예요. 실행해볼게요.
import re
re.findall(r'\d+(?=원)', '가격 5000원 그리고 300개')
결과는 이래요.
['5000']
5000만 깔끔하게 나왔죠? 원은 조건으로만 확인됐을 뿐 결과엔 안 들어갔어요. 300은 뒤가 개라서 "원이 있어야 함" 조건에 걸려 탈락했고요. 만약 전방탐색 없이 \d+원이라고 썼다면 5000원이 통째로 잡혀서 원을 또 떼어내야 했을 거예요. 룩어라운드가 그 수고를 없애준 거죠.
15.3 앞에 특정 글자가 오는 것만 (후방탐색)
이번엔 반대로 앞을 보는 경우예요. 상황은 $50처럼 달러 기호 뒤의 숫자만 잡되, $는 빼고 싶어요. 패턴은 이래요.
(?<=\$)\d+
앞의 (?<=\$)가 후방탐색이에요. "이 숫자 바로 앞에 달러 기호가 있어야 해. 단, 그 기호는 결과에 안 넣어"라는 뜻이에요. 여기서 \$의 역슬래시는 $가 정규식에서 특별한 뜻(줄 끝)을 갖기 때문에 그 능력을 꺼서 평범한 글자로 만든 거예요. 대상은 cost $50 and 30 units예요. 실행해볼게요.
re.findall(r'(?<=\$)\d+', 'cost $50 and 30 units')
결과는 이래요.
['50']
50만 나왔어요. 앞에 $가 있는 숫자니까요. 30은 앞에 달러 기호가 없어서 탈락했고요. 이렇게 후방탐색은 접두 기호(앞에 붙는 표시)를 결과에서 깔끔히 빼내고 싶을 때 아주 유용해요. 가격표, 태그, 특정 기호로 시작하는 값을 다룰 때 자주 만나게 될 거예요.
참고로 후방탐색에도 "없어야 함" 버전인 부정 후방탐색 (?<!...)가 있어요. "앞에 이런 게 있으면 안 돼"라는 뜻이죠. 전방탐색의 긍정·부정, 후방탐색의 긍정·부정, 이렇게 네 식구가 대칭을 이뤄요. 표기가 물음표로 시작하는 건 같고, 앞을 볼 땐 <가 들어가고, "없어야 함"일 땐 등호 대신 느낌표가 들어간다고 기억하면 네 가지가 정리돼요. 파이썬의 후방탐색은 확인할 길이가 고정되어야 한다는 작은 제약이 있는데, 위 예제처럼 기호 한 글자를 보는 경우엔 걱정할 일이 없어요.
15.4 뒤에 특정 글자가 안 오는 것만 (부정 전방탐색)
이번엔 "있어야 함"이 아니라 "없어야 함"이에요. 상황은 뒤에 개가 안 오는 숫자를 잡는 거예요. 패턴은 이래요.
\d+(?!개)
(?!개)가 부정 전방탐색이에요. "이 숫자 뒤에 개가 있으면 안 돼"라는 뜻이죠. 대상은 5000원 300개예요. 실행하면 결과가 조금 의외예요.
re.findall(r'\d+(?!개)', '5000원 300개')
결과는 이래요.
['5000', '30']
5000이야 뒤가 원이니 당연히 통과예요. 그런데 300개에서 30이 왜 나왔을까요? 여기가 이 예제의 묘미예요. 탐욕적인 \d+가 처음엔 300 전부를 잡으려 했는데, 그러면 바로 뒤가 개라서 "개가 없어야 함" 조건에 걸려요. 그래서 한 글자 뱉고(백트래킹, 되돌아가기) 30으로 줄여보니, 이번엔 뒤가 0이라 조건을 통과한 거예요. 마지막 0 하나는 뒤가 개라서 결국 탈락했고요. 부정 룩어라운드는 이렇게 백트래킹 때문에 부분 매치가 생길 수 있으니, 결과가 예상과 다를 때 이 과정을 떠올리면 이해가 돼요.
15.5 천 단위 콤마는 어떻게 넣나요?
룩어라운드가 소비를 안 한다는 성질을 활용한 유명한 실전 기법이 있어요. 바로 큰 숫자에 천 단위 콤마를 넣는 거예요. 1234567을 1,234,567로 바꾸는 그거요. 패턴과 치환은 이래요.
re.sub(r'(\d)(?=(\d{3})+$)', r'\1,', '1234567')
결과는 이래요.
'1,234,567'
원리를 풀면 이래요. (\d)로 숫자 한 개를 잡고, (?=(\d{3})+$)는 "이 뒤에 세 자리씩 딱 떨어지는 숫자가 끝까지 남아 있어야 함"을 확인해요. 그런 자리마다 \1,로 그 숫자 뒤에 콤마를 끼워 넣는 거죠. 전방탐색이 자릿수를 세기만 하고 먹지 않으니까, 같은 숫자를 여러 번 확인하며 정확한 위치에만 콤마를 넣을 수 있어요. 소비하지 않는 특성이 없으면 이런 처리는 훨씬 까다로워요. 룩어라운드가 왜 실전에서 사랑받는지 보여주는 대표 예제예요.
이 패턴에서 $는 앞에서 배운 "문자열 끝"이라는 뜻이에요. "세 자리씩 묶음이 끝까지 딱 맞게 남아 있는 자리"를 찾으려고 끝을 기준으로 센 거죠. 처음 보면 (\d)(?=(\d{3})+$)가 암호 같지만, "숫자 하나 뒤에 세 자리 묶음이 끝까지 남았으면 콤마" 이 한 문장으로 읽으면 뜻이 통해요. 콤마 넣기 같은 실전 작업을 룩어라운드로 이렇게 짧게 끝낼 수 있다는 게 정말 매력적이에요.
15.6 오늘 배운 것 정리
룩어라운드는 앞뒤 조건을 확인만 하고 그 부분을 결과에 넣지 않는 기술이에요. 뒤를 보는 전방탐색은 긍정 (?=...), 부정 (?!...)이고, 앞을 보는 후방탐색은 긍정 (?<=...), 부정 (?<!...)예요. \d+(?=원)으로 원 앞 숫자만, (?<=\$)\d+로 달러 뒤 숫자만 뽑았고, 부정 전방탐색 \d+(?!개)에서는 백트래킹 때문에 30 같은 부분 매치가 생길 수 있다는 것도 봤어요. 마지막으로 소비하지 않는 성질을 활용해 1234567에 콤마를 넣어 1,234,567을 만들어봤고요. 기호가 좀 많지만 "확인만 하고 안 먹는다"는 핵심 하나만 붙잡으면 나머지는 자연스럽게 따라와요.