정규표현식
在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']}")