security
CHAPTER 74 / 84
읽기 약 2분
FUNCTION
정규식 마스터: 로그 파싱
핵심 개념
명명 그룹·멀티라인 매칭으로 Apache Combined 로그 한 줄을 완전 파싱한다.
본문
명명 그룹 — 가독성 + 추출
# ⚠️ 이 코드는 허가된 환경에서만 사용하세요.
import re
# 기존 — 인덱스 기반 (가독성 ↓)
pattern = re.compile(r'(\S+) (\S+) (\S+) \[([^\]]+)\]')
m = pattern.match('192.168.1.1 - - [27/Apr/2026:10:23:11 +0000]')
ip = m.group(1) # 무엇인지 모름
# 명명 그룹 — 명확
pattern = re.compile(r'(?P<ip>\S+) (?P<ident>\S+) (?P<user>\S+) \[(?P<time>[^\]]+)\]')
m = pattern.match('192.168.1.1 - - [27/Apr/2026:10:23:11 +0000]')
print(m.group('ip')) # 192.168.1.1
print(m.groupdict()) # {'ip': '...', 'time': '...'}Apache Combined 완전 파싱
# Apache/Nginx 표준 형식
APACHE_COMBINED = re.compile(
r'^(?P<ip>\S+)\s+' # IP
r'(?P<ident>\S+)\s+' # ident
r'(?P<user>\S+)\s+' # 인증된 사용자
r'\[(?P<time>[^\]]+)\]\s+' # 시간
r'"(?P<method>\S+)\s+(?P<path>\S+)\s+(?P<protocol>[^"]+)"\s+' # 요청
r'(?P<status>\d{3})\s+' # 상태 코드
r'(?P<bytes>\S+)\s+' # 응답 크기
r'"(?P<referer>[^"]*)"\s+' # Referer
r'"(?P<user_agent>[^"]*)"' # User-Agent
)
def parse_apache_line(line: str) -> dict | None:
m = APACHE_COMBINED.match(line)
if m:
d = m.groupdict()
# 타입 변환
d['status'] = int(d['status'])
d['bytes'] = 0 if d['bytes'] == '-' else int(d['bytes'])
return d
return None
# 테스트
sample = '192.168.1.10 - - [27/Apr/2026:10:23:11 +0000] "GET /admin HTTP/1.1" 200 1234 "-" "Mozilla/5.0"'
print(parse_apache_line(sample))
# {'ip': '192.168.1.10', 'method': 'GET', 'path': '/admin', 'status': 200, ...}시간 파싱 — datetime 변환
from datetime import datetime
def parse_apache_time(time_str: str) -> datetime:
"""27/Apr/2026:10:23:11 +0000 → datetime"""
# %z는 +0000 같은 타임존
return datetime.strptime(time_str, '%d/%b/%Y:%H:%M:%S %z')
t = parse_apache_time('27/Apr/2026:10:23:11 +0000')
print(t.isoformat()) # 2026-04-27T10:23:11+00:00멀티라인 매칭 — 스택 트레이스
# 일부 로그는 여러 줄에 걸쳐 한 이벤트
ERROR_LOG = """
2026-04-27 10:23:11 [ERROR] api.payment: Charge failed
Traceback (most recent call last):
File "/app/payment.py", line 42, in charge
response = stripe.Charge.create(...)
File "/usr/lib/python/stripe/api_resources.py", line 89
raise StripeError(...)
stripe.error.CardError: Your card was declined.
2026-04-27 10:23:15 [INFO] api.auth: Login attempt user=admin
"""
def parse_multiline_errors(text: str) -> list[dict]:
"""멀티라인 ERROR 블록을 하나의 이벤트로 묶기."""
pattern = re.compile(
r'^(?P<date>\d{4}-\d{2}-\d{2})\s+'
r'(?P<time>\d{2}:\d{2}:\d{2})\s+'
r'\[(?P<level>ERROR|WARNING|INFO|DEBUG|CRITICAL)\]\s+'
r'(?P<logger>\S+):\s+'
r'(?P<message>.+)',
re.MULTILINE,
)
events = []
matches = list(pattern.finditer(text))
for i, m in enumerate(matches):
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
block = text[m.start():end]
d = m.groupdict()
d['full_block'] = block
d['has_traceback'] = 'Traceback' in block
events.append(d)
return events1만 줄 로그에서 패턴 추출
import re
from collections import Counter
def fast_log_summary(log_path: str) -> dict:
"""대용량 로그 빠른 통계 — 한 번 순회로 모든 지표 수집."""
ip_counter = Counter()
status_counter = Counter()
path_counter = Counter()
error_count = 0
total = 0
with open(log_path, encoding='utf-8', errors='replace') as f:
for line in f:
total += 1
m = APACHE_COMBINED.match(line)
if not m:
continue
ip_counter[m.group('ip')] += 1
status = int(m.group('status'))
status_counter[status] += 1
path_counter[m.group('path')] += 1
if status >= 500:
error_count += 1
return {
'total_requests': total,
'top_ips': ip_counter.most_common(10),
'top_paths': path_counter.most_common(10),
'status_distribution': dict(status_counter),
'server_errors': error_count,
}정규식 성능 팁
# ❌ 매번 컴파일 (느림)
def slow_check(line):
return re.match(r'(\d{4}-\d{2}-\d{2})', line)
# ✅ 한 번만 컴파일
PATTERN = re.compile(r'(\d{4}-\d{2}-\d{2})')
def fast_check(line):
return PATTERN.match(line)
# 1만 줄 처리 시: 슬로우 5초 vs 패스트 0.5초ReDoS 방어 — 사용자 입력 정규식
사용자가 정규식을 입력하는 도구라면 timeout 필수:
# pip install regex (re의 timeout 지원 대체)
import regex
def safe_search(pattern: str, text: str, timeout: float = 1.0):
"""ReDoS 방어 — 1초 안에 못 찾으면 포기."""
try:
return regex.search(pattern, text, timeout=timeout)
except TimeoutError:
return NoneAI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 로그 파싱 정규식의 명명 그룹·예외 케이스·성능을 분석하고 ReDoS 안전한 형태로 개선해줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
Apache vs Nginx vs JSON 로그의 파싱 코드 비교 + 각각의 장단점을 실전 코드로 보여줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 1GB+ 웹 로그 전체를 분석해서 시간대·IP·경로·상태 코드 분포를 시각화 가능한 통계 리포트로 만들어줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 로그 파싱 트렌드 — 정규식 기반 vs ML 기반 NLP 파서 1인 SaaS에 적합한 방법을 솔직히 알려줘.
⭐ 이것만 기억하세요
정규식 마스터: 로그 파싱은 이 3가지만 확실히 잡으세요
1.명명 그룹(?P<name>...) + 컴파일된 정규식으로 Apache Combined 로그 1만 줄을 1초 안에 파싱할 수 있다
2.datetime.strptime + Counter로 시간대/IP/경로 통계를 한 번의 순회로 만든다
3.다음 챕터에서 이 파싱 결과로 브루트포스 공격을 자동 탐지한다
공유하기
진행도 74 / 84