Postfixのログからcsvを生成

```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("プログラムを終了します。")

```