中級者向け No.14

ログファイルアナライザー

ログファイルを読み込んでエラー・警告を抽出・フィルタリング・統計表示するツール。正規表現の実践活用を学びます。

🎯 難易度: ★★☆ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

ログファイルを読み込んでエラー・警告を抽出・フィルタリング・統計表示するツール。正規表現の実践活用を学びます。

このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。

Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。

ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。

GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。

2. 機能一覧

  • ログファイルアナライザーのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • キーボードショートカット対応

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows・Mac・Linux すべて対応

以下の環境で動作確認しています。

  • Python 3.10 以上
  • OS: Windows 10/11・macOS 12+・Ubuntu 20.04+

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

app14.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import re
from datetime import datetime
from collections import Counter
import os


class App14:
    """ログファイルアナライザー"""

    # よくあるログ形式の正規表現パターン
    PATTERNS = {
        "Apache/Nginx": r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\S+)',
        "Python/標準": r'(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[,.]?\d*) (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) (?P<logger>\S+):? (?P<message>.*)',
        "Syslog": r'(?P<time>\w{3}\s+\d+\s+\d+:\d+:\d+) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',
        "Windows Event": r'(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<source>[^\s]+) (?P<id>\d+) (?P<message>.*)',
    }

    LEVEL_COLORS = {
        "ERROR": "#ff7b72", "CRITICAL": "#ff0000",
        "WARNING": "#ffa657", "INFO": "#3fb950",
        "DEBUG": "#8b949e",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("ログファイルアナライザー")
        self.root.geometry("980x640")
        self.root.configure(bg="#0d1117")
        self.lines = []
        self.filtered_lines = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=8)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 ログファイルアナライザー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#161b22", fg="#58a6ff").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="📂 ファイルを開く",
                   command=self._open_file).pack(side=tk.LEFT, padx=8)
        ttk.Button(header, text="📊 統計表示",
                   command=self._show_stats).pack(side=tk.LEFT, padx=4)

        # フィルターバー
        filter_frame = tk.Frame(self.root, bg="#161b22", pady=6)
        filter_frame.pack(fill=tk.X)

        tk.Label(filter_frame, text="🔍 検索:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=24)
        search_entry.pack(side=tk.LEFT, padx=4)
        search_entry.bind("<KeyRelease>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="レベル:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.level_var = tk.StringVar(value="すべて")
        level_cb = ttk.Combobox(filter_frame, textvariable=self.level_var,
                                 values=["すべて", "ERROR", "CRITICAL",
                                         "WARNING", "INFO", "DEBUG"],
                                 state="readonly", width=10)
        level_cb.pack(side=tk.LEFT)
        level_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="パターン:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.pattern_var = tk.StringVar(value="Python/標準")
        pattern_cb = ttk.Combobox(filter_frame, textvariable=self.pattern_var,
                                   values=["自動検出"] + list(self.PATTERNS.keys()),
                                   state="readonly", width=14)
        pattern_cb.pack(side=tk.LEFT)

        tk.Label(filter_frame, text="行数:",
                 bg="#161b22", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.count_label = tk.Label(filter_frame, text="0 / 0",
                                    bg="#161b22", fg="#58a6ff",
                                    font=("Arial", 10, "bold"))
        self.count_label.pack(side=tk.LEFT)

        # メインエリア
        paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # ログビューアー
        log_frame = tk.Frame(paned, bg="#0d1117")
        self.log_text = tk.Text(log_frame, bg="#0d1117", fg="#c9d1d9",
                                 font=("Courier New", 10),
                                 wrap=tk.NONE, state=tk.DISABLED,
                                 relief=tk.FLAT)
        h_sb = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL,
                             command=self.log_text.xview)
        v_sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
                             command=self.log_text.yview)
        self.log_text.configure(xscrollcommand=h_sb.set,
                                yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        h_sb.pack(side=tk.BOTTOM, fill=tk.X)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        paned.add(log_frame, weight=3)

        # タグ設定
        for level, color in self.LEVEL_COLORS.items():
            self.log_text.tag_configure(level, foreground=color)
        self.log_text.tag_configure("highlight", background="#ffd700",
                                    foreground="#000")
        self.log_text.tag_configure("lineno", foreground="#8b949e")

        # 統計パネル
        stats_frame = tk.Frame(paned, bg="#161b22")
        tk.Label(stats_frame, text="エラー統計",
                 bg="#161b22", fg="#8b949e",
                 font=("Arial", 9)).pack(anchor="w", padx=8, pady=2)
        self.stats_canvas = tk.Canvas(stats_frame, bg="#161b22",
                                       height=100, highlightthickness=0)
        self.stats_canvas.pack(fill=tk.X, padx=8)
        paned.add(stats_frame, weight=1)

        self.status_var = tk.StringVar(value="ログファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#161b22", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("ログファイル", "*.log *.txt *.out"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            for enc in ["utf-8", "cp932", "latin-1"]:
                try:
                    with open(path, encoding=enc, errors="replace") as f:
                        self.lines = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            self.root.title(f"ログアナライザー — {os.path.basename(path)}")
            self._apply_filter()
            self._update_stats()
            self.status_var.set(
                f"{os.path.basename(path)}  |  {len(self.lines)} 行")
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _apply_filter(self):
        q = self.search_var.get().strip().lower()
        level_filter = self.level_var.get()
        result = []
        for i, line in enumerate(self.lines):
            line_l = line.lower()
            # レベルフィルター
            if level_filter != "すべて":
                if level_filter.lower() not in line_l:
                    continue
            # テキスト検索
            if q and q not in line_l:
                continue
            result.append((i + 1, line))

        self.filtered_lines = result
        self.count_label.config(
            text=f"{len(result)} / {len(self.lines)}")
        self._render_log()

    def _render_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete("1.0", tk.END)
        q = self.search_var.get().strip().lower()
        for lineno, line in self.filtered_lines:
            start = self.log_text.index(tk.END)
            self.log_text.insert(tk.END, f"{lineno:6d} │ {line}")
            end = self.log_text.index(tk.END)
            # レベルカラー
            line_upper = line.upper()
            for level in self.LEVEL_COLORS:
                if level in line_upper:
                    self.log_text.tag_add(level, start, end)
                    break
            # 検索ハイライト
            if q:
                content = line.lower()
                idx = 0
                while True:
                    pos = content.find(q, idx)
                    if pos < 0:
                        break
                    tag_start = f"{start}+{pos+9}c"  # 行番号分オフセット
                    tag_end = f"{start}+{pos+9+len(q)}c"
                    self.log_text.tag_add("highlight", tag_start, tag_end)
                    idx = pos + 1
        self.log_text.config(state=tk.DISABLED)

    def _update_stats(self):
        level_counts = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    break

        self.stats_canvas.delete("all")
        total = sum(level_counts.values()) or 1
        x = 10
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            color = self.LEVEL_COLORS.get(level, "#ccc")
            bar_w = int(count / total * 200)
            self.stats_canvas.create_rectangle(
                x, 20, x + bar_w, 60, fill=color, outline="")
            self.stats_canvas.create_text(
                x + bar_w // 2, 40, text=f"{level}\n{count}",
                fill="#fff", font=("Arial", 8))
            x += bar_w + 10

    def _show_stats(self):
        if not self.lines:
            messagebox.showinfo("情報", "ファイルを開いてください")
            return
        level_counts = Counter()
        error_messages = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    if level in ("ERROR", "CRITICAL"):
                        msg = line.strip()[-80:]
                        error_messages[msg] += 1
                    break

        win = tk.Toplevel(self.root)
        win.title("ログ統計")
        win.geometry("600x400")
        win.configure(bg="#0d1117")
        txt = tk.Text(win, bg="#0d1117", fg="#c9d1d9",
                      font=("Courier New", 11))
        txt.pack(fill=tk.BOTH, expand=True)
        txt.insert(tk.END, f"【ログ統計】  全 {len(self.lines)} 行\n\n")
        txt.insert(tk.END, "=== レベル別件数 ===\n")
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            bar = "█" * min(40, count // max(1, len(self.lines)//40))
            txt.insert(tk.END, f"  {level:<10} {count:>6} 件  {bar}\n")
        txt.insert(tk.END, "\n=== エラー Top10 ===\n")
        for msg, count in error_messages.most_common(10):
            txt.insert(tk.END, f"  [{count:>4}回] {msg[:70]}\n")
        txt.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App14(root)
    root.mainloop()

5. コード解説

ログファイルアナライザーのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。

クラス設計とコンストラクタ

App14クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import re
from datetime import datetime
from collections import Counter
import os


class App14:
    """ログファイルアナライザー"""

    # よくあるログ形式の正規表現パターン
    PATTERNS = {
        "Apache/Nginx": r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\S+)',
        "Python/標準": r'(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[,.]?\d*) (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) (?P<logger>\S+):? (?P<message>.*)',
        "Syslog": r'(?P<time>\w{3}\s+\d+\s+\d+:\d+:\d+) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',
        "Windows Event": r'(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<source>[^\s]+) (?P<id>\d+) (?P<message>.*)',
    }

    LEVEL_COLORS = {
        "ERROR": "#ff7b72", "CRITICAL": "#ff0000",
        "WARNING": "#ffa657", "INFO": "#3fb950",
        "DEBUG": "#8b949e",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("ログファイルアナライザー")
        self.root.geometry("980x640")
        self.root.configure(bg="#0d1117")
        self.lines = []
        self.filtered_lines = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=8)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 ログファイルアナライザー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#161b22", fg="#58a6ff").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="📂 ファイルを開く",
                   command=self._open_file).pack(side=tk.LEFT, padx=8)
        ttk.Button(header, text="📊 統計表示",
                   command=self._show_stats).pack(side=tk.LEFT, padx=4)

        # フィルターバー
        filter_frame = tk.Frame(self.root, bg="#161b22", pady=6)
        filter_frame.pack(fill=tk.X)

        tk.Label(filter_frame, text="🔍 検索:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=24)
        search_entry.pack(side=tk.LEFT, padx=4)
        search_entry.bind("<KeyRelease>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="レベル:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.level_var = tk.StringVar(value="すべて")
        level_cb = ttk.Combobox(filter_frame, textvariable=self.level_var,
                                 values=["すべて", "ERROR", "CRITICAL",
                                         "WARNING", "INFO", "DEBUG"],
                                 state="readonly", width=10)
        level_cb.pack(side=tk.LEFT)
        level_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="パターン:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.pattern_var = tk.StringVar(value="Python/標準")
        pattern_cb = ttk.Combobox(filter_frame, textvariable=self.pattern_var,
                                   values=["自動検出"] + list(self.PATTERNS.keys()),
                                   state="readonly", width=14)
        pattern_cb.pack(side=tk.LEFT)

        tk.Label(filter_frame, text="行数:",
                 bg="#161b22", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.count_label = tk.Label(filter_frame, text="0 / 0",
                                    bg="#161b22", fg="#58a6ff",
                                    font=("Arial", 10, "bold"))
        self.count_label.pack(side=tk.LEFT)

        # メインエリア
        paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # ログビューアー
        log_frame = tk.Frame(paned, bg="#0d1117")
        self.log_text = tk.Text(log_frame, bg="#0d1117", fg="#c9d1d9",
                                 font=("Courier New", 10),
                                 wrap=tk.NONE, state=tk.DISABLED,
                                 relief=tk.FLAT)
        h_sb = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL,
                             command=self.log_text.xview)
        v_sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
                             command=self.log_text.yview)
        self.log_text.configure(xscrollcommand=h_sb.set,
                                yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        h_sb.pack(side=tk.BOTTOM, fill=tk.X)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        paned.add(log_frame, weight=3)

        # タグ設定
        for level, color in self.LEVEL_COLORS.items():
            self.log_text.tag_configure(level, foreground=color)
        self.log_text.tag_configure("highlight", background="#ffd700",
                                    foreground="#000")
        self.log_text.tag_configure("lineno", foreground="#8b949e")

        # 統計パネル
        stats_frame = tk.Frame(paned, bg="#161b22")
        tk.Label(stats_frame, text="エラー統計",
                 bg="#161b22", fg="#8b949e",
                 font=("Arial", 9)).pack(anchor="w", padx=8, pady=2)
        self.stats_canvas = tk.Canvas(stats_frame, bg="#161b22",
                                       height=100, highlightthickness=0)
        self.stats_canvas.pack(fill=tk.X, padx=8)
        paned.add(stats_frame, weight=1)

        self.status_var = tk.StringVar(value="ログファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#161b22", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("ログファイル", "*.log *.txt *.out"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            for enc in ["utf-8", "cp932", "latin-1"]:
                try:
                    with open(path, encoding=enc, errors="replace") as f:
                        self.lines = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            self.root.title(f"ログアナライザー — {os.path.basename(path)}")
            self._apply_filter()
            self._update_stats()
            self.status_var.set(
                f"{os.path.basename(path)}  |  {len(self.lines)} 行")
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _apply_filter(self):
        q = self.search_var.get().strip().lower()
        level_filter = self.level_var.get()
        result = []
        for i, line in enumerate(self.lines):
            line_l = line.lower()
            # レベルフィルター
            if level_filter != "すべて":
                if level_filter.lower() not in line_l:
                    continue
            # テキスト検索
            if q and q not in line_l:
                continue
            result.append((i + 1, line))

        self.filtered_lines = result
        self.count_label.config(
            text=f"{len(result)} / {len(self.lines)}")
        self._render_log()

    def _render_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete("1.0", tk.END)
        q = self.search_var.get().strip().lower()
        for lineno, line in self.filtered_lines:
            start = self.log_text.index(tk.END)
            self.log_text.insert(tk.END, f"{lineno:6d} │ {line}")
            end = self.log_text.index(tk.END)
            # レベルカラー
            line_upper = line.upper()
            for level in self.LEVEL_COLORS:
                if level in line_upper:
                    self.log_text.tag_add(level, start, end)
                    break
            # 検索ハイライト
            if q:
                content = line.lower()
                idx = 0
                while True:
                    pos = content.find(q, idx)
                    if pos < 0:
                        break
                    tag_start = f"{start}+{pos+9}c"  # 行番号分オフセット
                    tag_end = f"{start}+{pos+9+len(q)}c"
                    self.log_text.tag_add("highlight", tag_start, tag_end)
                    idx = pos + 1
        self.log_text.config(state=tk.DISABLED)

    def _update_stats(self):
        level_counts = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    break

        self.stats_canvas.delete("all")
        total = sum(level_counts.values()) or 1
        x = 10
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            color = self.LEVEL_COLORS.get(level, "#ccc")
            bar_w = int(count / total * 200)
            self.stats_canvas.create_rectangle(
                x, 20, x + bar_w, 60, fill=color, outline="")
            self.stats_canvas.create_text(
                x + bar_w // 2, 40, text=f"{level}\n{count}",
                fill="#fff", font=("Arial", 8))
            x += bar_w + 10

    def _show_stats(self):
        if not self.lines:
            messagebox.showinfo("情報", "ファイルを開いてください")
            return
        level_counts = Counter()
        error_messages = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    if level in ("ERROR", "CRITICAL"):
                        msg = line.strip()[-80:]
                        error_messages[msg] += 1
                    break

        win = tk.Toplevel(self.root)
        win.title("ログ統計")
        win.geometry("600x400")
        win.configure(bg="#0d1117")
        txt = tk.Text(win, bg="#0d1117", fg="#c9d1d9",
                      font=("Courier New", 11))
        txt.pack(fill=tk.BOTH, expand=True)
        txt.insert(tk.END, f"【ログ統計】  全 {len(self.lines)} 行\n\n")
        txt.insert(tk.END, "=== レベル別件数 ===\n")
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            bar = "█" * min(40, count // max(1, len(self.lines)//40))
            txt.insert(tk.END, f"  {level:<10} {count:>6} 件  {bar}\n")
        txt.insert(tk.END, "\n=== エラー Top10 ===\n")
        for msg, count in error_messages.most_common(10):
            txt.insert(tk.END, f"  [{count:>4}回] {msg[:70]}\n")
        txt.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App14(root)
    root.mainloop()

LabelFrameによるセクション分け

ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import re
from datetime import datetime
from collections import Counter
import os


class App14:
    """ログファイルアナライザー"""

    # よくあるログ形式の正規表現パターン
    PATTERNS = {
        "Apache/Nginx": r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\S+)',
        "Python/標準": r'(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[,.]?\d*) (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) (?P<logger>\S+):? (?P<message>.*)',
        "Syslog": r'(?P<time>\w{3}\s+\d+\s+\d+:\d+:\d+) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',
        "Windows Event": r'(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<source>[^\s]+) (?P<id>\d+) (?P<message>.*)',
    }

    LEVEL_COLORS = {
        "ERROR": "#ff7b72", "CRITICAL": "#ff0000",
        "WARNING": "#ffa657", "INFO": "#3fb950",
        "DEBUG": "#8b949e",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("ログファイルアナライザー")
        self.root.geometry("980x640")
        self.root.configure(bg="#0d1117")
        self.lines = []
        self.filtered_lines = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=8)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 ログファイルアナライザー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#161b22", fg="#58a6ff").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="📂 ファイルを開く",
                   command=self._open_file).pack(side=tk.LEFT, padx=8)
        ttk.Button(header, text="📊 統計表示",
                   command=self._show_stats).pack(side=tk.LEFT, padx=4)

        # フィルターバー
        filter_frame = tk.Frame(self.root, bg="#161b22", pady=6)
        filter_frame.pack(fill=tk.X)

        tk.Label(filter_frame, text="🔍 検索:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=24)
        search_entry.pack(side=tk.LEFT, padx=4)
        search_entry.bind("<KeyRelease>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="レベル:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.level_var = tk.StringVar(value="すべて")
        level_cb = ttk.Combobox(filter_frame, textvariable=self.level_var,
                                 values=["すべて", "ERROR", "CRITICAL",
                                         "WARNING", "INFO", "DEBUG"],
                                 state="readonly", width=10)
        level_cb.pack(side=tk.LEFT)
        level_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="パターン:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.pattern_var = tk.StringVar(value="Python/標準")
        pattern_cb = ttk.Combobox(filter_frame, textvariable=self.pattern_var,
                                   values=["自動検出"] + list(self.PATTERNS.keys()),
                                   state="readonly", width=14)
        pattern_cb.pack(side=tk.LEFT)

        tk.Label(filter_frame, text="行数:",
                 bg="#161b22", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.count_label = tk.Label(filter_frame, text="0 / 0",
                                    bg="#161b22", fg="#58a6ff",
                                    font=("Arial", 10, "bold"))
        self.count_label.pack(side=tk.LEFT)

        # メインエリア
        paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # ログビューアー
        log_frame = tk.Frame(paned, bg="#0d1117")
        self.log_text = tk.Text(log_frame, bg="#0d1117", fg="#c9d1d9",
                                 font=("Courier New", 10),
                                 wrap=tk.NONE, state=tk.DISABLED,
                                 relief=tk.FLAT)
        h_sb = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL,
                             command=self.log_text.xview)
        v_sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
                             command=self.log_text.yview)
        self.log_text.configure(xscrollcommand=h_sb.set,
                                yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        h_sb.pack(side=tk.BOTTOM, fill=tk.X)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        paned.add(log_frame, weight=3)

        # タグ設定
        for level, color in self.LEVEL_COLORS.items():
            self.log_text.tag_configure(level, foreground=color)
        self.log_text.tag_configure("highlight", background="#ffd700",
                                    foreground="#000")
        self.log_text.tag_configure("lineno", foreground="#8b949e")

        # 統計パネル
        stats_frame = tk.Frame(paned, bg="#161b22")
        tk.Label(stats_frame, text="エラー統計",
                 bg="#161b22", fg="#8b949e",
                 font=("Arial", 9)).pack(anchor="w", padx=8, pady=2)
        self.stats_canvas = tk.Canvas(stats_frame, bg="#161b22",
                                       height=100, highlightthickness=0)
        self.stats_canvas.pack(fill=tk.X, padx=8)
        paned.add(stats_frame, weight=1)

        self.status_var = tk.StringVar(value="ログファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#161b22", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("ログファイル", "*.log *.txt *.out"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            for enc in ["utf-8", "cp932", "latin-1"]:
                try:
                    with open(path, encoding=enc, errors="replace") as f:
                        self.lines = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            self.root.title(f"ログアナライザー — {os.path.basename(path)}")
            self._apply_filter()
            self._update_stats()
            self.status_var.set(
                f"{os.path.basename(path)}  |  {len(self.lines)} 行")
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _apply_filter(self):
        q = self.search_var.get().strip().lower()
        level_filter = self.level_var.get()
        result = []
        for i, line in enumerate(self.lines):
            line_l = line.lower()
            # レベルフィルター
            if level_filter != "すべて":
                if level_filter.lower() not in line_l:
                    continue
            # テキスト検索
            if q and q not in line_l:
                continue
            result.append((i + 1, line))

        self.filtered_lines = result
        self.count_label.config(
            text=f"{len(result)} / {len(self.lines)}")
        self._render_log()

    def _render_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete("1.0", tk.END)
        q = self.search_var.get().strip().lower()
        for lineno, line in self.filtered_lines:
            start = self.log_text.index(tk.END)
            self.log_text.insert(tk.END, f"{lineno:6d} │ {line}")
            end = self.log_text.index(tk.END)
            # レベルカラー
            line_upper = line.upper()
            for level in self.LEVEL_COLORS:
                if level in line_upper:
                    self.log_text.tag_add(level, start, end)
                    break
            # 検索ハイライト
            if q:
                content = line.lower()
                idx = 0
                while True:
                    pos = content.find(q, idx)
                    if pos < 0:
                        break
                    tag_start = f"{start}+{pos+9}c"  # 行番号分オフセット
                    tag_end = f"{start}+{pos+9+len(q)}c"
                    self.log_text.tag_add("highlight", tag_start, tag_end)
                    idx = pos + 1
        self.log_text.config(state=tk.DISABLED)

    def _update_stats(self):
        level_counts = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    break

        self.stats_canvas.delete("all")
        total = sum(level_counts.values()) or 1
        x = 10
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            color = self.LEVEL_COLORS.get(level, "#ccc")
            bar_w = int(count / total * 200)
            self.stats_canvas.create_rectangle(
                x, 20, x + bar_w, 60, fill=color, outline="")
            self.stats_canvas.create_text(
                x + bar_w // 2, 40, text=f"{level}\n{count}",
                fill="#fff", font=("Arial", 8))
            x += bar_w + 10

    def _show_stats(self):
        if not self.lines:
            messagebox.showinfo("情報", "ファイルを開いてください")
            return
        level_counts = Counter()
        error_messages = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    if level in ("ERROR", "CRITICAL"):
                        msg = line.strip()[-80:]
                        error_messages[msg] += 1
                    break

        win = tk.Toplevel(self.root)
        win.title("ログ統計")
        win.geometry("600x400")
        win.configure(bg="#0d1117")
        txt = tk.Text(win, bg="#0d1117", fg="#c9d1d9",
                      font=("Courier New", 11))
        txt.pack(fill=tk.BOTH, expand=True)
        txt.insert(tk.END, f"【ログ統計】  全 {len(self.lines)} 行\n\n")
        txt.insert(tk.END, "=== レベル別件数 ===\n")
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            bar = "█" * min(40, count // max(1, len(self.lines)//40))
            txt.insert(tk.END, f"  {level:<10} {count:>6} 件  {bar}\n")
        txt.insert(tk.END, "\n=== エラー Top10 ===\n")
        for msg, count in error_messages.most_common(10):
            txt.insert(tk.END, f"  [{count:>4}回] {msg[:70]}\n")
        txt.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App14(root)
    root.mainloop()

Entryウィジェットとイベントバインド

ttk.Entryで入力フィールドを作成します。bind('', ...)でEnterキー押下時に処理を実行できます。これにより、マウスを使わずキーボードだけで操作できるUXが実現できます。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import re
from datetime import datetime
from collections import Counter
import os


class App14:
    """ログファイルアナライザー"""

    # よくあるログ形式の正規表現パターン
    PATTERNS = {
        "Apache/Nginx": r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\S+)',
        "Python/標準": r'(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[,.]?\d*) (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) (?P<logger>\S+):? (?P<message>.*)',
        "Syslog": r'(?P<time>\w{3}\s+\d+\s+\d+:\d+:\d+) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',
        "Windows Event": r'(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<source>[^\s]+) (?P<id>\d+) (?P<message>.*)',
    }

    LEVEL_COLORS = {
        "ERROR": "#ff7b72", "CRITICAL": "#ff0000",
        "WARNING": "#ffa657", "INFO": "#3fb950",
        "DEBUG": "#8b949e",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("ログファイルアナライザー")
        self.root.geometry("980x640")
        self.root.configure(bg="#0d1117")
        self.lines = []
        self.filtered_lines = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=8)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 ログファイルアナライザー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#161b22", fg="#58a6ff").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="📂 ファイルを開く",
                   command=self._open_file).pack(side=tk.LEFT, padx=8)
        ttk.Button(header, text="📊 統計表示",
                   command=self._show_stats).pack(side=tk.LEFT, padx=4)

        # フィルターバー
        filter_frame = tk.Frame(self.root, bg="#161b22", pady=6)
        filter_frame.pack(fill=tk.X)

        tk.Label(filter_frame, text="🔍 検索:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=24)
        search_entry.pack(side=tk.LEFT, padx=4)
        search_entry.bind("<KeyRelease>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="レベル:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.level_var = tk.StringVar(value="すべて")
        level_cb = ttk.Combobox(filter_frame, textvariable=self.level_var,
                                 values=["すべて", "ERROR", "CRITICAL",
                                         "WARNING", "INFO", "DEBUG"],
                                 state="readonly", width=10)
        level_cb.pack(side=tk.LEFT)
        level_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="パターン:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.pattern_var = tk.StringVar(value="Python/標準")
        pattern_cb = ttk.Combobox(filter_frame, textvariable=self.pattern_var,
                                   values=["自動検出"] + list(self.PATTERNS.keys()),
                                   state="readonly", width=14)
        pattern_cb.pack(side=tk.LEFT)

        tk.Label(filter_frame, text="行数:",
                 bg="#161b22", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.count_label = tk.Label(filter_frame, text="0 / 0",
                                    bg="#161b22", fg="#58a6ff",
                                    font=("Arial", 10, "bold"))
        self.count_label.pack(side=tk.LEFT)

        # メインエリア
        paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # ログビューアー
        log_frame = tk.Frame(paned, bg="#0d1117")
        self.log_text = tk.Text(log_frame, bg="#0d1117", fg="#c9d1d9",
                                 font=("Courier New", 10),
                                 wrap=tk.NONE, state=tk.DISABLED,
                                 relief=tk.FLAT)
        h_sb = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL,
                             command=self.log_text.xview)
        v_sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
                             command=self.log_text.yview)
        self.log_text.configure(xscrollcommand=h_sb.set,
                                yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        h_sb.pack(side=tk.BOTTOM, fill=tk.X)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        paned.add(log_frame, weight=3)

        # タグ設定
        for level, color in self.LEVEL_COLORS.items():
            self.log_text.tag_configure(level, foreground=color)
        self.log_text.tag_configure("highlight", background="#ffd700",
                                    foreground="#000")
        self.log_text.tag_configure("lineno", foreground="#8b949e")

        # 統計パネル
        stats_frame = tk.Frame(paned, bg="#161b22")
        tk.Label(stats_frame, text="エラー統計",
                 bg="#161b22", fg="#8b949e",
                 font=("Arial", 9)).pack(anchor="w", padx=8, pady=2)
        self.stats_canvas = tk.Canvas(stats_frame, bg="#161b22",
                                       height=100, highlightthickness=0)
        self.stats_canvas.pack(fill=tk.X, padx=8)
        paned.add(stats_frame, weight=1)

        self.status_var = tk.StringVar(value="ログファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#161b22", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("ログファイル", "*.log *.txt *.out"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            for enc in ["utf-8", "cp932", "latin-1"]:
                try:
                    with open(path, encoding=enc, errors="replace") as f:
                        self.lines = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            self.root.title(f"ログアナライザー — {os.path.basename(path)}")
            self._apply_filter()
            self._update_stats()
            self.status_var.set(
                f"{os.path.basename(path)}  |  {len(self.lines)} 行")
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _apply_filter(self):
        q = self.search_var.get().strip().lower()
        level_filter = self.level_var.get()
        result = []
        for i, line in enumerate(self.lines):
            line_l = line.lower()
            # レベルフィルター
            if level_filter != "すべて":
                if level_filter.lower() not in line_l:
                    continue
            # テキスト検索
            if q and q not in line_l:
                continue
            result.append((i + 1, line))

        self.filtered_lines = result
        self.count_label.config(
            text=f"{len(result)} / {len(self.lines)}")
        self._render_log()

    def _render_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete("1.0", tk.END)
        q = self.search_var.get().strip().lower()
        for lineno, line in self.filtered_lines:
            start = self.log_text.index(tk.END)
            self.log_text.insert(tk.END, f"{lineno:6d} │ {line}")
            end = self.log_text.index(tk.END)
            # レベルカラー
            line_upper = line.upper()
            for level in self.LEVEL_COLORS:
                if level in line_upper:
                    self.log_text.tag_add(level, start, end)
                    break
            # 検索ハイライト
            if q:
                content = line.lower()
                idx = 0
                while True:
                    pos = content.find(q, idx)
                    if pos < 0:
                        break
                    tag_start = f"{start}+{pos+9}c"  # 行番号分オフセット
                    tag_end = f"{start}+{pos+9+len(q)}c"
                    self.log_text.tag_add("highlight", tag_start, tag_end)
                    idx = pos + 1
        self.log_text.config(state=tk.DISABLED)

    def _update_stats(self):
        level_counts = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    break

        self.stats_canvas.delete("all")
        total = sum(level_counts.values()) or 1
        x = 10
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            color = self.LEVEL_COLORS.get(level, "#ccc")
            bar_w = int(count / total * 200)
            self.stats_canvas.create_rectangle(
                x, 20, x + bar_w, 60, fill=color, outline="")
            self.stats_canvas.create_text(
                x + bar_w // 2, 40, text=f"{level}\n{count}",
                fill="#fff", font=("Arial", 8))
            x += bar_w + 10

    def _show_stats(self):
        if not self.lines:
            messagebox.showinfo("情報", "ファイルを開いてください")
            return
        level_counts = Counter()
        error_messages = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    if level in ("ERROR", "CRITICAL"):
                        msg = line.strip()[-80:]
                        error_messages[msg] += 1
                    break

        win = tk.Toplevel(self.root)
        win.title("ログ統計")
        win.geometry("600x400")
        win.configure(bg="#0d1117")
        txt = tk.Text(win, bg="#0d1117", fg="#c9d1d9",
                      font=("Courier New", 11))
        txt.pack(fill=tk.BOTH, expand=True)
        txt.insert(tk.END, f"【ログ統計】  全 {len(self.lines)} 行\n\n")
        txt.insert(tk.END, "=== レベル別件数 ===\n")
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            bar = "█" * min(40, count // max(1, len(self.lines)//40))
            txt.insert(tk.END, f"  {level:<10} {count:>6} 件  {bar}\n")
        txt.insert(tk.END, "\n=== エラー Top10 ===\n")
        for msg, count in error_messages.most_common(10):
            txt.insert(tk.END, f"  [{count:>4}回] {msg[:70]}\n")
        txt.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App14(root)
    root.mainloop()

Textウィジェットでの結果表示

結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import re
from datetime import datetime
from collections import Counter
import os


class App14:
    """ログファイルアナライザー"""

    # よくあるログ形式の正規表現パターン
    PATTERNS = {
        "Apache/Nginx": r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\S+)',
        "Python/標準": r'(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[,.]?\d*) (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) (?P<logger>\S+):? (?P<message>.*)',
        "Syslog": r'(?P<time>\w{3}\s+\d+\s+\d+:\d+:\d+) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',
        "Windows Event": r'(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<source>[^\s]+) (?P<id>\d+) (?P<message>.*)',
    }

    LEVEL_COLORS = {
        "ERROR": "#ff7b72", "CRITICAL": "#ff0000",
        "WARNING": "#ffa657", "INFO": "#3fb950",
        "DEBUG": "#8b949e",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("ログファイルアナライザー")
        self.root.geometry("980x640")
        self.root.configure(bg="#0d1117")
        self.lines = []
        self.filtered_lines = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=8)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 ログファイルアナライザー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#161b22", fg="#58a6ff").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="📂 ファイルを開く",
                   command=self._open_file).pack(side=tk.LEFT, padx=8)
        ttk.Button(header, text="📊 統計表示",
                   command=self._show_stats).pack(side=tk.LEFT, padx=4)

        # フィルターバー
        filter_frame = tk.Frame(self.root, bg="#161b22", pady=6)
        filter_frame.pack(fill=tk.X)

        tk.Label(filter_frame, text="🔍 検索:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=24)
        search_entry.pack(side=tk.LEFT, padx=4)
        search_entry.bind("<KeyRelease>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="レベル:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.level_var = tk.StringVar(value="すべて")
        level_cb = ttk.Combobox(filter_frame, textvariable=self.level_var,
                                 values=["すべて", "ERROR", "CRITICAL",
                                         "WARNING", "INFO", "DEBUG"],
                                 state="readonly", width=10)
        level_cb.pack(side=tk.LEFT)
        level_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="パターン:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.pattern_var = tk.StringVar(value="Python/標準")
        pattern_cb = ttk.Combobox(filter_frame, textvariable=self.pattern_var,
                                   values=["自動検出"] + list(self.PATTERNS.keys()),
                                   state="readonly", width=14)
        pattern_cb.pack(side=tk.LEFT)

        tk.Label(filter_frame, text="行数:",
                 bg="#161b22", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.count_label = tk.Label(filter_frame, text="0 / 0",
                                    bg="#161b22", fg="#58a6ff",
                                    font=("Arial", 10, "bold"))
        self.count_label.pack(side=tk.LEFT)

        # メインエリア
        paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # ログビューアー
        log_frame = tk.Frame(paned, bg="#0d1117")
        self.log_text = tk.Text(log_frame, bg="#0d1117", fg="#c9d1d9",
                                 font=("Courier New", 10),
                                 wrap=tk.NONE, state=tk.DISABLED,
                                 relief=tk.FLAT)
        h_sb = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL,
                             command=self.log_text.xview)
        v_sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
                             command=self.log_text.yview)
        self.log_text.configure(xscrollcommand=h_sb.set,
                                yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        h_sb.pack(side=tk.BOTTOM, fill=tk.X)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        paned.add(log_frame, weight=3)

        # タグ設定
        for level, color in self.LEVEL_COLORS.items():
            self.log_text.tag_configure(level, foreground=color)
        self.log_text.tag_configure("highlight", background="#ffd700",
                                    foreground="#000")
        self.log_text.tag_configure("lineno", foreground="#8b949e")

        # 統計パネル
        stats_frame = tk.Frame(paned, bg="#161b22")
        tk.Label(stats_frame, text="エラー統計",
                 bg="#161b22", fg="#8b949e",
                 font=("Arial", 9)).pack(anchor="w", padx=8, pady=2)
        self.stats_canvas = tk.Canvas(stats_frame, bg="#161b22",
                                       height=100, highlightthickness=0)
        self.stats_canvas.pack(fill=tk.X, padx=8)
        paned.add(stats_frame, weight=1)

        self.status_var = tk.StringVar(value="ログファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#161b22", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("ログファイル", "*.log *.txt *.out"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            for enc in ["utf-8", "cp932", "latin-1"]:
                try:
                    with open(path, encoding=enc, errors="replace") as f:
                        self.lines = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            self.root.title(f"ログアナライザー — {os.path.basename(path)}")
            self._apply_filter()
            self._update_stats()
            self.status_var.set(
                f"{os.path.basename(path)}  |  {len(self.lines)} 行")
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _apply_filter(self):
        q = self.search_var.get().strip().lower()
        level_filter = self.level_var.get()
        result = []
        for i, line in enumerate(self.lines):
            line_l = line.lower()
            # レベルフィルター
            if level_filter != "すべて":
                if level_filter.lower() not in line_l:
                    continue
            # テキスト検索
            if q and q not in line_l:
                continue
            result.append((i + 1, line))

        self.filtered_lines = result
        self.count_label.config(
            text=f"{len(result)} / {len(self.lines)}")
        self._render_log()

    def _render_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete("1.0", tk.END)
        q = self.search_var.get().strip().lower()
        for lineno, line in self.filtered_lines:
            start = self.log_text.index(tk.END)
            self.log_text.insert(tk.END, f"{lineno:6d} │ {line}")
            end = self.log_text.index(tk.END)
            # レベルカラー
            line_upper = line.upper()
            for level in self.LEVEL_COLORS:
                if level in line_upper:
                    self.log_text.tag_add(level, start, end)
                    break
            # 検索ハイライト
            if q:
                content = line.lower()
                idx = 0
                while True:
                    pos = content.find(q, idx)
                    if pos < 0:
                        break
                    tag_start = f"{start}+{pos+9}c"  # 行番号分オフセット
                    tag_end = f"{start}+{pos+9+len(q)}c"
                    self.log_text.tag_add("highlight", tag_start, tag_end)
                    idx = pos + 1
        self.log_text.config(state=tk.DISABLED)

    def _update_stats(self):
        level_counts = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    break

        self.stats_canvas.delete("all")
        total = sum(level_counts.values()) or 1
        x = 10
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            color = self.LEVEL_COLORS.get(level, "#ccc")
            bar_w = int(count / total * 200)
            self.stats_canvas.create_rectangle(
                x, 20, x + bar_w, 60, fill=color, outline="")
            self.stats_canvas.create_text(
                x + bar_w // 2, 40, text=f"{level}\n{count}",
                fill="#fff", font=("Arial", 8))
            x += bar_w + 10

    def _show_stats(self):
        if not self.lines:
            messagebox.showinfo("情報", "ファイルを開いてください")
            return
        level_counts = Counter()
        error_messages = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    if level in ("ERROR", "CRITICAL"):
                        msg = line.strip()[-80:]
                        error_messages[msg] += 1
                    break

        win = tk.Toplevel(self.root)
        win.title("ログ統計")
        win.geometry("600x400")
        win.configure(bg="#0d1117")
        txt = tk.Text(win, bg="#0d1117", fg="#c9d1d9",
                      font=("Courier New", 11))
        txt.pack(fill=tk.BOTH, expand=True)
        txt.insert(tk.END, f"【ログ統計】  全 {len(self.lines)} 行\n\n")
        txt.insert(tk.END, "=== レベル別件数 ===\n")
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            bar = "█" * min(40, count // max(1, len(self.lines)//40))
            txt.insert(tk.END, f"  {level:<10} {count:>6} 件  {bar}\n")
        txt.insert(tk.END, "\n=== エラー Top10 ===\n")
        for msg, count in error_messages.most_common(10):
            txt.insert(tk.END, f"  [{count:>4}回] {msg[:70]}\n")
        txt.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App14(root)
    root.mainloop()

例外処理とmessagebox

try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import re
from datetime import datetime
from collections import Counter
import os


class App14:
    """ログファイルアナライザー"""

    # よくあるログ形式の正規表現パターン
    PATTERNS = {
        "Apache/Nginx": r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<size>\S+)',
        "Python/標準": r'(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[,.]?\d*) (?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) (?P<logger>\S+):? (?P<message>.*)',
        "Syslog": r'(?P<time>\w{3}\s+\d+\s+\d+:\d+:\d+) (?P<host>\S+) (?P<process>\S+): (?P<message>.*)',
        "Windows Event": r'(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<source>[^\s]+) (?P<id>\d+) (?P<message>.*)',
    }

    LEVEL_COLORS = {
        "ERROR": "#ff7b72", "CRITICAL": "#ff0000",
        "WARNING": "#ffa657", "INFO": "#3fb950",
        "DEBUG": "#8b949e",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("ログファイルアナライザー")
        self.root.geometry("980x640")
        self.root.configure(bg="#0d1117")
        self.lines = []
        self.filtered_lines = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#161b22", pady=8)
        header.pack(fill=tk.X)
        tk.Label(header, text="📋 ログファイルアナライザー",
                 font=("Noto Sans JP", 14, "bold"),
                 bg="#161b22", fg="#58a6ff").pack(side=tk.LEFT, padx=12)
        ttk.Button(header, text="📂 ファイルを開く",
                   command=self._open_file).pack(side=tk.LEFT, padx=8)
        ttk.Button(header, text="📊 統計表示",
                   command=self._show_stats).pack(side=tk.LEFT, padx=4)

        # フィルターバー
        filter_frame = tk.Frame(self.root, bg="#161b22", pady=6)
        filter_frame.pack(fill=tk.X)

        tk.Label(filter_frame, text="🔍 検索:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=8)
        self.search_var = tk.StringVar()
        search_entry = ttk.Entry(filter_frame, textvariable=self.search_var, width=24)
        search_entry.pack(side=tk.LEFT, padx=4)
        search_entry.bind("<KeyRelease>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="レベル:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.level_var = tk.StringVar(value="すべて")
        level_cb = ttk.Combobox(filter_frame, textvariable=self.level_var,
                                 values=["すべて", "ERROR", "CRITICAL",
                                         "WARNING", "INFO", "DEBUG"],
                                 state="readonly", width=10)
        level_cb.pack(side=tk.LEFT)
        level_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_filter())

        tk.Label(filter_frame, text="パターン:",
                 bg="#161b22", fg="#ccc").pack(side=tk.LEFT, padx=(12, 4))
        self.pattern_var = tk.StringVar(value="Python/標準")
        pattern_cb = ttk.Combobox(filter_frame, textvariable=self.pattern_var,
                                   values=["自動検出"] + list(self.PATTERNS.keys()),
                                   state="readonly", width=14)
        pattern_cb.pack(side=tk.LEFT)

        tk.Label(filter_frame, text="行数:",
                 bg="#161b22", fg="#8b949e").pack(side=tk.LEFT, padx=(16, 4))
        self.count_label = tk.Label(filter_frame, text="0 / 0",
                                    bg="#161b22", fg="#58a6ff",
                                    font=("Arial", 10, "bold"))
        self.count_label.pack(side=tk.LEFT)

        # メインエリア
        paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)

        # ログビューアー
        log_frame = tk.Frame(paned, bg="#0d1117")
        self.log_text = tk.Text(log_frame, bg="#0d1117", fg="#c9d1d9",
                                 font=("Courier New", 10),
                                 wrap=tk.NONE, state=tk.DISABLED,
                                 relief=tk.FLAT)
        h_sb = ttk.Scrollbar(log_frame, orient=tk.HORIZONTAL,
                             command=self.log_text.xview)
        v_sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
                             command=self.log_text.yview)
        self.log_text.configure(xscrollcommand=h_sb.set,
                                yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        h_sb.pack(side=tk.BOTTOM, fill=tk.X)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        paned.add(log_frame, weight=3)

        # タグ設定
        for level, color in self.LEVEL_COLORS.items():
            self.log_text.tag_configure(level, foreground=color)
        self.log_text.tag_configure("highlight", background="#ffd700",
                                    foreground="#000")
        self.log_text.tag_configure("lineno", foreground="#8b949e")

        # 統計パネル
        stats_frame = tk.Frame(paned, bg="#161b22")
        tk.Label(stats_frame, text="エラー統計",
                 bg="#161b22", fg="#8b949e",
                 font=("Arial", 9)).pack(anchor="w", padx=8, pady=2)
        self.stats_canvas = tk.Canvas(stats_frame, bg="#161b22",
                                       height=100, highlightthickness=0)
        self.stats_canvas.pack(fill=tk.X, padx=8)
        paned.add(stats_frame, weight=1)

        self.status_var = tk.StringVar(value="ログファイルを開いてください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#161b22", fg="#8b949e", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("ログファイル", "*.log *.txt *.out"),
                       ("すべて", "*.*")])
        if not path:
            return
        try:
            for enc in ["utf-8", "cp932", "latin-1"]:
                try:
                    with open(path, encoding=enc, errors="replace") as f:
                        self.lines = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            self.root.title(f"ログアナライザー — {os.path.basename(path)}")
            self._apply_filter()
            self._update_stats()
            self.status_var.set(
                f"{os.path.basename(path)}  |  {len(self.lines)} 行")
        except Exception as e:
            messagebox.showerror("エラー", str(e))

    def _apply_filter(self):
        q = self.search_var.get().strip().lower()
        level_filter = self.level_var.get()
        result = []
        for i, line in enumerate(self.lines):
            line_l = line.lower()
            # レベルフィルター
            if level_filter != "すべて":
                if level_filter.lower() not in line_l:
                    continue
            # テキスト検索
            if q and q not in line_l:
                continue
            result.append((i + 1, line))

        self.filtered_lines = result
        self.count_label.config(
            text=f"{len(result)} / {len(self.lines)}")
        self._render_log()

    def _render_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete("1.0", tk.END)
        q = self.search_var.get().strip().lower()
        for lineno, line in self.filtered_lines:
            start = self.log_text.index(tk.END)
            self.log_text.insert(tk.END, f"{lineno:6d} │ {line}")
            end = self.log_text.index(tk.END)
            # レベルカラー
            line_upper = line.upper()
            for level in self.LEVEL_COLORS:
                if level in line_upper:
                    self.log_text.tag_add(level, start, end)
                    break
            # 検索ハイライト
            if q:
                content = line.lower()
                idx = 0
                while True:
                    pos = content.find(q, idx)
                    if pos < 0:
                        break
                    tag_start = f"{start}+{pos+9}c"  # 行番号分オフセット
                    tag_end = f"{start}+{pos+9+len(q)}c"
                    self.log_text.tag_add("highlight", tag_start, tag_end)
                    idx = pos + 1
        self.log_text.config(state=tk.DISABLED)

    def _update_stats(self):
        level_counts = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    break

        self.stats_canvas.delete("all")
        total = sum(level_counts.values()) or 1
        x = 10
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            color = self.LEVEL_COLORS.get(level, "#ccc")
            bar_w = int(count / total * 200)
            self.stats_canvas.create_rectangle(
                x, 20, x + bar_w, 60, fill=color, outline="")
            self.stats_canvas.create_text(
                x + bar_w // 2, 40, text=f"{level}\n{count}",
                fill="#fff", font=("Arial", 8))
            x += bar_w + 10

    def _show_stats(self):
        if not self.lines:
            messagebox.showinfo("情報", "ファイルを開いてください")
            return
        level_counts = Counter()
        error_messages = Counter()
        for line in self.lines:
            for level in self.LEVEL_COLORS:
                if level in line.upper():
                    level_counts[level] += 1
                    if level in ("ERROR", "CRITICAL"):
                        msg = line.strip()[-80:]
                        error_messages[msg] += 1
                    break

        win = tk.Toplevel(self.root)
        win.title("ログ統計")
        win.geometry("600x400")
        win.configure(bg="#0d1117")
        txt = tk.Text(win, bg="#0d1117", fg="#c9d1d9",
                      font=("Courier New", 11))
        txt.pack(fill=tk.BOTH, expand=True)
        txt.insert(tk.END, f"【ログ統計】  全 {len(self.lines)} 行\n\n")
        txt.insert(tk.END, "=== レベル別件数 ===\n")
        for level, count in sorted(level_counts.items(),
                                    key=lambda x: x[1], reverse=True):
            bar = "█" * min(40, count // max(1, len(self.lines)//40))
            txt.insert(tk.END, f"  {level:<10} {count:>6} 件  {bar}\n")
        txt.insert(tk.END, "\n=== エラー Top10 ===\n")
        for msg, count in error_messages.most_common(10):
            txt.insert(tk.END, f"  [{count:>4}回] {msg[:70]}\n")
        txt.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App14(root)
    root.mainloop()

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app14.py と保存します。

  2. 2
    クラスの骨格を作る

    App14クラスを定義し、__init__とmainloop()の最小構成を作ります。

  3. 3
    タイトルバーを作る

    Frameを使ってカラーバー付きのタイトルエリアを作ります。

  4. 4
    入力フォームを実装する

    LabelFrameとEntryウィジェットで入力エリアを作ります。

  5. 5
    処理ロジックを実装する

    _calculate()メソッドに計算・処理ロジックを実装します。

  6. 6
    結果表示を実装する

    TextウィジェットかLabelに結果を表示する_show_result()を実装します。

  7. 7
    エラー処理を追加する

    try-exceptとmessageboxでエラーハンドリングを追加します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 データのエクスポート機能

計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 入力履歴機能

以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。

8. よくある問題と解決法

❌ 日本語フォントが表示されない

原因:システムに日本語フォントが見つからない場合があります。

解決法:font引数を省略するかシステムに合ったフォントを指定してください。

❌ ウィンドウのサイズが変更できない

原因:resizable(False, False)が設定されています。

解決法:resizable(True, True)に変更してください。

9. 練習問題

アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。

  1. 課題1:機能拡張

    ログファイルアナライザーに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。

  2. 課題2:UIの改善

    色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。

  3. 課題3:保存機能の追加

    入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のNo.15に挑戦しましょう。