정규표현식
在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 |
| | OR | cat|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' ', ' ', clean)
clean = re.sub(r'<', '<', clean)
clean = re.sub(r'>', '>', clean)
clean = re.sub(r'&', '&', 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-*******