OPEN HYPER STEP
← 목록으로 (화이트햇 보안)
SECURITY · 74 / 84
security
CHAPTER 74 / 84
읽기 약 2
FUNCTION

정규식 마스터: 로그 파싱


핵심 개념

명명 그룹·멀티라인 매칭으로 Apache Combined 로그 한 줄을 완전 파싱한다.

본문

명명 그룹 — 가독성 + 추출

PYTHON📋 코드 (13줄)
# ⚠️ 이 코드는 허가된 환경에서만 사용하세요.
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 완전 파싱

PYTHON📋 코드 (27줄)
# 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 변환

PYTHON📋 코드 (9줄)
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

멀티라인 매칭 — 스택 트레이스

PYTHON📋 코드 (33줄)
# 일부 로그는 여러 줄에 걸쳐 한 이벤트
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 events

1만 줄 로그에서 패턴 추출

PYTHON📋 코드 (31줄)
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,
    }

정규식 성능 팁

PYTHON📋 코드 (10줄)
# ❌ 매번 컴파일 (느림)
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 필수:

PYTHON📋 코드 (9줄)
# 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 None

AI 프롬프트
🤖 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