회원 직접 작성 · 2026-08-28 10:27
파이썬 정규표현식(re) 완벽 가이드
정규표현식(Regular Expression, 흔히 "정규식" 또는 "regex")은 문자열에서 특정 패턴을 찾고, 검사하고, 치환하기 위한 미니 언어입니다. 파이썬에서는 표준 라이브러리 re 모듈을 통해 정규표현식을 사용합니다. 이 글에서는 re 모듈의 기본 사용법부터 패턴 문법, 실전 예제, 자주 하는 실수까지 한 번에 정리합니다.
목차
정규표현식이란
re 모듈 기본 함수
패턴 문법 A to Z
그룹(캡처)과 백레퍼런스
룩어헤드 / 룩비하인드
플래그(옵션)
Match 객체 다루기
re.sub 고급 활용
컴파일과 성능
실전 예제 모음
자주 하는 실수 체크리스트
치트시트 요약표
1. 정규표현식이란
정규표현식은 "문자열의 패턴"을 기술하는 표기법입니다. 예를 들어 \d{3}-\d{4}-\d{4}는 "숫자 3개-숫자 4개-숫자 4개" 형태의 문자열(휴대폰 번호 같은)을 표현합니다.
파이썬에서는 re 모듈을 import해서 사용하며, 패턴 문자열은 특수문자가 많이 섞이므로 항상 raw string(r"...")으로 작성하는 것이 원칙입니다.
import re
pattern = r"\d{3}-\d{4}-\d{4}" # 반드시 r"" (raw string)
text = "제 연락처는 010-1234-5678 입니다."
if re.search(pattern, text):
print("휴대폰 번호 형식을 찾았습니다.")
왜 raw string인가? 일반 문자열에서 \d는 파이썬이 먼저 이스케이프 시퀀스로 해석하려 시도합니다. \d, \w, \s 등은 파이썬 자체의 이스케이프 시퀀스가 아니라서 대부분 경고(DeprecationWarning) 없이 그대로 넘어가지만, \b(단어 경계)는 파이썬에서 백스페이스 문자로 해석되어 완전히 다른 결과가 나옵니다. r"..."을 쓰면 이런 충돌을 원천 차단합니다.
2. re 모듈 기본 함수
함수 | 설명 | 반환값 |
|---|
re.match(pattern, string)
| 문자열 맨 앞에서만 패턴이 일치하는지 검사 | Match 객체 또는 None |
re.search(pattern, string)
| 문자열 전체를 훑어 처음 일치하는 부분을 검사 | Match 객체 또는 None |
re.fullmatch(pattern, string)
| 문자열 전체가 패턴과 완전히 일치하는지 검사 | Match 객체 또는 None |
re.findall(pattern, string)
| 일치하는 모든 부분을 문자열 리스트로 반환 | list |
re.finditer(pattern, string)
| 일치하는 모든 부분을 Match 객체 이터레이터로 반환 | iterator |
re.sub(pattern, repl, string)
| 일치하는 부분을 치환 | 치환된 str |
re.subn(pattern, repl, string)
| sub와 같지만 (결과, 치환 횟수) 튜플 반환
| (str, int) |
re.split(pattern, string)
| 패턴을 구분자 삼아 문자열을 분리 | list |
re.compile(pattern)
| 패턴을 미리 컴파일해 재사용 | Pattern 객체 |
match vs search vs fullmatch 비교
import re
text = "hello world"
print(re.match(r"world", text)) # None (맨 앞부터 봐야 하는데 world로 시작 안 함)
print(re.search(r"world", text)) # <re.Match object; span=(6, 11), match='world'>
print(re.fullmatch(r"hello world", text)) # 전체가 일치해야 매치
print(re.fullmatch(r"hello", text)) # None (전체 문자열이 아니라 일부만 일치)
findall vs finditer
text = "사과 100원, 배 200원, 감 300원"
# findall: 그룹이 있으면 그룹 결과만, 없으면 전체 매치 문자열의 리스트
prices = re.findall(r"\d+", text)
print(prices) # ['100', '200', '300']
# finditer: Match 객체를 순회하며 위치(span) 등 상세 정보에 접근 가능
for m in re.finditer(r"(\w)\s(\d+)원", text):
print(m.group(0), "->", m.group(1), m.group(2), m.span())
주의: findall은 패턴에 그룹 ()이 하나 있으면 "그룹으로 캡처된 부분"만 리스트로 돌려주고, 그룹이 여러 개면 튜플의 리스트를 돌려줍니다. 그룹이 없을 때만 매치 전체 문자열을 돌려줍니다. 이 차이 때문에 실수가 잦으니 꼭 기억해 두세요.
re.findall(r"\d+원", "100원 200원") # ['100원', '200원'] (그룹 없음)
re.findall(r"(\d+)원", "100원 200원") # ['100', '200'] (그룹 1개 → 그룹 값만)
re.findall(r"(\d+)(원)", "100원 200원") # [('100', '원'), ('200', '원')] (그룹 2개 → 튜플)
3. 패턴 문법 A to Z
3-1. 메타문자 개요
기호 | 의미 |
|---|
.
| 줄바꿈(\n)을 제외한 임의의 문자 1개 |
^
| 문자열(또는 각 줄)의 시작 |
$
| 문자열(또는 각 줄)의 끝 |
*
| 앞 패턴이 0회 이상 반복 |
+
| 앞 패턴이 1회 이상 반복 |
?
| 앞 패턴이 0회 또는 1회 (선택적) |
{m,n}
| 앞 패턴이 최소 m회, 최대 n회 반복 |
[]
| 문자 클래스(대괄호 안 문자 중 하나) |
|
| OR (둘 중 하나) |
()
| 그룹(캡처) |
\
| 다음 문자를 이스케이프(메타문자를 문자 그대로 취급) 또는 특수 시퀀스 시작 |
3-2. 문자 클래스 [...]
re.findall(r"[aeiou]", "regular expression") # 모음만: ['e', 'u', 'a', 'e', 'e', ...]
re.findall(r"[0-9]", "abc123def456") # 숫자만: ['1','2','3','4','5','6']
re.findall(r"[^0-9]", "abc123") # ^가 대괄호 맨 앞이면 '부정'(숫자가 아닌 것)
re.findall(r"[a-zA-Z가-힣]", "Hello 안녕 123") # 영문자 + 한글 음절만
[abc] : a, b, c 중 하나
[a-z] : a~z 범위 중 하나
[^abc] : a, b, c가 아닌 문자 (대괄호 안에서 ^는 부정의 의미)
대괄호 안에서는 대부분의 메타문자(., *, + 등)가 문자 그대로 취급됩니다. 단, ], ^, -, \는 위치나 이스케이프에 주의해야 합니다.
3-3. 특수 시퀀스 (미리 정의된 문자 클래스)
시퀀스 | 의미 | 동등 문자 클래스 |
|---|
\d
| 숫자 | [0-9]
|
\D
| 숫자가 아닌 문자 | [^0-9]
|
\w
| 단어 문자(문자/숫자/밑줄, 유니코드 기본 지원) | [a-zA-Z0-9_] (ASCII 모드일 때)
|
\W
| 단어 문자가 아닌 것 | [^a-zA-Z0-9_]
|
\s
| 공백 문자(스페이스, 탭, 줄바꿈 등) | [ \t\n\r\f\v]
|
\S
| 공백이 아닌 문자 | [^ \t\n\r\f\v]
|
\b
| 단어 경계 (단어 문자↔비단어 문자 사이) | - |
\B
| 단어 경계가 아닌 위치 | - |
\A
| 문자열의 맨 처음 (MULTILINE에서도 ^와 달리 문자열 전체의 시작만) | - |
\Z
| 문자열의 맨 끝 | - |
파이썬 3에서 \w, \d, \s는 기본적으로 유니코드를 인식합니다. 즉 \d는 한글이나 아라비아 숫자 이외의 유니코드 숫자도 포함할 수 있고, \w는 한글 음절도 단어 문자로 인식합니다. ASCII만 매칭하고 싶다면 re.ASCII 플래그를 사용하세요.
re.findall(r"\w+", "안녕 hello_world 123") # ['안녕', 'hello_world', '123']
re.findall(r"\w+", "안녕 hello_world 123", re.ASCII) # ['hello_world', '123'] (한글 제외)
3-4. 수량자(반복) - 탐욕적(greedy) vs 게으른(lazy)
수량자 | 의미 |
|---|
*
| 0회 이상 (탐욕적) |
+
| 1회 이상 (탐욕적) |
?
| 0회 또는 1회 (탐욕적) |
{m}
| 정확히 m회 |
{m,}
| m회 이상 |
{,n}
| n회 이하 (m 생략 시 0부터) |
{m,n}
| m회 이상 n회 이하 |
*?, +?, ??, {m,n}?
| 각 수량자의 게으른(non-greedy) 버전 |
기본 수량자(*, +, {m,n} 등)는 **탐욕적(greedy)**이라서 조건을 만족하는 한 최대한 길게 매치합니다. 반대로 ?를 뒤에 붙이면 최소한으로만 매치합니다.
html = "<b>굵게</b><i>기울임</i>"
print(re.findall(r"<.+>", html)) # ['<b>굵게</b><i>기울임</i>'] ← 탐욕적: 처음~끝까지 최대한 매치
print(re.findall(r"<.+?>", html)) # ['<b>', '</b>', '<i>', '</i>'] ← 게으른: 태그 단위로 최소 매치
HTML/XML 태그를 정규식으로 파싱하려는 시도는 실무에서 자주 문제를 일으킵니다. 중첩 구조·속성값 안의 > 등을 정확히 다루기 어렵기 때문에, 진짜 HTML 파싱은 BeautifulSoup, lxml 같은 전용 파서를 쓰는 것이 안전합니다. 정규식은 간단한 태그 스트리핑 정도에만 사용하세요.
3-5. 앵커(위치 지정)
re.findall(r"^\d+", "123 apples, 456 oranges") # ['123'] - 문자열의 시작에서만
re.findall(r"\d+$", "apples: 123") # ['123'] - 문자열의 끝에서만
# 단어 경계 \b: "cat"이라는 온전한 단어만 찾고 "category"의 일부는 제외
re.findall(r"\bcat\b", "cat category concatenate") # ['cat']
re.findall(r"cat", "cat category concatenate") # ['cat', 'cat', 'cat'] (부분 문자열까지 매치)
^와 $는 기본적으로 문자열 전체의 시작/끝을 의미하지만, re.MULTILINE 플래그를 쓰면 각 줄의 시작/끝으로 의미가 바뀝니다 (6절 참고).
4. 그룹(캡처)과 백레퍼런스
4-1. 캡처 그룹 (...)
괄호로 감싼 부분은 "그룹"이 되어 나중에 개별적으로 꺼내 쓸 수 있습니다.
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "오늘은 2026-08-27 입니다.")
print(m.group(0)) # '2026-08-27' (전체 매치)
print(m.group(1)) # '2026'
print(m.group(2)) # '08'
print(m.group(3)) # '27'
print(m.groups()) # ('2026', '08', '27')
4-2. 이름 있는 그룹 (?P<name>...)
그룹이 많아지면 번호보다 이름으로 접근하는 것이 훨씬 가독성이 좋습니다.
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
m = re.search(pattern, "2026-08-27")
print(m.group("year")) # '2026'
print(m.groupdict()) # {'year': '2026', 'month': '08', 'day': '27'}
4-3. 비캡처 그룹 (?:...)
그룹핑(우선순위/반복 단위 지정)은 필요하지만 결과로 꺼내 쓸 필요가 없을 때 사용합니다. 그룹 번호를 차지하지 않아 뒤쪽 그룹의 번호가 밀리지 않는 장점도 있습니다.
re.findall(r"(?:https?://)([\w.]+)", "https://nextfinup.com 방문")
# ['nextfinup.com'] ← 프로토콜 부분은 그룹으로 뽑지 않음
4-4. 백레퍼런스(패턴 내부에서 앞 그룹 재참조) \1, (?P=name)
패턴 안에서 "앞서 캡처한 것과 똑같은 문자열이 다시 나오는" 경우를 표현할 수 있습니다. 중복 단어 탐지에 자주 쓰입니다.
text = "이건 이건 오타고, 저건 저건도 오타입니다."
re.findall(r"(\b\w+\b) \1", text) # ['이건', '저건'] - 같은 단어가 연달아 두 번 나온 경우
# 이름 있는 그룹의 백레퍼런스
re.search(r"(?P<word>\w+) (?P=word)", "hello hello world")
4-5. re.sub에서 그룹 치환 결과에 사용하기: \g<n>
text = "홍길동, 010-1234-5678"
masked = re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1-****-\3", text)
print(masked) # 홍길동, 010-****-5678
5. 룩어헤드 / 룩비하인드
"조건은 확인하되, 실제 매치 결과에는 포함시키지 않는" 고급 패턴입니다.
표기 | 이름 | 의미 |
|---|
(?=...)
| 긍정 룩어헤드 | 뒤에 이 패턴이 와야 매치 (매치 결과엔 미포함) |
(?!...)
| 부정 룩어헤드 | 뒤에 이 패턴이 오면 안 됨 |
(?<=...)
| 긍정 룩비하인드 | 앞에 이 패턴이 있어야 매치 (고정 길이 패턴만 가능) |
(?<!...)
| 부정 룩비하인드 | 앞에 이 패턴이 없어야 매치 |
text = "가격: 15000원, 정가 20000원"
# "원" 앞에 오는 숫자만 (원이라는 글자 자체는 결과에서 제외)
re.findall(r"\d+(?=원)", text) # ['15000', '20000']
# "정가 " 뒤에 오는 숫자만 (정가라는 글자 자체는 결과에서 제외)
re.findall(r"(?<=정가 )\d+", text) # ['20000']
# 숫자로만 이루어진 비밀번호 금지: 숫자만 있는 문자열은 매치되지 않게
re.match(r"^(?!\d+$).+", "12345") # None (숫자로만 구성 → 거부)
re.match(r"^(?!\d+$).+", "abc123") # 매치됨
룩비하인드는 고정 길이만 가능합니다(파이썬 표준 re의 제약). 즉 (?<=\d+)처럼 가변 길이 반복은 사용할 수 없고, (?<=\d{3})처럼 길이가 고정된 패턴만 허용됩니다. 가변 길이 룩비하인드가 필요하면 서드파티 regex 패키지(
pip install regex)를 고려하세요.
6. 플래그(옵션)
re.compile(pattern, flags) 또는 각 함수의 마지막 인자로 전달하며, |로 여러 개를 조합할 수 있습니다.
플래그 | 축약형 | 설명 |
|---|
re.IGNORECASE
| re.I
| 대소문자 구분 없이 매치 |
re.MULTILINE
| re.M
| ^, $가 문자열 전체가 아니라 각 줄의 시작/끝을 의미하게 함
|
re.DOTALL
| re.S
| .이 줄바꿈(\n)까지 포함해서 매치하게 함
|
re.VERBOSE
| re.X
| 패턴 안에 공백/줄바꿈/주석을 허용해 가독성 있게 작성 |
re.ASCII
| re.A
| \w, \d, \s 등을 ASCII 범위로만 제한
|
re.UNICODE
| re.U
| 파이썬 3 문자열 기본값(이미 유니코드) |
re.findall(r"^\w+", "line1\nline2\nline3", re.MULTILINE)
# ['line1', 'line2', 'line3'] (MULTILINE 없으면 ['line1']만)
re.search(r"start.*end", "start\nmiddle\nend", re.DOTALL)
# DOTALL이 없으면 '.'이 줄바꿈을 건너뛰지 못해 매치 실패
VERBOSE로 복잡한 패턴 가독성 높이기
phone_pattern = re.compile(r"""
(\d{2,3}) # 지역번호
-
(\d{3,4}) # 국번
-
(\d{4}) # 번호
""", re.VERBOSE)
m = phone_pattern.search("문의: 02-123-4567")
print(m.groups()) # ('02', '123', '4567')
7. Match 객체 다루기
re.search, re.match 등이 성공하면 돌려주는 Match 객체에는 유용한 메서드들이 있습니다.
메서드 | 설명 |
|---|
.group(n=0)
| n번째 그룹 문자열 (0은 전체 매치) |
.groups()
| 모든 그룹을 튜플로 |
.groupdict()
| 이름 있는 그룹을 딕셔너리로 |
.start(n=0) / .end(n=0)
| n번째 그룹(또는 전체)의 시작/끝 인덱스 |
.span(n=0)
| (start, end) 튜플
|
m = re.search(r"(?P<num>\d+)", "가격은 1500원 입니다")
print(m.group()) # '1500'
print(m.span()) # (4, 8)
print(m.start(), m.end()) # 4 8
8. re.sub 고급 활용
8-1. 함수(콜백)로 치환하기
repl 자리에 문자열 대신 함수를 넣으면, 매치될 때마다 그 함수가 호출되어 반환값으로 치환됩니다. Match 객체를 받아 동적으로 치환값을 계산할 때 매우 강력합니다.
def double_number(m):
return str(int(m.group()) * 2)
re.sub(r"\d+", double_number, "사과 3개, 배 5개")
# '사과 6개, 배 10개'
8-2. count로 치환 횟수 제한
re.sub(r"a", "X", "banana", count=1) # 'bXnana' (첫 번째만 치환)
8-3. split에서 그룹을 쓰면 구분자도 결과에 포함
re.split(r"(\s*,\s*)", "사과, 배,감 , 귤")
# ['사과', ', ', '배', ',', '감', ' , ', '귤'] ← 괄호로 감싸면 구분자 자체도 리스트에 포함됨
re.split(r"\s*,\s*", "사과, 배,감 , 귤")
# ['사과', '배', '감', '귤'] ← 괄호 없으면 구분자는 버려짐
9. 컴파일과 성능
같은 패턴을 반복해서 여러 번 사용한다면 re.compile()로 미리 컴파일해 두는 것이 성능상 유리하고 코드도 깔끔해집니다. (re 모듈은 내부적으로 최근 컴파일 결과를 캐싱하긴 하지만, 캐시 크기 제한이 있고 명시적으로 컴파일하는 편이 의도가 분명합니다.)
EMAIL_RE = re.compile(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$")
def is_valid_email(s: str) -> bool:
return bool(EMAIL_RE.fullmatch(s))
is_valid_email("[email protected]") # True
is_valid_email("잘못된-이메일") # False
성능 팁
반복 호출되는 함수/루프 안에서는 패턴을 매번 새로 만들지 말고 모듈 최상단이나 함수 밖에서 compile해 재사용하세요.
.*, .+ 를 남발한 복잡한 중첩 반복 패턴은 입력에 따라 **재귀적 백트래킹(catastrophic backtracking)**으로 처리 시간이 지수적으로 폭발할 수 있습니다. 가능하면 문자 클래스를 구체적으로 좁히세요 (예: .*" 대신 [^"]*").
대용량 텍스트를 한 번만 훑으면 되는 경우 findall보다 finditer가 메모리 효율적입니다 (전체 리스트를 만들지 않고 하나씩 순회).
10. 실전 예제 모음
10-1. 이메일 형식 검사
EMAIL_RE = re.compile(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$")
EMAIL_RE.fullmatch("[email protected]") # Match
10-2. 휴대폰 번호 추출 및 마스킹
text = "담당자 연락처: 010-1234-5678 / 예비: 010-9999-0000"
numbers = re.findall(r"01\d-\d{3,4}-\d{4}", text)
masked = re.sub(r"(01\d)-(\d{3,4})-(\d{4})", r"\1-****-\3", text)
10-3. URL 추출
url_pattern = re.compile(r"https?://[\w./?=&%#-]+")
url_pattern.findall("자세한 내용은 https://www.nextfinup.com/news/1 참고, 또는 http://old.site")
10-4. 주민등록번호 뒷자리 마스킹 (개인정보 보호)
text = "본인확인용: 900101-1234567"
masked = re.sub(r"(\d{6})-(\d{7})", lambda m: f"{m.group(1)}-{m.group(2)[0]}******", text)
# '본인확인용: 900101-1******'
10-5. 로그 파일에서 특정 필드 파싱
log_line = '2026-08-27 09:12:33 [ERROR] articles.views.news - 스크래핑 실패: timeout'
pattern = re.compile(
r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) "
r"\[(?P<level>\w+)\] (?P<logger>\S+) - (?P<message>.+)"
)
m = pattern.match(log_line)
if m:
print(m.groupdict())
# {'date': '2026-08-27', 'time': '09:12:33', 'level': 'ERROR',
# 'logger': 'articles.views.news', 'message': '스크래핑 실패: timeout'}
10-6. HTML 태그 간단히 제거(순수 텍스트만 추출)
html = "<p>안녕하세요 <b>NextFinUp</b> 입니다.</p>"
plain = re.sub(r"<[^>]+>", "", html)
print(plain) # 안녕하세요 NextFinUp 입니다.
위처럼 태그를 지우는 정도는 괜찮지만, 속성값 안에 >가 들어간 복잡한 HTML은 정규식만으로는 100% 안전하게 처리하기 어렵습니다. 신뢰할 수 없는 입력이라면 전용 HTML 파서를 사용하세요.
10-7. 여러 공백/탭/줄바꿈을 하나의 공백으로 정리
messy = "여러 공백과\t탭,\n줄바꿈이 섞인 문자열"
clean = re.sub(r"\s+", " ", messy).strip()
print(clean) # 여러 공백과 탭, 줄바꿈이 섞인 문자열
10-8. 카멜케이스 → 스네이크케이스 변환
def camel_to_snake(name: str) -> str:
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
camel_to_snake("StockDailyPrice") # 'stock_daily_price'
10-9. 숫자에 3자리마다 콤마 넣기 (정규식 + 룩어헤드)
def add_comma(num: str) -> str:
return re.sub(r"(?<!^)(?=(\d{3})+$)", ",", num)
add_comma("1234567") # '1,234,567'
10-10. 문자열 안의 숫자만 모두 정수로 변환해 합산
text = "1분기 매출 1200, 2분기 매출 3400, 3분기 매출 2900"
total = sum(int(n) for n in re.findall(r"\d+", text))
print(total) # 7500
11. 자주 하는 실수 체크리스트
raw string을 안 씀 → r"\d+"가 아니라 "\d+"로 써서 이스케이프 충돌 발생
.이 모든 문자를 매치한다고 착각 → 기본적으로 줄바꿈(\n)은 매치 안 함 (필요하면 re.DOTALL)
^, $가 항상 "전체 문자열"의 시작/끝이라고 착각 → re.MULTILINE을 켜면 "각 줄"의 시작/끝으로 바뀜
탐욕적 매칭으로 원치 않게 너무 길게 매치됨 → .+ 대신 .+?(게으른) 또는 더 구체적인 문자 클래스([^"]+) 사용
findall에 그룹이 있을 때 전체 매치가 아니라 그룹 값만 반환된다는 것을 몰라 결과 형태에 당황 → finditer + .group(0)을 쓰거나 비캡처 그룹 (?:...) 사용
HTML/XML을 정규식만으로 완벽 파싱하려 함 → 전용 파서(BeautifulSoup 등) 사용
복잡한 중첩 반복 패턴으로 catastrophic backtracking 유발 → 문자 클래스를 구체화하거나 서드파티 regex/re2 계열 검토
매번 같은 패턴을 반복 컴파일 → 반복 호출되는 코드에서는 re.compile로 캐싱
한글 처리 시 \w가 한글을 포함한다는 걸 모르고 별도로 [가-힣]을 추가해 중복/누락 발생 → 파이썬 3의 \w는 기본적으로 한글 음절을 포함(유니코드 모드)
12. 치트시트 요약표
. 임의의 한 문자(줄바꿈 제외)
^ 문자열/줄의 시작
$ 문자열/줄의 끝
* 0회 이상
+ 1회 이상
? 0회 또는 1회 (수량자 뒤에 붙으면 게으른 매칭)
{m,n} m회 이상 n회 이하
[] 문자 클래스
[^] 부정 문자 클래스
| OR
() 캡처 그룹
(?:) 비캡처 그룹
(?P<n>) 이름 있는 그룹
\1 1번 그룹 백레퍼런스
(?=) 긍정 룩어헤드
(?!) 부정 룩어헤드
(?<=) 긍정 룩비하인드
(?<!) 부정 룩비하인드
\d \D 숫자 / 숫자 아님
\w \W 단어문자 / 단어문자 아님
\s \S 공백 / 공백 아님
\b \B 단어 경계 / 단어 경계 아님
\A \Z 문자열의 시작 / 끝
마무리
정규표현식은 처음엔 암호처럼 보이지만, "리터럴 → 문자 클래스 → 수량자 → 앵커 → 그룹 → 룩어라운드" 순서로 하나씩 익히면 금방 손에 익습니다. 실무에서는 regex101.com 같은 온라인 테스터로 패턴을 시각적으로 검증하면서 작성하는 것을 추천합니다. 복잡한 패턴에는 re.VERBOSE와 주석을 적극 활용해서, 나중에 코드를 다시 봤을 때도 바로 이해할 수 있도록 작성하는 습관을 들이세요.
파이썬 정규표현식(re) 완벽 가이드
정규표현식(Regular Expression, 흔히 "정규식" 또는 "regex")은 문자열에서 특정 패턴을 찾고, 검사하고, 치환하기 위한 미니 언어입니다. 파이썬에서는 표준 라이브러리 re 모듈을 통해 정규표현식을 사용합니다. 이 글에서는 re 모듈의 기본 사용법부터 패턴 문법, 실전 예제, 자주 하는 실수까지 한 번에 정리합니다.
목차
정규표현식이란
re 모듈 기본 함수
패턴 문법 A to Z
그룹(캡처)과 백레퍼런스
룩어헤드 / 룩비하인드
플래그(옵션)
Match 객체 다루기
re.sub 고급 활용
컴파일과 성능
실전 예제 모음
자주 하는 실수 체크리스트
치트시트 요약표
1. 정규표현식이란
정규표현식은 "문자열의 패턴"을 기술하는 표기법입니다. 예를 들어 \d{3}-\d{4}-\d{4}는 "숫자 3개-숫자 4개-숫자 4개" 형태의 문자열(휴대폰 번호 같은)을 표현합니다.
파이썬에서는 re 모듈을 import해서 사용하며, 패턴 문자열은 특수문자가 많이 섞이므로 항상 raw string(r"...")으로 작성하는 것이 원칙입니다.
import re
pattern = r"\d{3}-\d{4}-\d{4}" # 반드시 r"" (raw string)
text = "제 연락처는 010-1234-5678 입니다."
if re.search(pattern, text):
print("휴대폰 번호 형식을 찾았습니다.")
왜 raw string인가? 일반 문자열에서 \d는 파이썬이 먼저 이스케이프 시퀀스로 해석하려 시도합니다. \d, \w, \s 등은 파이썬 자체의 이스케이프 시퀀스가 아니라서 대부분 경고(DeprecationWarning) 없이 그대로 넘어가지만, \b(단어 경계)는 파이썬에서 백스페이스 문자로 해석되어 완전히 다른 결과가 나옵니다. r"..."을 쓰면 이런 충돌을 원천 차단합니다.
2. re 모듈 기본 함수
함수
설명
반환값
re.match(pattern, string)
문자열 맨 앞에서만 패턴이 일치하는지 검사
Match 객체 또는 None
re.search(pattern, string)
문자열 전체를 훑어 처음 일치하는 부분을 검사
Match 객체 또는 None
re.fullmatch(pattern, string)
문자열 전체가 패턴과 완전히 일치하는지 검사
Match 객체 또는 None
re.findall(pattern, string)
일치하는 모든 부분을 문자열 리스트로 반환
list
re.finditer(pattern, string)
일치하는 모든 부분을 Match 객체 이터레이터로 반환
iterator
re.sub(pattern, repl, string)
일치하는 부분을 치환
치환된 str
re.subn(pattern, repl, string)
sub와 같지만 (결과, 치환 횟수) 튜플 반환
(str, int)
re.split(pattern, string)
패턴을 구분자 삼아 문자열을 분리
list
re.compile(pattern)
패턴을 미리 컴파일해 재사용
Pattern 객체
match vs search vs fullmatch 비교
import re
text = "hello world"
print(re.match(r"world", text)) # None (맨 앞부터 봐야 하는데 world로 시작 안 함)
print(re.search(r"world", text)) # <re.Match object; span=(6, 11), match='world'>
print(re.fullmatch(r"hello world", text)) # 전체가 일치해야 매치
print(re.fullmatch(r"hello", text)) # None (전체 문자열이 아니라 일부만 일치)
findall vs finditer
text = "사과 100원, 배 200원, 감 300원"
# findall: 그룹이 있으면 그룹 결과만, 없으면 전체 매치 문자열의 리스트
prices = re.findall(r"\d+", text)
print(prices) # ['100', '200', '300']
# finditer: Match 객체를 순회하며 위치(span) 등 상세 정보에 접근 가능
for m in re.finditer(r"(\w)\s(\d+)원", text):
print(m.group(0), "->", m.group(1), m.group(2), m.span())
주의: findall은 패턴에 그룹 ()이 하나 있으면 "그룹으로 캡처된 부분"만 리스트로 돌려주고, 그룹이 여러 개면 튜플의 리스트를 돌려줍니다. 그룹이 없을 때만 매치 전체 문자열을 돌려줍니다. 이 차이 때문에 실수가 잦으니 꼭 기억해 두세요.
re.findall(r"\d+원", "100원 200원") # ['100원', '200원'] (그룹 없음)
re.findall(r"(\d+)원", "100원 200원") # ['100', '200'] (그룹 1개 → 그룹 값만)
re.findall(r"(\d+)(원)", "100원 200원") # [('100', '원'), ('200', '원')] (그룹 2개 → 튜플)
3. 패턴 문법 A to Z
3-1. 메타문자 개요
기호
의미
.
줄바꿈(\n)을 제외한 임의의 문자 1개
^
문자열(또는 각 줄)의 시작
$
문자열(또는 각 줄)의 끝
*
앞 패턴이 0회 이상 반복
+
앞 패턴이 1회 이상 반복
?
앞 패턴이 0회 또는 1회 (선택적)
{m,n}
앞 패턴이 최소 m회, 최대 n회 반복
[]
문자 클래스(대괄호 안 문자 중 하나)
|
OR (둘 중 하나)
()
그룹(캡처)
\
다음 문자를 이스케이프(메타문자를 문자 그대로 취급) 또는 특수 시퀀스 시작
3-2. 문자 클래스 [...]
re.findall(r"[aeiou]", "regular expression") # 모음만: ['e', 'u', 'a', 'e', 'e', ...]
re.findall(r"[0-9]", "abc123def456") # 숫자만: ['1','2','3','4','5','6']
re.findall(r"[^0-9]", "abc123") # ^가 대괄호 맨 앞이면 '부정'(숫자가 아닌 것)
re.findall(r"[a-zA-Z가-힣]", "Hello 안녕 123") # 영문자 + 한글 음절만
[abc] : a, b, c 중 하나
[a-z] : a~z 범위 중 하나
[^abc] : a, b, c가 아닌 문자 (대괄호 안에서 ^는 부정의 의미)
대괄호 안에서는 대부분의 메타문자(., *, + 등)가 문자 그대로 취급됩니다. 단, ], ^, -, \는 위치나 이스케이프에 주의해야 합니다.
3-3. 특수 시퀀스 (미리 정의된 문자 클래스)
시퀀스
의미
동등 문자 클래스
\d
숫자
[0-9]
\D
숫자가 아닌 문자
[^0-9]
\w
단어 문자(문자/숫자/밑줄, 유니코드 기본 지원)
[a-zA-Z0-9_] (ASCII 모드일 때)
\W
단어 문자가 아닌 것
[^a-zA-Z0-9_]
\s
공백 문자(스페이스, 탭, 줄바꿈 등)
[ \t\n\r\f\v]
\S
공백이 아닌 문자
[^ \t\n\r\f\v]
\b
단어 경계 (단어 문자↔비단어 문자 사이)
-
\B
단어 경계가 아닌 위치
-
\A
문자열의 맨 처음 (MULTILINE에서도 ^와 달리 문자열 전체의 시작만)
-
\Z
문자열의 맨 끝
-
파이썬 3에서 \w, \d, \s는 기본적으로 유니코드를 인식합니다. 즉 \d는 한글이나 아라비아 숫자 이외의 유니코드 숫자도 포함할 수 있고, \w는 한글 음절도 단어 문자로 인식합니다. ASCII만 매칭하고 싶다면 re.ASCII 플래그를 사용하세요.
re.findall(r"\w+", "안녕 hello_world 123") # ['안녕', 'hello_world', '123']
re.findall(r"\w+", "안녕 hello_world 123", re.ASCII) # ['hello_world', '123'] (한글 제외)
3-4. 수량자(반복) - 탐욕적(greedy) vs 게으른(lazy)
수량자
의미
*
0회 이상 (탐욕적)
+
1회 이상 (탐욕적)
?
0회 또는 1회 (탐욕적)
{m}
정확히 m회
{m,}
m회 이상
{,n}
n회 이하 (m 생략 시 0부터)
{m,n}
m회 이상 n회 이하
*?, +?, ??, {m,n}?
각 수량자의 게으른(non-greedy) 버전
기본 수량자(*, +, {m,n} 등)는 **탐욕적(greedy)**이라서 조건을 만족하는 한 최대한 길게 매치합니다. 반대로 ?를 뒤에 붙이면 최소한으로만 매치합니다.
html = "<b>굵게</b><i>기울임</i>"
print(re.findall(r"<.+>", html)) # ['<b>굵게</b><i>기울임</i>'] ← 탐욕적: 처음~끝까지 최대한 매치
print(re.findall(r"<.+?>", html)) # ['<b>', '</b>', '<i>', '</i>'] ← 게으른: 태그 단위로 최소 매치
HTML/XML 태그를 정규식으로 파싱하려는 시도는 실무에서 자주 문제를 일으킵니다. 중첩 구조·속성값 안의 > 등을 정확히 다루기 어렵기 때문에, 진짜 HTML 파싱은 BeautifulSoup, lxml 같은 전용 파서를 쓰는 것이 안전합니다. 정규식은 간단한 태그 스트리핑 정도에만 사용하세요.
3-5. 앵커(위치 지정)
re.findall(r"^\d+", "123 apples, 456 oranges") # ['123'] - 문자열의 시작에서만
re.findall(r"\d+$", "apples: 123") # ['123'] - 문자열의 끝에서만
# 단어 경계 \b: "cat"이라는 온전한 단어만 찾고 "category"의 일부는 제외
re.findall(r"\bcat\b", "cat category concatenate") # ['cat']
re.findall(r"cat", "cat category concatenate") # ['cat', 'cat', 'cat'] (부분 문자열까지 매치)
^와 $는 기본적으로 문자열 전체의 시작/끝을 의미하지만, re.MULTILINE 플래그를 쓰면 각 줄의 시작/끝으로 의미가 바뀝니다 (6절 참고).
4. 그룹(캡처)과 백레퍼런스
4-1. 캡처 그룹 (...)
괄호로 감싼 부분은 "그룹"이 되어 나중에 개별적으로 꺼내 쓸 수 있습니다.
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "오늘은 2026-08-27 입니다.")
print(m.group(0)) # '2026-08-27' (전체 매치)
print(m.group(1)) # '2026'
print(m.group(2)) # '08'
print(m.group(3)) # '27'
print(m.groups()) # ('2026', '08', '27')
4-2. 이름 있는 그룹 (?P<name>...)
그룹이 많아지면 번호보다 이름으로 접근하는 것이 훨씬 가독성이 좋습니다.
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
m = re.search(pattern, "2026-08-27")
print(m.group("year")) # '2026'
print(m.groupdict()) # {'year': '2026', 'month': '08', 'day': '27'}
4-3. 비캡처 그룹 (?:...)
그룹핑(우선순위/반복 단위 지정)은 필요하지만 결과로 꺼내 쓸 필요가 없을 때 사용합니다. 그룹 번호를 차지하지 않아 뒤쪽 그룹의 번호가 밀리지 않는 장점도 있습니다.
re.findall(r"(?:https?://)([\w.]+)", "https://nextfinup.com 방문")
# ['nextfinup.com'] ← 프로토콜 부분은 그룹으로 뽑지 않음
4-4. 백레퍼런스(패턴 내부에서 앞 그룹 재참조) \1, (?P=name)
패턴 안에서 "앞서 캡처한 것과 똑같은 문자열이 다시 나오는" 경우를 표현할 수 있습니다. 중복 단어 탐지에 자주 쓰입니다.
text = "이건 이건 오타고, 저건 저건도 오타입니다."
re.findall(r"(\b\w+\b) \1", text) # ['이건', '저건'] - 같은 단어가 연달아 두 번 나온 경우
# 이름 있는 그룹의 백레퍼런스
re.search(r"(?P<word>\w+) (?P=word)", "hello hello world")
4-5. re.sub에서 그룹 치환 결과에 사용하기: \g<n>
text = "홍길동, 010-1234-5678"
masked = re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1-****-\3", text)
print(masked) # 홍길동, 010-****-5678
5. 룩어헤드 / 룩비하인드
"조건은 확인하되, 실제 매치 결과에는 포함시키지 않는" 고급 패턴입니다.
표기
이름
의미
(?=...)
긍정 룩어헤드
뒤에 이 패턴이 와야 매치 (매치 결과엔 미포함)
(?!...)
부정 룩어헤드
뒤에 이 패턴이 오면 안 됨
(?<=...)
긍정 룩비하인드
앞에 이 패턴이 있어야 매치 (고정 길이 패턴만 가능)
(?<!...)
부정 룩비하인드
앞에 이 패턴이 없어야 매치
text = "가격: 15000원, 정가 20000원"
# "원" 앞에 오는 숫자만 (원이라는 글자 자체는 결과에서 제외)
re.findall(r"\d+(?=원)", text) # ['15000', '20000']
# "정가 " 뒤에 오는 숫자만 (정가라는 글자 자체는 결과에서 제외)
re.findall(r"(?<=정가 )\d+", text) # ['20000']
# 숫자로만 이루어진 비밀번호 금지: 숫자만 있는 문자열은 매치되지 않게
re.match(r"^(?!\d+$).+", "12345") # None (숫자로만 구성 → 거부)
re.match(r"^(?!\d+$).+", "abc123") # 매치됨
룩비하인드는 고정 길이만 가능합니다(파이썬 표준 re의 제약). 즉 (?<=\d+)처럼 가변 길이 반복은 사용할 수 없고, (?<=\d{3})처럼 길이가 고정된 패턴만 허용됩니다. 가변 길이 룩비하인드가 필요하면 서드파티 regex 패키지(pip install regex)를 고려하세요.
6. 플래그(옵션)
re.compile(pattern, flags) 또는 각 함수의 마지막 인자로 전달하며, |로 여러 개를 조합할 수 있습니다.
플래그
축약형
설명
re.IGNORECASE
re.I
대소문자 구분 없이 매치
re.MULTILINE
re.M
^, $가 문자열 전체가 아니라 각 줄의 시작/끝을 의미하게 함
re.DOTALL
re.S
.이 줄바꿈(\n)까지 포함해서 매치하게 함
re.VERBOSE
re.X
패턴 안에 공백/줄바꿈/주석을 허용해 가독성 있게 작성
re.ASCII
re.A
\w, \d, \s 등을 ASCII 범위로만 제한
re.UNICODE
re.U
파이썬 3 문자열 기본값(이미 유니코드)
re.findall(r"^\w+", "line1\nline2\nline3", re.MULTILINE)
# ['line1', 'line2', 'line3'] (MULTILINE 없으면 ['line1']만)
re.search(r"start.*end", "start\nmiddle\nend", re.DOTALL)
# DOTALL이 없으면 '.'이 줄바꿈을 건너뛰지 못해 매치 실패
VERBOSE로 복잡한 패턴 가독성 높이기
phone_pattern = re.compile(r"""
(\d{2,3}) # 지역번호
-
(\d{3,4}) # 국번
-
(\d{4}) # 번호
""", re.VERBOSE)
m = phone_pattern.search("문의: 02-123-4567")
print(m.groups()) # ('02', '123', '4567')
7. Match 객체 다루기
re.search, re.match 등이 성공하면 돌려주는 Match 객체에는 유용한 메서드들이 있습니다.
메서드
설명
.group(n=0)
n번째 그룹 문자열 (0은 전체 매치)
.groups()
모든 그룹을 튜플로
.groupdict()
이름 있는 그룹을 딕셔너리로
.start(n=0) / .end(n=0)
n번째 그룹(또는 전체)의 시작/끝 인덱스
.span(n=0)
(start, end) 튜플
m = re.search(r"(?P<num>\d+)", "가격은 1500원 입니다")
print(m.group()) # '1500'
print(m.span()) # (4, 8)
print(m.start(), m.end()) # 4 8
8. re.sub 고급 활용
8-1. 함수(콜백)로 치환하기
repl 자리에 문자열 대신 함수를 넣으면, 매치될 때마다 그 함수가 호출되어 반환값으로 치환됩니다. Match 객체를 받아 동적으로 치환값을 계산할 때 매우 강력합니다.
def double_number(m):
return str(int(m.group()) * 2)
re.sub(r"\d+", double_number, "사과 3개, 배 5개")
# '사과 6개, 배 10개'
8-2. count로 치환 횟수 제한
re.sub(r"a", "X", "banana", count=1) # 'bXnana' (첫 번째만 치환)
8-3. split에서 그룹을 쓰면 구분자도 결과에 포함
re.split(r"(\s*,\s*)", "사과, 배,감 , 귤")
# ['사과', ', ', '배', ',', '감', ' , ', '귤'] ← 괄호로 감싸면 구분자 자체도 리스트에 포함됨
re.split(r"\s*,\s*", "사과, 배,감 , 귤")
# ['사과', '배', '감', '귤'] ← 괄호 없으면 구분자는 버려짐
9. 컴파일과 성능
같은 패턴을 반복해서 여러 번 사용한다면 re.compile()로 미리 컴파일해 두는 것이 성능상 유리하고 코드도 깔끔해집니다. (re 모듈은 내부적으로 최근 컴파일 결과를 캐싱하긴 하지만, 캐시 크기 제한이 있고 명시적으로 컴파일하는 편이 의도가 분명합니다.)
EMAIL_RE = re.compile(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$")
def is_valid_email(s: str) -> bool:
return bool(EMAIL_RE.fullmatch(s))
is_valid_email("
[email protected]") # True
is_valid_email("잘못된-이메일") # False
성능 팁
반복 호출되는 함수/루프 안에서는 패턴을 매번 새로 만들지 말고 모듈 최상단이나 함수 밖에서 compile해 재사용하세요.
.*, .+ 를 남발한 복잡한 중첩 반복 패턴은 입력에 따라 **재귀적 백트래킹(catastrophic backtracking)**으로 처리 시간이 지수적으로 폭발할 수 있습니다. 가능하면 문자 클래스를 구체적으로 좁히세요 (예: .*" 대신 [^"]*").
대용량 텍스트를 한 번만 훑으면 되는 경우 findall보다 finditer가 메모리 효율적입니다 (전체 리스트를 만들지 않고 하나씩 순회).
10. 실전 예제 모음
10-1. 이메일 형식 검사
EMAIL_RE = re.compile(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$")
EMAIL_RE.fullmatch("
[email protected]") # Match
10-2. 휴대폰 번호 추출 및 마스킹
text = "담당자 연락처: 010-1234-5678 / 예비: 010-9999-0000"
numbers = re.findall(r"01\d-\d{3,4}-\d{4}", text)
masked = re.sub(r"(01\d)-(\d{3,4})-(\d{4})", r"\1-****-\3", text)
10-3. URL 추출
url_pattern = re.compile(r"https?://[\w./?=&%#-]+")
url_pattern.findall("자세한 내용은 https://www.nextfinup.com/news/1 참고, 또는 http://old.site")
10-4. 주민등록번호 뒷자리 마스킹 (개인정보 보호)
text = "본인확인용: 900101-1234567"
masked = re.sub(r"(\d{6})-(\d{7})", lambda m: f"{m.group(1)}-{m.group(2)[0]}******", text)
# '본인확인용: 900101-1******'
10-5. 로그 파일에서 특정 필드 파싱
log_line = '2026-08-27 09:12:33 [ERROR] articles.views.news - 스크래핑 실패: timeout'
pattern = re.compile(
r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) "
r"\[(?P<level>\w+)\] (?P<logger>\S+) - (?P<message>.+)"
)
m = pattern.match(log_line)
if m:
print(m.groupdict())
# {'date': '2026-08-27', 'time': '09:12:33', 'level': 'ERROR',
# 'logger': 'articles.views.news', 'message': '스크래핑 실패: timeout'}
10-6. HTML 태그 간단히 제거(순수 텍스트만 추출)
html = "<p>안녕하세요 <b>NextFinUp</b> 입니다.</p>"
plain = re.sub(r"<[^>]+>", "", html)
print(plain) # 안녕하세요 NextFinUp 입니다.
위처럼 태그를 지우는 정도는 괜찮지만, 속성값 안에 >가 들어간 복잡한 HTML은 정규식만으로는 100% 안전하게 처리하기 어렵습니다. 신뢰할 수 없는 입력이라면 전용 HTML 파서를 사용하세요.
10-7. 여러 공백/탭/줄바꿈을 하나의 공백으로 정리
messy = "여러 공백과\t탭,\n줄바꿈이 섞인 문자열"
clean = re.sub(r"\s+", " ", messy).strip()
print(clean) # 여러 공백과 탭, 줄바꿈이 섞인 문자열
10-8. 카멜케이스 → 스네이크케이스 변환
def camel_to_snake(name: str) -> str:
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
camel_to_snake("StockDailyPrice") # 'stock_daily_price'
10-9. 숫자에 3자리마다 콤마 넣기 (정규식 + 룩어헤드)
def add_comma(num: str) -> str:
return re.sub(r"(?<!^)(?=(\d{3})+$)", ",", num)
add_comma("1234567") # '1,234,567'
10-10. 문자열 안의 숫자만 모두 정수로 변환해 합산
text = "1분기 매출 1200, 2분기 매출 3400, 3분기 매출 2900"
total = sum(int(n) for n in re.findall(r"\d+", text))
print(total) # 7500
11. 자주 하는 실수 체크리스트
raw string을 안 씀 → r"\d+"가 아니라 "\d+"로 써서 이스케이프 충돌 발생
.이 모든 문자를 매치한다고 착각 → 기본적으로 줄바꿈(\n)은 매치 안 함 (필요하면 re.DOTALL)
^, $가 항상 "전체 문자열"의 시작/끝이라고 착각 → re.MULTILINE을 켜면 "각 줄"의 시작/끝으로 바뀜
탐욕적 매칭으로 원치 않게 너무 길게 매치됨 → .+ 대신 .+?(게으른) 또는 더 구체적인 문자 클래스([^"]+) 사용
findall에 그룹이 있을 때 전체 매치가 아니라 그룹 값만 반환된다는 것을 몰라 결과 형태에 당황 → finditer + .group(0)을 쓰거나 비캡처 그룹 (?:...) 사용
HTML/XML을 정규식만으로 완벽 파싱하려 함 → 전용 파서(BeautifulSoup 등) 사용
복잡한 중첩 반복 패턴으로 catastrophic backtracking 유발 → 문자 클래스를 구체화하거나 서드파티 regex/re2 계열 검토
매번 같은 패턴을 반복 컴파일 → 반복 호출되는 코드에서는 re.compile로 캐싱
한글 처리 시 \w가 한글을 포함한다는 걸 모르고 별도로 [가-힣]을 추가해 중복/누락 발생 → 파이썬 3의 \w는 기본적으로 한글 음절을 포함(유니코드 모드)
12. 치트시트 요약표
. 임의의 한 문자(줄바꿈 제외)
^ 문자열/줄의 시작
$ 문자열/줄의 끝
* 0회 이상
+ 1회 이상
? 0회 또는 1회 (수량자 뒤에 붙으면 게으른 매칭)
{m,n} m회 이상 n회 이하
[] 문자 클래스
[^] 부정 문자 클래스
| OR
() 캡처 그룹
(?:) 비캡처 그룹
(?P<n>) 이름 있는 그룹
\1 1번 그룹 백레퍼런스
(?=) 긍정 룩어헤드
(?!) 부정 룩어헤드
(?<=) 긍정 룩비하인드
(?<!) 부정 룩비하인드
\d \D 숫자 / 숫자 아님
\w \W 단어문자 / 단어문자 아님
\s \S 공백 / 공백 아님
\b \B 단어 경계 / 단어 경계 아님
\A \Z 문자열의 시작 / 끝
마무리
정규표현식은 처음엔 암호처럼 보이지만, "리터럴 → 문자 클래스 → 수량자 → 앵커 → 그룹 → 룩어라운드" 순서로 하나씩 익히면 금방 손에 익습니다. 실무에서는 regex101.com 같은 온라인 테스터로 패턴을 시각적으로 검증하면서 작성하는 것을 추천합니다. 복잡한 패턴에는 re.VERBOSE와 주석을 적극 활용해서, 나중에 코드를 다시 봤을 때도 바로 이해할 수 있도록 작성하는 습관을 들이세요.