정규식을 쓰다 보면 "이것 아니면 저것"을 한 번에 찾고 싶을 때가 자주 와요. 고양이 글이든 강아지 글이든 다 모으고 싶고, jpg든 png든 이미지 파일은 다 잡고 싶죠. 저도 검색 기능을 만들 때 "에러 로그랑 경고 로그를 한 번에 걸러줘" 같은 요청을 받고 이걸 처음 제대로 썼어요. 이럴 때 쓰는 게 대체(alternation, 여러 갈래 중 하나) 기호 |예요. 그리고 이 갈래의 범위를 정확히 정해주는 게 그룹(group, 여러 글자를 하나로 묶기) ()예요. 이 둘은 짝꿍처럼 같이 다녀서 오늘 한 번에 볼게요. 세로 막대 하나, 괄호 한 쌍이면 표현할 수 있는 게 확 넓어져요.
12.1 세로 막대는 "또는"이에요
|는 "왼쪽 아니면 오른쪽"을 뜻해요. cat|dog는 "cat 또는 dog"예요.
import re
re.findall(r"cat|dog", "cat dog bird dog")
# ['cat', 'dog', 'dog']
cat 한 번, dog 두 번이 잡히고, 후보에 없는 bird는 빠졌어요. 갈래는 두 개만 되는 게 아니에요. cat|dog|bird처럼 |로 얼마든지 이어 붙일 수 있어요. 예를 들어 로그에서 ERROR|WARN|INFO라고 하면 세 종류 로그를 한 패턴으로 다 잡아요. 여러 후보를 한 번에 검색하는 거죠.
12.2 대괄호랑 뭐가 다른가요?
[ae] 같은 문자 클래스(대괄호로 여러 후보 중 한 글자)를 아신다면 헷갈릴 수 있어요. 결정적 차이가 있어요. 대괄호는 한 글자, 세로 막대는 통째예요. 예를 들어 gray와 grey는 가운데 한 글자만 달라서 gr[ae]y로 잡아요.
re.findall(r"gr[ae]y", "gray grey groy")
# ['gray', 'grey']
여기서 [ae]는 "a 또는 e 중 한 글자"라서 이런 자잘한 차이에 딱이에요. groy는 가운데가 o라 후보에 없어 빠졌고요. 하지만 cat이나 dog처럼 여러 글자로 된 덩어리 중 하나를 고를 땐 대괄호로는 안 되고 |가 필요해요. [catdog]라고 쓰면 "c, a, t, d, o, g 중 한 글자"라는 엉뚱한 뜻이 되거든요. 한 글자면 대괄호, 덩어리면 세로 막대, 이렇게 나눠서 기억하세요.
12.3 괄호로 갈래의 범위를 묶어요
|는 욕심이 많아서, 그냥 두면 왼쪽 전체와 오른쪽 전체로 갈라져요. 그래서 갈래를 일부에만 적용하려면 괄호로 묶어 범위를 정해줘야 해요. 호칭 뒤에 이름이 오는 걸 잡아볼게요. (?:Mr|Ms|Dr)는 "Mr, Ms, Dr 중 하나"를 하나의 묶음으로 만든 거예요.
re.findall(r"(?:Mr|Ms|Dr)\. \w+", "Dr. Kim and Ms. Lee")
# ['Dr. Kim', 'Ms. Lee']
Dr. Kim과 Ms. Lee가 통째로 잡혔어요. 괄호로 호칭 부분만 묶었기 때문에, 뒤의 \. \w+(마침표, 공백, 이름)는 어느 호칭에나 공통으로 붙어요. 여기서 (?:...)는 묶기만 하는 그룹이에요. 그냥 ()만 쓰면 묶는 동시에 그 부분을 따로 저장까지 하는데, 지금처럼 저장이 필요 없고 묶기만 하고 싶을 땐 ?:를 넣어줘요. 저장하는 그룹은 조금 더 무거운 일을 하니, 묶기만 필요하면 ?:를 붙이는 게 깔끔하고 효율적이에요.
12.4 괄호를 빼먹으면 엉뚱하게 갈라져요
괄호가 왜 중요한지 실감 나는 예를 볼게요. |는 정규식에서 결합력이 가장 약한 기호예요. 쉽게 말해 마지막에 편을 가르는데, 그때 양옆에 있는 걸 최대한 넓게 잡아 나눠요. 그래서 괄호 없이 ^cat|dog$라고 쓰면, 이건 "cat 또는 dog"에 앞뒤 조건이 붙은 게 아니에요. ^cat 전체와 dog$ 전체로 갈라져서, "맨 앞의 cat" 또는 "맨 끝의 dog"라는 전혀 다른 뜻이 돼요. 원래 의도대로 "cat이나 dog가 한 줄을 통째로 이루는가"를 검사하려면 ^(?:cat|dog)$처럼 괄호로 갈래만 묶어 앵커 사이에 넣어야 해요. 이 실수는 정말 흔하니, |를 쓸 땐 "이 갈래의 범위가 어디까지지?"를 항상 의심하고 괄호로 명확히 묶는 습관을 들이세요.
12.5 묶음을 통째로 반복시켜요
괄호의 또 다른 쓸모는 수량자를 묶음 전체에 걸기예요. ab라는 묶음이 반복되는 걸 잡고 싶다고 해볼게요. (?:ab)+는 "ab 묶음이 1개 이상"이에요.
re.findall(r"(?:ab)+", "ab abab ababab a")
# ['ab', 'abab', 'ababab']
ab, abab, ababab가 잡히고 외톨이 a는 ab 묶음이 아니라 빠졌어요. 여기서 괄호가 없으면 뜻이 완전히 달라져요. ab+는 괄호가 없어서 +가 바로 앞 글자 b 하나에만 걸려요. 그래서 ab+는 "a 다음에 b가 여러 개"(abbb 같은)를 뜻하고, (?:ab)+는 "ab 묶음이 여러 개"(abababab 같은)를 뜻해요. 괄호 하나로 수량자가 걸리는 범위가 이렇게 달라지는 거예요.
12.6 이미지 파일만 골라내요
실전에서 아주 흔한 조합이 "고정된 부분 + 여러 갈래"예요. 파일명에서 이미지 파일(jpg, png, gif)만 잡아볼게요. \w+\.(?:jpg|png|gif)는 "단어 글자들, 마침표, 그리고 jpg나 png나 gif"라는 뜻이에요.
re.findall(r"\w+\.(?:jpg|png|gif)", "a.jpg b.png c.txt d.gif")
# ['a.jpg', 'b.png', 'd.gif']
a.jpg, b.png, d.gif가 잡히고 c.txt는 확장자가 후보에 없어 빠졌어요. 앞부분은 고정, 확장자만 (?:...)로 갈래를 묶는 이 꼴은 실무에서 정말 자주 만나요. 파일 확장자, 로그 종류, 허용 도메인 같은 걸 걸러낼 때 이 패턴 하나로 다 해결돼요. 새 확장자가 필요하면 | 뒤에 하나 더 붙이기만 하면 되니 넓히기도 쉽고요.
12.7 선택적 글자로도 갈래를 만들 수 있어요
갈래를 표현하는 방법이 |만 있는 건 아니에요. 두 단어가 글자 하나 있고 없고 차이라면 ?가 더 간단해요. 미국식 color와 영국식 colour는 u 하나 차이죠. colou?r에서 u?는 "u가 있거나 없거나"예요.
re.findall(r"colou?r", "color colour")
# ['color', 'colour']
둘 다 잡혔어요. 이걸 |로 쓰면 color|colour가 되는데, 겹치는 부분이 많아 길고 지저분하죠. 이렇게 차이가 글자 하나면 ?, 차이가 덩어리면 |를 쓰는 게 깔끔해요. 같은 결과를 내는 길이 여러 개일 때, 짧고 읽기 쉬운 쪽을 고르는 것도 실력이에요.
12.8 오늘 내용을 정리하면요
|는 "또는"이라 여러 갈래 중 하나를 잡아요. 한 글자 중 고르는 대괄호와 달리, |는 여러 글자 덩어리 중 하나를 골라요. |는 결합력이 약해 양옆을 넓게 갈라버리니, 갈래의 범위는 반드시 괄호로 묶어 정해줘야 해요. 묶기만 하고 저장은 필요 없을 땐 (?:...)를 쓰고요. 괄호로 묶으면 수량자가 묶음 전체에 걸려서 (?:ab)+처럼 덩어리 반복도 표현돼요. "고정 부분 + (?:갈래1|갈래2)" 조합은 확장자나 도메인을 걸러낼 때 실무에서 가장 자주 쓰는 꼴이니 꼭 손에 익혀두세요.