지난 편에서 대괄호 [ ] 안에 후보 글자를 나열하면 그중 한 글자에 매치된다는 걸 배웠어요. 그런데 소문자 스물여섯 개를 [abcdefghijklmnopqrstuvwxyz]처럼 다 적으려면 손가락이 아프잖아요. 그래서 정규식에는 범위를 짧게 쓰는 방법과, 반대로 이것만 빼고를 뜻하는 부정이 있어요. 저는 이 두 가지를 익히고 나서 대괄호가 갑자기 강력하게 느껴졌어요. 오늘은 이 범위제외를 완전 초보 눈높이로 하나씩 따라 쳐볼게요. 준비물은 파이썬과 re 모듈(정규식 도구상자) 하나면 충분해요.

05.1 대괄호 안에 범위는 왜 필요할까요?

대괄호 안에서 붙임표(하이픈, -)를 두 글자 사이에 넣으면 그 사이의 모든 글자라는 뜻이 돼요. 그러니까 [a-z]는 "a부터 z까지 소문자 아무거나 한 글자"예요. 같은 식으로 [0-9]는 "0부터 9까지 숫자 한 글자"고요. 스물여섯 개를 다 적을 필요 없이 시작과 끝만 딱 정해주면 되는 거죠. 이게 바로 문자 범위예요.


여기서 딱 하나 기억할 게 있어요. 붙임표는 대괄호 안에서, 그것도 두 글자 사이에 있을 때만 범위라는 특별한 뜻을 가져요. 그 외의 자리에서는 그냥 평범한 붙임표 글자일 뿐이에요. 이 위치 감각이 오늘 내내 중요하게 쓰이니까 미리 마음에 담아두세요.

05.2 소문자만 어떻게 골라낼까요?

대소문자와 숫자가 뒤섞인 문장에서 소문자로 된 조각만 뽑고 싶다고 해봐요. 이럴 때 [a-z]가 딱이에요. 뒤에 붙은 +는 "앞의 것이 한 개 이상 이어진다"는 뜻인데, 오늘은 범위에 집중하고 +는 "연속된 덩어리로 묶어준다" 정도로만 알아두면 돼요.


import re


re.findall(r"[a-z]+", "Hello World 123")


# ['ello', 'orld']


결과가 재밌죠? 대문자 HW는 소문자 범위(a~z) 밖이라 빠졌고, 숫자 123도 빠졌어요. 그래서 Hello에서는 대문자 H를 뺀 ello만, World에서는 W를 뺀 orld만 잡힌 거예요. [a-z]가 정확히 소문자만 골라낸다는 게 눈으로 보이시나요? 만약 대문자까지 잡고 싶었다면 범위를 하나 더 넣어야 하는데, 그건 잠시 뒤에 다뤄볼게요.

05.3 숫자 덩어리는 어떻게 잡나요?

이번엔 문장 속에 섞인 숫자만 뽑아볼게요. 방 번호나 층수처럼요. 소문자 범위와 원리가 똑같아요. 숫자 범위 [0-9]를 쓰면 돼요.


re.findall(r"[0-9]+", "room 101 floor 25")


# ['101', '25']


글자 부분인 room과 floor는 숫자 범위 밖이라 전부 무시되고, 10125만 깔끔하게 잡혔어요. 이렇게 [0-9]는 숫자를 뽑을 때 아주 자주 쓰여요. 사실 이 [0-9]는 나중에 배울 \d라는 더 짧은 기호와 완전히 같은 뜻이에요. 지금은 "숫자 범위는 이렇게 직접 적을 수도 있구나" 하고 원리부터 익혀두면, 나중에 축약 기호가 훨씬 편하게 다가올 거예요.

05.4 "이것만 빼고"는 어떻게 표현하나요?

여기서부터가 오늘의 핵심이에요. 지금까지는 "이 글자들 중 하나"를 찾았는데, 반대로 이 글자들만 빼고 나머지 전부를 찾고 싶을 때가 있어요. 예를 들어 "숫자가 아닌 부분만" 같은 거죠. 이럴 때 대괄호 맨 앞캐럿(^)을 붙여요. 이게 부정, 그러니까 "제외"를 뜻해요.


숫자가 아닌 글자만 골라내 볼게요. [^0-9]는 "0부터 9까지가 아닌 한 글자"라는 뜻이에요.


re.findall(r"[^0-9]+", "a1b2c3")


# ['a', 'b', 'c']


숫자 1, 2, 3은 전부 빠지고 그 사이사이의 글자 a, b, c만 잡혔죠? 대괄호 맨 앞의 ^가 "이 범위를 뒤집어라"라고 명령한 거예요. 그래서 [0-9]가 "숫자"였다면 [^0-9]는 "숫자 빼고 전부"가 돼요. 이 부정 기능은 "원하지 않는 것 목록"을 만들어 걸러낼 때 정말 유용해요. 허용할 걸 다 적기보다 금지할 것만 적는 게 편할 때가 많거든요.

05.5 범위를 여러 개 섞어도 되나요?

되고말고요. 대괄호 안에는 범위를 여러 개 이어 붙일 수 있어요. 예를 들어 색상 코드에 쓰이는 16진수 글자들, 그러니까 0부터 9까지의 숫자와 a부터 f까지, A부터 F까지의 알파벳만 잡고 싶다고 해봐요. 이럴 땐 범위 세 개를 그냥 나란히 적으면 돼요.


re.findall(r"[A-Fa-f0-9]+", "color FF00aa zz")


# ['c', 'FF00aa']


결과를 하나씩 뜯어볼게요. FF00aa는 전부 16진수 범위 안이라 통째로 잡혔어요. 그런데 앞의 c는 왜 잡혔을까요? color의 c가 마침 a~f 범위에 들기 때문이에요. 반면 color의 olor는 범위 밖이라 빠졌고요. 맨 뒤의 zz는 z가 어느 범위에도 안 들어서 통째로 빠졌어요. 여기서 배울 점은, 범위를 여러 개 섞을 수 있지만 우연히 범위에 드는 글자(color의 c 같은)도 딸려 온다는 거예요. 그래서 범위를 넓게 잡을 땐 "혹시 원치 않는 게 걸리지 않나" 하고 문맥을 한 번 더 살펴야 해요.


참고로 범위를 붙이는 순서는 상관없어요. [A-Fa-f0-9][0-9a-fA-F]든 결과는 똑같아요. 정규식은 "이 후보들 중 하나"만 따지지, 나열 순서는 신경 쓰지 않거든요. 읽기 좋은 순서로 적으면 그만이에요.

05.6 진짜 캐럿(^) 기호는 어떻게 찾나요?

여기서 초보가 가장 많이 헷갈리는 함정을 짚고 갈게요. 아까 ^가 "제외"라고 했잖아요? 그런데 그건 ^대괄호 맨 앞에 있을 때만이에요. 만약 진짜 캐럿 기호(^) 자체를 찾고 싶다면 어떻게 할까요? 답은 간단해요. ^맨 앞이 아닌 자리에 두면 돼요.


re.findall(r"[a^]", "a^b")


# ['a', '^']


여기서 [a^]는 "a 또는 ^ 한 글자"라는 뜻이에요. ^가 맨 앞이 아니라 a 뒤에 있으니까 제외가 아니라 그냥 평범한 캐럿 글자가 된 거죠. 그래서 대상 문자열 "a^b"에서 a^가 잡혔고, 후보에 없는 b만 빠졌어요. 정리하면 이래요. 대괄호 맨 앞의 캐럿은 제외, 그 외 자리의 캐럿은 그냥 글자. 똑같은 기호인데 위치가 뜻을 완전히 바꾼다는 게 신기하죠?


저는 신입 때 이걸 몰라서 [^] 관련 패턴을 한참 헤맨 적이 있어요. 캐럿을 넣었는데 자꾸 반대로 동작하길래 한참을 봤더니, 위치를 잘못 둔 거였어요. 그때 "정규식은 같은 기호라도 자리에 따라 뜻이 달라진다"는 걸 뼈저리게 배웠어요. 여러분은 이 편을 봤으니 그 시간을 아끼실 수 있을 거예요.

05.7 오늘 배운 걸 정리하면요?

오늘은 대괄호를 훨씬 똑똑하게 쓰는 법을 배웠어요. 짧게 되짚어 볼게요.


첫째, 범위는 붙임표로 표현해요. [a-z]는 소문자 전체, [0-9]는 숫자 전체를 뜻하죠. 스물여섯 개를 다 적지 않아도 시작과 끝만 정하면 돼요. 둘째, 부정은 대괄호 맨 앞의 캐럿으로 표현해요. [^0-9]는 "숫자가 아닌 한 글자"처럼 "이것만 빼고 나머지"를 잡아요. 셋째, 범위는 [A-Fa-f0-9]처럼 여러 개를 섞을 수 있는데, 우연히 범위에 드는 글자도 딸려 오니 문맥을 조심해야 해요. 넷째, 캐럿은 위치가 생명이에요. 맨 앞이면 제외, 그 외 자리면 그냥 캐럿 글자예요.


이 네 가지만 손에 익혀도 "특정 종류의 글자만 골라내기"라는 실전 작업의 절반은 끝난 거예요. 오늘 나온 예제들을 꼭 직접 한 번씩 쳐보면서, 어떤 글자가 왜 잡히고 왜 빠졌는지 눈으로 확인해보세요. 범위와 제외는 앞으로 배울 거의 모든 패턴에 밑재료로 들어가니까, 지금 확실히 다져두면 두고두고 편하답니다.