2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Postfixのログからcsvを生成

Last updated at Posted at 2024-03-04
extract_postfix_maillog_sub.py
import re
import csv
from datetime import datetime
import argparse
from pathlib import Path
import logging
import socket
import os
import sys

#===============================================================================
# ロギング設定
#===============================================================================
def setup_logging():
    host_name = socket.gethostname()
    program_name = os.path.basename(__file__)
    log_file_path = Path(__file__).resolve().parent.parent / 'log' / 'pythonlog.log'
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
    file_handler.setFormatter(logging.Formatter(f'%(asctime)s {host_name} [{program_name}:%(levelname)s] [%(process)d] %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
    stream_handler = logging.StreamHandler(sys.stdout)
    stream_handler.setFormatter(logging.Formatter(f'%(asctime)s {host_name} [{program_name}:%(levelname)s] [%(process)d] %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)

#===============================================================================
# 引数の解析
#===============================================================================
def parse_arguments():
    parser = argparse.ArgumentParser(description="メールログファイルを解析して、特定の条件に基づいてフィルタリングし、結果をCSVに出力するスクリプト。")
    parser.add_argument("--log_path", type=str, nargs='+', required=True, help="解析するメールログファイルへのパス。複数指定可能。")
    parser.add_argument("--csv_append", type=str, required=True, help="出力するCSVファイル名に追加するテキスト。")
    parser.add_argument("--csv_path", type=str, help="CSVファイルの保存先ディレクトリのパス。デフォルトはこのスクリプトのディレクトリ。", default=Path(__file__).parent)
    parser.add_argument("--srv_type", type=str, required=True, choices=['ms', 'mr'], help="サーバの種類。[ms/mr]")
    return parser.parse_args()

#===============================================================================
# ユーティリティ関数
#===============================================================================
def extract_data_from_line_ms(line):
    """
    フィルタリングされた行から必要な情報を抽出し、抽出されたデータを返します。
    """
    try:
        date = extract_date_ms(line)
        ms_hostname = extract_mshost_ms(line)
        from_hostname = extract_find_pattern_common(line, "from ", " (")
        from_ipaddress = extract_from_ipaddress_ms(line)
        from_address = extract_find_pattern_common(line, "from=<", ">")
        to_address = extract_find_pattern_common(line, "to=<", ">")
        to_domain = "@" + to_address.split('@')[1] if '@' in to_address else ""
        esmtp_id1, esmtp_id2 = extract_esmtp_ids_ms(line)
        return [date, ms_hostname, from_hostname, from_ipaddress, from_address, to_domain, esmtp_id1, esmtp_id2]
    except Exception as e:
        logging.error(f"関数: extract_data_from_line_ms: {e}")
        return None

def extract_data_from_line_mr(line):
    """
    フィルタリングされた行から必要な情報を抽出し、抽出されたデータを返します。
    """
    try:
        date = extract_date_mr(line)
        ms_hostname = extract_mshost_mr(line)
        to_address = extract_find_pattern_common(line, "to=<", ">")
        esmtpid = extract_esmtp_ids_mr(line)
        return [date, ms_hostname, to_address, esmtpid]
    except Exception as e:
        logging.error(f"関数: extract_data_from_line_mr: {e}")
        return None

def extract_date_ms(line):
    """
    日付を抽出し、datetimeオブジェクトとして返します。
    """
    date_pattern = re.compile(r'^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}')
    date_match = date_pattern.match(line)
    return datetime.strptime(date_match.group(0), '%Y/%m/%d %H:%M:%S') if date_match else None

def extract_date_mr(line):
    """
    ログ行から日付を抽出し、datetimeオブジェクトとして返します。
    ログの日付フォーマットが 'Mar 4 10:43:31' の形式であることを前提としています。
    年跨ぎの可能性を考慮して、適切な年を日付に設定します。
    """
    # 日付の正規表現パターンを定義します
    date_pattern = re.compile(r'(\w+\s+\d+\s+\d+:\d+:\d+)')
    match = date_pattern.search(line)
    if match:
        log_date_str = match.group(1)
        now = datetime.now()
        year = now.year
        log_date = datetime.strptime(f"{year} {log_date_str}", '%Y %b %d %H:%M:%S')
        if log_date > now:
            log_date = log_date.replace(year=year - 1)
        return log_date.strftime('%Y-%m-%d %H:%M:%S')
    else:
        return None

def extract_mshost_ms(line):
    """
    メールサーバのホスト名を抽出します。
    """
    pattern = r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} (\S+)'
    match = re.search(pattern, line)
    if match:
        ms_hostname = match.group(1)
    return ms_hostname

def extract_mshost_mr(line):
    """
    メールサーバのホスト名を抽出します。
    """
    pattern = r'\w+\s+\d+\s+\d+:\d+:\d+ (\S+)'
    match = re.search(pattern, line)
    if match:
        ms_hostname = match.group(1)
    return ms_hostname

def extract_find_pattern_common(line, start_pattern, end_pattern):
    """
    特定文字列の範囲の文字列を抽出します。
    """
    start = line.find(start_pattern) + len(start_pattern)
    end = line.find(end_pattern, start)
    return line[start:end]

def extract_from_ipaddress_ms(line):
    """
    ipアドレスを抽出します。
    """
    pattern = r"replace: header Received: from [^\s]+ \([^\s]+ \[?(\b(?:\d{1,3}\.){3}\d{1,3}\b)"
    match = re.search(pattern, line)
    return match.group(1)

def extract_esmtp_ids_ms(line):
    """
    ESMTP IDを抽出します。
    """
    esmtpid1_pattern = re.compile(r'with E?SMTP(S)? id (\S+)')
    esmtpid1_match = esmtpid1_pattern.search(line)
    esmtpid1 = esmtpid1_match.group(2).split("??")[0].strip(';') if esmtpid1_match else ""
    esmtpid2_start = line.find(": ") + 2
    esmtpid2_end = line.find(": ", esmtpid2_start)
    esmtpid2 = line[esmtpid2_start:esmtpid2_end]
    return esmtpid1, esmtpid2

def extract_esmtp_ids_mr(line):
    """
    ESMTP IDを抽出します。
    """
    esmtpid_start = line.find(": ") + 2
    esmtpid_end = line.find(": ", esmtpid_start)
    esmtpid = line[esmtpid_start:esmtpid_end]
    return esmtpid

def write_data_to_csv_common(data, csv_file_path, csv_headers):
    """
    抽出したデータをCSVファイルに書き込みます。
    """
    try:
        with open(csv_file_path, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(csv_headers)
            writer.writerows(data)
            logging.info(f"CSVファイル '{csv_file_path}' へのデータ書き込みが完了しました。")
    except Exception as e:
            logging.error(f"関数: write_data_to_csv_ms, CSVファイルへの書き込み中にエラーが発生しました: {csv_file_path}, エラー: {e}")

def filter_log_lines(file_path, include_patterns, exclude_patterns):
    """
    ログファイルから指定されたすべてのフィルターパターンに一致し、
    指定されたすべての除外パターンに一致しない行をフィルタリングします。
    """
    filtered_lines = []
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            for line in file:
                # include_patterns すべてに一致し、exclude_patterns に一致しないかどうかを判定
                if all(pattern.search(line) for pattern in include_patterns) and not any(pattern.search(line) for pattern in exclude_patterns):
                    filtered_lines.append(line)
    except Exception as e:
        logging.error(f"ログファイルのフィルタリング中にエラーが発生しました: {file_path}, エラー: {e}")
    return filtered_lines

def process_log_files(log_file_paths):
    """
    ログファイルを処理してCSVファイルを生成します。
    """
    script_name = Path(__file__).stem
    for log_file_path in log_file_paths:
        log_file_name = Path(log_file_path).stem
        csv_file_name = f"{script_name}_{args.csv_append}_{log_file_name}.csv"
        csv_file_path = Path(args.csv_path) / csv_file_name

        # ログファイルからフィルタリング
        filtered_lines = filter_log_lines(log_file_path, include_patterns, exclude_patterns)
        if not filtered_lines:
            logging.warning(f"ファイル '{log_file_path}' に一致する行がありません。")
            continue

        # フィルタリングされた行からデータを抽出
        if args.srv_type == 'ms':
            all_data = []
            for line in filtered_lines:
                data = extract_data_from_line_ms(line)
                if data is not None:
                    all_data.append(data)
            if not all_data:
                logging.warning(f"ファイル '{log_file_path}' から有効なデータが抽出されませんでした。")
                continue
        elif args.srv_type == 'mr':
            all_data = []
            for line in filtered_lines:
                data = extract_data_from_line_mr(line)
                if data is not None:
                    all_data.append(data)
            if not all_data:
                logging.warning(f"ファイル '{log_file_path}' から有効なデータが抽出されませんでした。")
                continue

        # CSVファイルへの書き込み
        if args.srv_type == 'ms':
            csv_headers = ['Date', 'MS HostName', 'From HostName', 'From IPAddress', 'From:', 'To Domain:', 'ESMTP ID1', 'ESMTP ID2']
            write_data_to_csv_common(all_data, csv_file_path, csv_headers)
            logging.info(f"ファイル '{log_file_path}' の処理が完了しました。")
        elif args.srv_type == 'mr':
            csv_headers = ['Date', 'MS HostName', 'To:', 'ESMTP ID']
            write_data_to_csv_common(all_data, csv_file_path, csv_headers)
            logging.info(f"ファイル '{log_file_path}' の処理が完了しました。")

if __name__ == "__main__":
    setup_logging()
    args = parse_arguments()
    logging.info(f"プログラムを開始します。オプション: {args}")

    if args.srv_type == 'ms':
        try:
            # 複数のフィルターパターンと除外パターンをリストとして定義
            include_patterns = [
                re.compile(r': replace: header Received: from .*'),
                # 他のフィルターパターンをここに追加
            ]
            exclude_patterns = [
                re.compile(r': replace: header Received: from (ws|tp|wp)bkbmp0ms00[1-4]f0'),
                # 他の除外パターンをここに追加
            ]
            process_log_files(args.log_path)
        except Exception as e:
            logging.error(f"プログラムの実行中にエラーが発生しました: {e}")
        finally:
            logging.info("プログラムを終了します。")
    elif args.srv_type == 'mr':
        try:
            # 複数のフィルターパターンと除外パターンをリストとして定義
            include_patterns = [
                re.compile(r'postfix/virtual'),
                re.compile(r'to='),
                # 他のフィルターパターンをここに追加
            ]
            exclude_patterns = [
                re.compile(r'bkbmp01_BCC@bk.mufg.jp'),
                re.compile(r'bkbmp01_NDR@bk.mufg.jp'),
                re.compile(r'bkbmp01_BCC@bk.mufg.local'),
                re.compile(r'bkbmp01_NDR@bk.mufg.local'),
                # 他の除外パターンをここに追加
            ]
            process_log_files(args.log_path)
        except Exception as e:
            logging.error(f"プログラムの実行中にエラーが発生しました: {e}")
        finally:
            logging.info("プログラムを終了します。")

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?