import argparse
import json
from collections import Counter
from datetime import datetime
from typing import Dict, List, Optional, Tuple, Union
def parse_args() -> argparse.Namespace:
"""명령줄 인자를 파싱합니다."""
parser = argparse.ArgumentParser(description="웹 서버 접속 로그를 집계합니다.")
parser.add_argument(
"path",
nargs="?",
default="access.log",
help="로그 파일 경로 (기본값: access.log)",
)
return parser.parse_args()
def parse_line(line: str) -> Optional[Tuple[int, str, int, int]]:
"""한 줄을 파싱합니다. 형식이 맞지 않으면 None을 반환합니다."""
fields = line.split()
# 열 개수가 정확히 6개인지 확인
if len(fields) != 6:
return None
# 상태코드와 응답시간이 정수인지 확인
try:
status = int(fields[4])
response_ms = int(fields[5])
except ValueError:
return None
# ISO 8601 시각에서 시간대(0~23) 추출
hour = datetime.fromisoformat(fields[0]).hour
return hour, fields[1], status, response_ms
def process_file(path: str) -> Tuple[Dict[str, int], Counter, int, int, int]:
"""로그 파일을 읽어 시간대별 요청 수, IP별 요청 수, 오류 수, 전체 수, 건너뛴 수를 계산합니다."""
hourly: Dict[str, int] = {f"{hour:02d}": 0 for hour in range(24)}
ip_counter: Counter = Counter()
total = 0
error_count = 0
skipped = 0
with open(path, "r", encoding="utf-8") as file:
for line in file:
parsed = parse_line(line)
if parsed is None:
skipped += 1
continue
hour, ip, status, _ = parsed
hourly[f"{hour:02d}"] += 1
ip_counter[ip] += 1
total += 1
if status >= 500:
error_count += 1
return hourly, ip_counter, error_count, total, skipped
def build_result(
hourly: Dict[str, int],
ip_counter: Counter,
error_count: int,
total: int,
skipped: int,
) -> Dict[str, object]:
"""최종 JSON 결과를 만듭니다."""
top_ips: List[List[Union[str, int]]] = [
[ip, count]
for ip, count in sorted(
ip_counter.items(),
key=lambda item: (-item[1], item[0]),
)[:5]
]
error_rate = round((error_count / total * 100), 1) if total else 0.0
return {
"hourly": hourly,
"top_ips": top_ips,
"error_rate": error_rate,
"skipped": skipped,
}
def main() -> None:
"""프로그램 진입점입니다."""
args = parse_args()
hourly, ip_counter, error_count, total, skipped = process_file(args.path)
result = build_result(hourly, ip_counter, error_count, total, skipped)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
다음과 같이 실행하시면 됩니다: python script.py access.log
경로를 생략하시면 기본값인 access.log를 사용합니다.