thenullpage.com
로그에서 ERROR가 들어간 줄과 WARN이 들어간 줄을 한 번에 걸러내고 싶은데 패턴을 두 개 만들어 두 번 돌리고 계시진 않나요. 날짜 구분자가 하이픈일 수도 슬래시일 수도 있어서 코드가 길어지는 경우도 흔하고요. 후보가 여러 개일 때 쓰는 게 대체(alternation)예요. 기호는 세로줄 하나뿐이라 배우는 데 5분이면 되는데, 범위와 순서를 모르면 조용히 틀린 결과를 내주는 기호이기도 해요.
07.1 후보 중에 아무거나 걸리면 되는 패턴은 어떻게 쓰나요?
세로줄 |를 후보 사이에 끼워 넣으면 돼요. 뜻은 그냥 또는이에요.
import re
문장 = "고양이랑 강아지랑 토끼"
re.findall(r"고양이|강아지", 문장)
결과: ['고양이', '강아지']
두 후보 중 하나라도 맞으면 잡아줬어요. 후보는 몇 개든 이어 붙일 수 있고, 치환이나 쪼개기에서도 똑같이 써요.
re.sub(r"고양이|냥이", "야옹이", "고양이랑 냥이")
결과: '야옹이랑 야옹이'
re.split(r",|;|\|", "사과,배;감|귤")
결과: ['사과', '배', '감', '귤']
쪼개기 쪽 패턴에서 마지막 후보에만 백슬래시가 붙은 게 보이시죠. 진짜 세로줄 문자를 찾고 싶으면 \|라고 적어 기호의 뜻을 꺼줘야 해요.
다만 후보가 전부 한 글자라면 문자클래스가 나아요. 방금 것은 r"[,;|]"라고 적어도 결과가 같은데, 대괄호 안에서는 세로줄도 그냥 문자라 백슬래시조차 필요 없어요. 한 글자 후보는 대괄호, 두 글자 이상 덩어리는 세로줄이라고 나눠두시면 편해요.
07.2 세로줄이 어디까지 걸리는 건가요?
대체에서 사고가 제일 많이 나는 지점이에요. 세로줄은 정규식 기호 중에서 힘이 가장 약해요. 그래서 괄호로 막아주지 않으면 패턴 전체를 왼쪽 덩어리와 오른쪽 덩어리로 두 동강 내버려요.
패턴 = r"^cat|dog$"
bool(re.search(패턴, "catfish"))
결과: True
bool(re.search(패턴, "hotdog"))
결과: True
cat 또는 dog라는 단어 하나만 통과시키려고 만든 검사인데 catfish도 hotdog도 통과했어요. 이 패턴을 엔진은 "^cat" 또는 "dog$"로 읽거든요. catfish는 앞이 cat으로 시작하니 왼쪽 후보에, hotdog는 끝이 dog로 끝나니 오른쪽 후보에 걸린 거예요.
고치는 방법은 후보를 괄호로 묶어서 대체의 범위를 못 박는 거예요.
패턴 = r"^(?:cat|dog)$"
bool(re.search(패턴, "catfish"))
결과: False
bool(re.search(패턴, "dog"))
결과: True
이제 앵커가 괄호 바깥에 있으니 두 후보 모두에 걸려요. 값을 꺼내 쓸 게 아니라면 (?:로 시작하는 비캡처 괄호가 깔끔하고요. 일단 괄호부터 치고 안에 후보를 넣는다를 습관으로 만들어두시면 이 사고는 아예 안 만나요.
07.3 후보 순서가 결과를 바꾸나요?
바꿔요. 모르면 한참 헤매는 성질이에요.
re.findall(r"Jan|January", "January 3")
결과: ['Jan']
re.findall(r"January|Jan", "January 3")
결과: ['January']
같은 문자열인데 후보 순서만 바꿨더니 잡힌 길이가 달라졌어요. 엔진은 후보를 왼쪽부터 하나씩 시도하다가 처음 성공한 곳에서 멈춰요. 첫 번째 패턴에서는 Jan이 먼저 성공했으니 뒤에 uary가 남아있든 말든 신경 쓰지 않은 거예요.
규칙은 간단해요. 앞부분이 겹치는 후보끼리는 긴 것을 왼쪽에 두세요. 참고로 파이썬과 자바스크립트, 자바가 이 방식이고 grep 같은 POSIX 계열은 반대로 가장 긴 후보를 골라줘요.
07.4 단어 목록을 패턴으로 만들려면요?
금지어 목록처럼 후보가 파이썬 리스트에 들어있는 경우가 많죠. 이럴 땐 join으로 세로줄을 끼워 붙이면 돼요.
단어 = ["c++", "c#", "go"]
패턴 = "|".join(re.escape(w) for w in sorted(단어, key=len, reverse=True))
re.findall(패턴, "c++와 c# 그리고 go")
결과: ['c++', 'c#', 'go']
안전장치가 두 개 들어가 있어요. 하나는 re.escape예요. 단어에 플러스나 별표 같은 기호가 섞여 있으면 그게 정규식 기호로 해석돼버리는데, 이 함수가 앞에 백슬래시를 붙여서 평범한 글자로 만들어줘요.
print(re.escape("c++"))
결과: c\+\+
다른 하나는 sorted로 길이 역순 정렬한 부분이에요. 07.3의 순서 문제를 목록에서도 그대로 겪거든요. go와 golang이 이 순서로 들어있으면 golang에서 go만 잘려 나와요.
단어 단위로만 찾고 싶어서 경계를 붙일 때도 괄호를 잊으면 안 돼요.
re.findall(r"\bcat|dog\b", "catalog hotdog")
결과: ['cat', 'dog']
re.findall(r"\b(?:cat|dog)\b", "catalog hotdog cat")
결과: ['cat']
위쪽은 앞 경계가 cat에만, 뒤 경계가 dog에만 붙어서 catalog와 hotdog 안쪽까지 다 잡혔어요. 아래처럼 후보를 묶고 경계를 괄호 바깥에 두면 진짜 단어로 쓰인 하나만 남고요.
07.5 findall 결과에서 글자가 사라질 때가 있어요?
대체를 괄호로 묶다 보면 만나는 현상이에요.
re.findall(r"(cat|dog)s?", "cats dogs")
결과: ['cat', 'dog']
분명 cats와 dogs가 잡혔을 텐데 s가 사라졌죠. findall은 패턴 안에 캡처 괄호가 있으면 매칭 전체가 아니라 괄호에 잡힌 것만 돌려주기 때문이에요. 괄호를 (?:로 바꾸면 ['cats', 'dogs']가 제대로 나와요.
반대로 어느 후보가 걸렸는지 알아야 할 때는 일부러 캡처를 나눠 쓰기도 해요.
m = re.match(r"(cat)|(dog)", "dog")
m.groups()
결과: (None, 'dog')
선택되지 않은 후보 자리는 None으로 비어 있어요. 값이 들어있는 자리만 보면 어느 쪽이 걸렸는지 알 수 있으니, 후보마다 다르게 처리하는 코드에서 요긴해요.
07.6 실전에서는 어떻게 조합해서 쓰나요?
제일 흔한 건 로그에서 특정 레벨만 추리는 작업이에요.
로그 = "INFO 시작\nDEBUG 상세\nERROR 실패"
re.findall(r"^(?:INFO|ERROR) \S+", 로그, re.M)
결과: ['INFO 시작', 'ERROR 실패']
줄 앞에서 두 레벨 중 하나를 확인하고 뒤따르는 낱말까지 가져왔어요. 여기서도 괄호가 없으면 앵커가 INFO에만 붙어 DEBUG 줄이 섞여 들어와요.
반대로 대체를 쓰지 않는 게 나은 경우도 알아두면 좋아요. 구분자가 여러 개인 날짜가 그렇거든요.
re.findall(r"\d{4}[-/.]\d{2}[-/.]\d{2}", "2026-07-26 2026/07/27 2026.07.28")
결과: ['2026-07-26', '2026/07/27', '2026.07.28']
날짜 형식을 세 번 반복해 세로줄로 잇는 대신, 달라지는 자리가 한 글자뿐이니 대괄호 한 쌍으로 끝냈어요. 대괄호 안에서는 점도 아무 문자가 아니라 그냥 점이라 백슬래시가 필요 없고요.
후보끼리 앞부분이 같을 때 공통 부분을 밖으로 빼는 것도 좋은 습관이에요. r"파이썬|파이참"은 r"파이(?:썬|참)"이라고 적을 수 있어요. 결과는 같은데 앞 두 글자를 두 번 비교하지 않으니 후보가 늘어날수록 차이가 벌어져요.
정리하면 대체는 후보 나열이고 챙길 건 세 가지예요. 범위는 괄호로 못 박기, 겹치는 후보는 긴 것을 왼쪽에, 값을 안 쓸 괄호는 비캡처로. 한 글자 후보만 있다면 애초에 대괄호가 정답이라는 것까지 같이 기억해두시면 돼요.