跳至正文

정규표현식

在Python中 정규표현식(Regular Expression)을 사용하는 방법讓我們學習. 정규표현식은 문자열 패턴을 찾고 조작하는 강력한 도구입니다.

정규표현식이란? 🔍

정규표현식(regex)은 특정 패턴의 문자열을 검색하고 조작하기 위한 형식 언어입니다.

import re

# 간단한 예제
text = '내 전화번호는 010-1234-5678입니다.'
pattern = r'\d{3}-\d{4}-\d{4}'

match = re.search(pattern, text)
if match:
print(match.group()) # 010-1234-5678

re 모듈 기본 함수

match(): 문자열 시작 부분 매칭

import re

text = 'Python is great'

# 시작 부분이 매칭되면 Match 객체 반환
match = re.match(r'Python', text)
if match:
print('매칭됨:', match.group()) # Python

# 시작 부분이 매칭되지 않으면 None
match = re.match(r'Java', text)
if match:
print('매칭됨')
else:
print('매칭 안됨')

search(): 문자열 전체에서 첫 번째 매칭

import re

text = 'I love Python programming'

# 문자열 어디든 매칭되면 Match 객체 반환
match = re.search(r'Python', text)
if match:
print('찾음:', match.group()) # Python
print('위치:', match.start(), '-', match.end()) # 7 - 13

findall(): 모든 매칭 찾기

import re

text = '전화번호: 010-1234-5678, 010-9876-5432'
pattern = r'\d{3}-\d{4}-\d{4}'

# 모든 매칭을 리스트로 반환
matches = re.findall(pattern, text)
print(matches) # ['010-1234-5678', '010-9876-5432']

finditer(): 모든 매칭을 iterator로

import re

text = '가격: 1000원, 2000원, 3000원'
pattern = r'\d+'

# Match 객체를 iterator로 반환
for match in re.finditer(pattern, text):
print(f'{match.group()} (위치: {match.start()}-{match.end()})')
# 1000 (위치: 4-8)
# 2000 (위치: 11-15)
# 3000 (위치: 17-21)

sub(): 패턴 치환

import re

text = '내 전화번호는 010-1234-5678입니다.'
pattern = r'\d{3}-\d{4}-\d{4}'

# 패턴을 다른 문자열로 치환
result = re.sub(pattern, '***-****-****', text)
print(result) # 내 전화번호는 ***-****-****입니다.

# 함수로 치환
def mask_phone(match):
phone = match.group()
return phone[:3] + '-****-' + phone[-4:]

result = re.sub(pattern, mask_phone, text)
print(result) # 내 전화번호는 010-****-5678입니다.

split(): 패턴으로 분리

import re

text = 'apple,banana;orange:grape'

# 여러 구분자로 분리
parts = re.split(r'[,;:]', text)
print(parts) # ['apple', 'banana', 'orange', 'grape']

# 공백으로 분리 (여러 개의 공백 허용)
text2 = 'hello world python'
words = re.split(r'\s+', text2)
print(words) # ['hello', 'world', 'python']

기본 패턴 📝

리터럴 문자

import re

# 정확한 문자열 매칭
print(re.search(r'hello', 'hello world')) # 매칭
print(re.search(r'hello', 'Hello world')) # None (대소문자 구분)

# 대소문자 무시
print(re.search(r'hello', 'Hello world', re.IGNORECASE)) # 매칭

메타 문자

문자의미예제
.임의의 문자 1개a.c → abc, a1c, a c
^문자열 시작^hello → hello로 시작
$문자열 끝world$ → world로 끝남
*0번 이상 반복ab*c → ac, abc, abbc
+1번 이상 반복ab+c → abc, abbc
?0번 또는 1번ab?c → ac, abc
|ORcat|dog → cat 또는 dog
()그룹(ab)+ → ab, abab
[]문자 클래스[abc] → a, b, c 중 하나
{}반복 횟수a{3} → aaa
import re

# 점(.)은 줄바꿈 제외한 모든 문자
print(re.search(r'a.c', 'abc').group()) # abc
print(re.search(r'a.c', 'a1c').group()) # a1c

# 별표(*): 0번 이상
print(re.search(r'ab*c', 'ac').group()) # ac
print(re.search(r'ab*c', 'abc').group()) # abc
print(re.search(r'ab*c', 'abbc').group()) # abbc

# 플러스(+): 1번 이상
print(re.search(r'ab+c', 'abc').group()) # abc
print(re.search(r'ab+c', 'ac')) # None

# 물음표(?): 0번 또는 1번
print(re.search(r'colou?r', 'color').group()) # color
print(re.search(r'colou?r', 'colour').group()) # colour

문자 클래스

import re

# 문자 집합
print(re.findall(r'[aeiou]', 'hello')) # ['e', 'o']

# 범위
print(re.findall(r'[a-z]', 'Hello123')) # ['e', 'l', 'l', 'o']
print(re.findall(r'[A-Z]', 'Hello123')) # ['H']
print(re.findall(r'[0-9]', 'Hello123')) # ['1', '2', '3']

# 부정 (^)
print(re.findall(r'[^0-9]', 'Hello123')) # ['H', 'e', 'l', 'l', 'o']

# 특수 문자 클래스
print(re.findall(r'\d', 'abc123')) # ['1', '2', '3'] (숫자)
print(re.findall(r'\D', 'abc123')) # ['a', 'b', 'c'] (비숫자)
print(re.findall(r'\w', 'a_1 !')) # ['a', '_', '1'] (단어 문자)
print(re.findall(r'\W', 'a_1 !')) # [' ', '!'] (비단어 문자)
print(re.findall(r'\s', 'a b\tc')) # [' ', '\t'] (공백)
print(re.findall(r'\S', 'a b')) # ['a', 'b'] (비공백)

반복 지정

import re

# {n}: 정확히 n번
print(re.search(r'a{3}', 'aaa').group()) # aaa
print(re.search(r'a{3}', 'aa')) # None

# {n,}: n번 이상
print(re.search(r'a{2,}', 'aaa').group()) # aaa

# {n,m}: n번 이상 m번 이하
print(re.search(r'a{2,4}', 'aaaaa').group()) # aaaa

# 전화번호 패턴
phone = re.search(r'\d{3}-\d{4}-\d{4}', '010-1234-5678')
print(phone.group()) # 010-1234-5678

그룹과 캡처 🎯

기본 그룹

import re

text = '이름: 홍길동, 나이: 30세'

# 그룹으로 캡처
pattern = r'이름: (\w+), 나이: (\d+)세'
match = re.search(pattern, text)

if match:
print('전체:', match.group(0)) # 이름: 홍길동, 나이: 30세
print('이름:', match.group(1)) # 홍길동
print('나이:', match.group(2)) # 30
print('모든 그룹:', match.groups()) # ('홍길동', '30')

이름 있는 그룹

import re

text = '2025-11-27'
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'

match = re.search(pattern, text)
if match:
print('연도:', match.group('year')) # 2025
print('월:', match.group('month')) # 11
print('일:', match.group('day')) # 27
print('딕셔너리:', match.groupdict()) # {'year': '2025', 'month': '11', 'day': '27'}

비캡처 그룹

import re

# (?:...)는 그룹이지만 캡처하지 않음
text = 'http://www.example.com'
pattern = r'(?:http|https)://(\w+\.\w+\.\w+)'

match = re.search(pattern, text)
if match:
print('전체:', match.group(0)) # http://www.example.com
print('도메인:', match.group(1)) # www.example.com
# group(2)는 없음 (비캡처 그룹)

컴파일과 플래그

패턴 컴파일

import re

# 같은 패턴을 여러 번 사용할 때 컴파일하면 효율적
pattern = re.compile(r'\d{3}-\d{4}-\d{4}')

text1 = '전화: 010-1234-5678'
text2 = '연락처: 010-9876-5432'

print(pattern.search(text1).group()) # 010-1234-5678
print(pattern.search(text2).group()) # 010-9876-5432

플래그

import re

text = 'Hello\nWorld'

# IGNORECASE: 대소문자 무시
print(re.findall(r'hello', text, re.IGNORECASE)) # ['Hello']

# MULTILINE: ^와 $가 각 줄에 적용
print(re.findall(r'^World', text, re.MULTILINE)) # ['World']

# DOTALL: .이 줄바꿈도 매칭
print(re.search(r'Hello.World', text, re.DOTALL)) # 매칭됨

# VERBOSE: 읽기 쉽게 작성
pattern = re.compile(r'''
\d{3} # 지역번호
- # 하이픈
\d{4} # 국번
- # 하이픈
\d{4} # 번호
''', re.VERBOSE)

print(pattern.search('010-1234-5678').group()) # 010-1234-5678

實踐範例 💡

예제 1: 이메일 검증

import re

def validate_email(email):
"""이메일 주소 검증"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None

# 테스트
emails = [
'user@example.com', # 유효
'user.name@example.co.kr', # 유효
'user+tag@example.com', # 유효
'invalid@', # 무효
'@example.com', # 무효
'user@example', # 무효
]

for email in emails:
result = '✓' if validate_email(email) else '✗'
print(f'{result} {email}')

예제 2: 전화번호 검증 및 포맷팅

import re

class PhoneValidator:
"""전화번호 검증 및 포맷팅"""

# 다양한 형식의 전화번호 패턴
PATTERNS = [
r'010-\d{4}-\d{4}', # 010-1234-5678
r'010\d{8}', # 01012345678
r'\d{2,3}-\d{3,4}-\d{4}', # 02-1234-5678, 031-123-4567
]

@classmethod
def extract_numbers(cls, phone):
"""전화번호에서 숫자만 추출"""
return re.sub(r'\D', '', phone)

@classmethod
def format_phone(cls, phone):
"""전화번호 포맷팅 (010-1234-5678 형식)"""
numbers = cls.extract_numbers(phone)

if len(numbers) == 11 and numbers.startswith('010'):
return f'{numbers[:3]}-{numbers[3:7]}-{numbers[7:]}'
elif len(numbers) == 10:
if numbers.startswith('02'):
return f'{numbers[:2]}-{numbers[2:6]}-{numbers[6:]}'
else:
return f'{numbers[:3]}-{numbers[3:6]}-{numbers[6:]}'

return phone # 포맷팅 불가

@classmethod
def validate(cls, phone):
"""전화번호 유효성 검증"""
for pattern in cls.PATTERNS:
if re.match(pattern, phone):
return True
return False

# 사용 예제
phones = [
'010-1234-5678',
'01012345678',
'02-1234-5678',
'031-123-4567',
]

for phone in phones:
formatted = PhoneValidator.format_phone(phone)
is_valid = PhoneValidator.validate(phone)
print(f'{phone}{formatted} (유효: {is_valid})')

예제 3: URL 파싱

import re

def parse_url(url):
"""URL을 구성요소로 분해"""
pattern = r'''
(?P<protocol>https?://)? # 프로토콜 (선택)
(?P<subdomain>[\w-]+\.)* # 서브도메인 (선택, 반복)
(?P<domain>[\w-]+) # 도메인
\.(?P<tld>[a-z]{2,}) # 최상위 도메인
(?::(?P<port>\d+))? # 포트 (선택)
(?P<path>/[^\s?]*)? # 경로 (선택)
(?:\?(?P<query>[^\s#]*))? # 쿼리 (선택)
(?:\#(?P<fragment>[^\s]*))? # 프래그먼트 (선택)
'''

match = re.match(pattern, url, re.VERBOSE)
if match:
return match.groupdict()
return None

# 테스트
urls = [
'https://www.example.com:8080/path/to/page?key=value#section',
'http://blog.example.co.kr/posts',
'example.com/page',
]

for url in urls:
print(f'\nURL: {url}')
parts = parse_url(url)
if parts:
for key, value in parts.items():
if value:
print(f' {key}: {value}')

예제 4: 로그 파일 파싱

import re
from datetime import datetime

class LogParser:
"""로그 파일 파서"""

# 로그 패턴: [2025-11-27 14:30:45] ERROR: Something went wrong
LOG_PATTERN = r'\[(?P<timestamp>[\d-]+ [\d:]+)\] (?P<level>\w+): (?P<message>.*)'

def __init__(self, log_file):
self.log_file = log_file
self.pattern = re.compile(self.LOG_PATTERN)

def parse_line(self, line):
"""한 줄 파싱"""
match = self.pattern.match(line)
if match:
data = match.groupdict()
data['timestamp'] = datetime.strptime(
data['timestamp'],
'%Y-%m-%d %H:%M:%S'
)
return data
return None

def parse_file(self):
"""전체 파일 파싱"""
logs = []
with open(self.log_file, 'r', encoding='utf-8') as f:
for line in f:
parsed = self.parse_line(line.strip())
if parsed:
logs.append(parsed)
return logs

def filter_by_level(self, level):
"""특정 레벨의 로그만 필터링"""
logs = self.parse_file()
return [log for log in logs if log['level'] == level]

# 샘플 로그 생성
sample_logs = [
'[2025-11-27 14:30:45] INFO: Server started',
'[2025-11-27 14:30:46] ERROR: Connection failed',
'[2025-11-27 14:30:47] WARNING: Memory usage high',
'[2025-11-27 14:30:48] ERROR: Database timeout',
]

with open('app.log', 'w', encoding='utf-8') as f:
f.write('\n'.join(sample_logs))

# 파싱
parser = LogParser('app.log')
errors = parser.filter_by_level('ERROR')

print(f'총 {len(errors)}개의 에러 발견:')
for error in errors:
print(f" {error['timestamp']}: {error['message']}")

예제 5: 비밀번호 강도 검증

import re

class PasswordValidator:
"""비밀번호 강도 검증기"""

def __init__(self, min_length=8):
self.min_length = min_length

def validate(self, password):
"""비밀번호 검증"""
checks = {
'length': len(password) >= self.min_length,
'uppercase': bool(re.search(r'[A-Z]', password)),
'lowercase': bool(re.search(r'[a-z]', password)),
'digit': bool(re.search(r'\d', password)),
'special': bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)),
}

return checks

def get_strength(self, password):
"""비밀번호 강도 계산"""
checks = self.validate(password)
score = sum(checks.values())

if score == 5:
return '매우 강함'
elif score >= 4:
return '강함'
elif score >= 3:
return '보통'
else:
return '약함'

def get_feedback(self, password):
"""비밀번호 개선 제안"""
checks = self.validate(password)
feedback = []

if not checks['length']:
feedback.append(f'최소 {self.min_length}자 이상이어야 합니다')
if not checks['uppercase']:
feedback.append('대문자를 포함해야 합니다')
if not checks['lowercase']:
feedback.append('소문자를 포함해야 합니다')
if not checks['digit']:
feedback.append('숫자를 포함해야 합니다')
if not checks['special']:
feedback.append('특수문자를 포함해야 합니다')

return feedback

# 사용 예제
validator = PasswordValidator()

passwords = [
'password',
'Password1',
'Password1!',
'P@ssw0rd',
]

for pwd in passwords:
strength = validator.get_strength(pwd)
feedback = validator.get_feedback(pwd)

print(f'\n비밀번호: {pwd}')
print(f'강도: {strength}')
if feedback:
print('개선사항:')
for fb in feedback:
print(f' - {fb}')

예제 6: HTML 태그 제거

import re

def remove_html_tags(text):
"""HTML 태그 제거"""
# <...> 형식의 태그 제거
clean = re.sub(r'<[^>]+>', '', text)
# HTML 엔티티 변환
clean = re.sub(r'&nbsp;', ' ', clean)
clean = re.sub(r'&lt;', '<', clean)
clean = re.sub(r'&gt;', '>', clean)
clean = re.sub(r'&amp;', '&', clean)
return clean.strip()

# 테스트
html = '''
<div class="content">
<h1>제목</h1>
<p>이것은 <strong>중요한</strong> 내용입니다.</p>
<a href="http://example.com">링크</a>
</div>
'''

clean_text = remove_html_tags(html)
print(clean_text)

예제 7: 텍스트에서 정보 추출

import re

class TextExtractor:
"""텍스트에서 다양한 정보 추출"""

@staticmethod
def extract_emails(text):
"""이메일 주소 추출"""
pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
return re.findall(pattern, text)

@staticmethod
def extract_urls(text):
"""URL 추출"""
pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
return re.findall(pattern, text)

@staticmethod
def extract_phone_numbers(text):
"""전화번호 추출"""
pattern = r'(\d{2,3}[-.]?\d{3,4}[-.]?\d{4})'
return re.findall(pattern, text)

@staticmethod
def extract_dates(text):
"""날짜 추출 (YYYY-MM-DD, YYYY/MM/DD)"""
pattern = r'\d{4}[-/]\d{2}[-/]\d{2}'
return re.findall(pattern, text)

@staticmethod
def extract_hashtags(text):
"""해시태그 추출"""
pattern = r'#\w+'
return re.findall(pattern, text)

# 사용 예제
text = '''
연락처: user@example.com, 010-1234-5678
웹사이트: https://www.example.com
날짜: 2025-11-27
소셜: #python #regex #coding
'''

extractor = TextExtractor()

print('이메일:', extractor.extract_emails(text))
print('URL:', extractor.extract_urls(text))
print('전화번호:', extractor.extract_phone_numbers(text))
print('날짜:', extractor.extract_dates(text))
print('해시태그:', extractor.extract_hashtags(text))

예제 8: 문자열 정규화

import re

class TextNormalizer:
"""텍스트 정규화"""

@staticmethod
def normalize_whitespace(text):
"""공백 정규화 (여러 공백을 하나로)"""
return re.sub(r'\s+', ' ', text).strip()

@staticmethod
def remove_special_chars(text):
"""특수문자 제거 (알파벳, 숫자, 공백만 유지)"""
return re.sub(r'[^a-zA-Z0-9가-힣\s]', '', text)

@staticmethod
def normalize_phone(phone):
"""전화번호 정규화"""
# 숫자만 추출
numbers = re.sub(r'\D', '', phone)

# 11자리인 경우 (010-xxxx-xxxx)
if len(numbers) == 11:
return f'{numbers[:3]}-{numbers[3:7]}-{numbers[7:]}'

return phone

@staticmethod
def normalize_email(email):
"""이메일 정규화 (소문자 변환)"""
email = email.lower().strip()
# 공백 제거
email = re.sub(r'\s', '', email)
return email

# 사용 예제
normalizer = TextNormalizer()

print(normalizer.normalize_whitespace('hello world !'))
# 'hello world !'

print(normalizer.remove_special_chars('Hello, World! 123'))
# 'Hello World 123'

print(normalizer.normalize_phone('010 1234 5678'))
# '010-1234-5678'

print(normalizer.normalize_email(' User@Example.COM '))
# 'user@example.com'

예제 9: 민감한 정보 마스킹

import re

class DataMasker:
"""민감한 정보 마스킹"""

@staticmethod
def mask_email(email):
"""이메일 마스킹: u***@example.com"""
pattern = r'([a-zA-Z])[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})'
return re.sub(pattern, r'\1***\2', email)

@staticmethod
def mask_phone(phone):
"""전화번호 마스킹: 010-****-5678"""
pattern = r'(\d{2,3})[-.]?(\d{3,4})[-.]?(\d{4})'
return re.sub(pattern, r'\1-****-\3', phone)

@staticmethod
def mask_card(card_number):
"""카드번호 마스킹: ****-****-****-1234"""
pattern = r'(\d{4})[-\s]?(\d{4})[-\s]?(\d{4})[-\s]?(\d{4})'
return re.sub(pattern, r'****-****-****-\4', card_number)

@staticmethod
def mask_ssn(ssn):
"""주민번호 마스킹: 123456-*******"""
pattern = r'(\d{6})[-]?(\d{7})'
return re.sub(pattern, r'\1-*******', ssn)

# 사용 예제
masker = DataMasker()

print(masker.mask_email('user123@example.com'))
# u***@example.com

print(masker.mask_phone('010-1234-5678'))
# 010-****-5678

print(masker.mask_card('1234-5678-9012-3456'))
# ****-****-****-3456

print(masker.mask_ssn('123456-1234567'))
# 123456-*******

예제 10: 코드 하이라이팅 (간단한 예제)

import re

class SimpleHighlighter:
"""간단한 Python 코드 하이라이터"""

PATTERNS = {
'keyword': r'\b(def|class|if|else|elif|for|while|return|import|from)\b',
'string': r'(["\'])(?:(?=(\\?))\2.)*?\1',
'number': r'\b\d+\b',
'comment': r'#.*$',
'function': r'\b([a-zA-Z_]\w*)\s*(?=\()',
}

def highlight(self, code):
"""코드를 HTML로 하이라이팅"""
highlighted = code

# 키워드
highlighted = re.sub(
self.PATTERNS['keyword'],
r'<span class="keyword">\1</span>',
highlighted
)

# 문자열
highlighted = re.sub(
self.PATTERNS['string'],
r'<span class="string">\g<0></span>',
highlighted
)

# 숫자
highlighted = re.sub(
self.PATTERNS['number'],
r'<span class="number">\g<0></span>',
highlighted
)

return highlighted

# 사용 예제
highlighter = SimpleHighlighter()
code = 'def hello(name): return "Hello, " + name'
print(highlighter.highlight(code))

常見問題 ❓

Q1: 탐욕적(greedy) vs 비탐욕적(non-greedy) 매칭은?

import re

text = '<div>content1</div><div>content2</div>'

# 탐욕적: 가능한 한 많이 매칭
greedy = re.search(r'<div>.*</div>', text).group()
print(greedy)
# <div>content1</div><div>content2</div>

# 비탐욕적: 가능한 한 적게 매칭
non_greedy = re.search(r'<div>.*?</div>', text).group()
print(non_greedy)
# <div>content1</div>

Q2: 역슬래시를 매칭하려면?

import re

# raw string 사용
pattern = r'\\'
print(re.search(pattern, 'a\\b').group()) # \

# 또는 이스케이프
pattern = '\\\\'
print(re.search(pattern, 'a\\b').group()) # \

Q3: 정규표현식을 디버그하는 방법은?

import re

# 패턴이 복잡할 때 부분별로 테스트
pattern = r'\d{3}-\d{4}-\d{4}'

# 단계별 확인
print(re.search(r'\d{3}', '010')) # 010
print(re.search(r'\d{3}-', '010-')) # 010-
print(re.search(r'\d{3}-\d{4}', '010-1234')) # 010-1234

# 온라인 도구 사용: regex101.com, regexr.com

Q4: 같은 패턴이 여러 번 나올 때 어떻게 처리하나요?

import re

text = 'Price: 1000원, 2000원, 3000원'

# findall로 모두 찾기
prices = re.findall(r'\d+', text)
print(prices) # ['1000', '2000', '3000']

# finditer로 위치 정보와 함께
for match in re.finditer(r'\d+', text):
print(f'{match.group()} at {match.start()}-{match.end()}')

Q5: 정규표현식 성능을 개선하는 방법은?

import re

# 1. 패턴 컴파일 (반복 사용 시)
pattern = re.compile(r'\d+')
for text in texts:
pattern.search(text)

# 2. 비캡처 그룹 사용
# 나쁨: r'(http|https)://...'
# 좋음: r'(?:http|https)://...'

# 3. 탐욕적 매칭 제한
# 나쁨: r'.*'
# 좋음: r'[^>]*' 또는 r'.*?'

정규표현식 치트 시트 📋

자주 사용하는 패턴

patterns = {
'이메일': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
'전화번호': r'^\d{3}-\d{3,4}-\d{4}$',
'URL': r'^https?://[^\s]+$',
'IP주소': r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$',
'날짜(YYYY-MM-DD)': r'^\d{4}-\d{2}-\d{2}$',
'시간(HH:MM)': r'^([01]\d|2[0-3]):([0-5]\d)$',
'한글': r'[가-힣]+',
'영문': r'[a-zA-Z]+',
'숫자': r'\d+',
'공백제거': r'\s+',
}

下一步

  • 파일 입출력: 로그 파일 파싱에 정규표현식 활용
  • JSON과 CSV: 데이터 검증에 정규표현식 사용
  • 웹 스크래핑: BeautifulSoup과 정규표현식 조합
  • 고급 정규표현식: lookahead, lookbehind 패턴