WhatsAppログを日付ごとに
分割するプログラムを作った

メッセージアプリのログを整理したいと思ったことはありませんか?特にWhatsAppのような長期間使うアプリでは、ログが長大になりがち。今回は、WhatsAppのチャットログを日付ごとに分けるプログラムについて考えてみました。

関連記事

1. なぜログを分割するの?

WhatsAppのチャットログはどんどん長くなります。友達との会話、家族グループ、仕事の連絡…。これをすべて一つのファイルで管理していると、後から特定の日の会話を探すのが大変です。

日付ごとにファイルを分けると、次のようなメリットがあります:

  • 特定の日の会話をすぐに見つけられる
  • ファイルが小さくなるので扱いやすい
  • 必要な分だけを取り出して整理できる

1.1. プログラムの設計

まず、WhatsAppのログがどのような形式になっているか見てみましょう。一般的には以下のような形式です:

[2025/04/07 9:21:23] すずき: 昨日は、お疲れ様でした!
Code language: CSS (css)

最初に日付と時刻が「[]」で囲まれ、その後に名前とメッセージが続きます。この形式を利用して、日付を取り出してグループ分けできます。

プログラムの基本的な流れはこうです:

  1. ログファイルを1行ずつ読み込む
  2. 各行から日付を取り出す
  3. 日付ごとにメッセージをグループ化する
  4. 日付ごとにファイルを作成して保存する

オプションとして、最新の2日分だけを取り出す機能も付け加えます。

1.2. Pythonで実装してみよう

Pythonなら、シンプルに実装できます。以下が基本的なコードです:

import re
from collections import defaultdict

def parse_log(file_path):
    day_pattern = re.compile(r'^\[(\d{4}/\d{2}/\d{2}) \d{2}:\d{2}:\d{2}\]')
    days = defaultdict(list)
    with open(file_path, encoding='utf-8') as f:
        for line in f:
            m = day_pattern.match(line)
            if m:
                day = m.group(1)
                days[day].append(line.rstrip())
    return days

def write_per_day(days, output_dir):
    for day, lines in days.items():
        with open(f'{output_dir}/{day.replace("/", "-")}.txt', 'w', encoding='utf-8') as f:
            for line in lines:
                f.write(line + '\n')

def write_latest_n_days(days, output_dir, n=2):
    sorted_days = sorted(days.keys(), reverse=True)
    for day in sorted_days[:n]:
        with open(f'{output_dir}/{day.replace("/", "-")}.txt', 'w', encoding='utf-8') as f:
            for line in days[day]:
                f.write(line + '\n')

# 使い方例
days = parse_log('chat.txt')
write_per_day(days, 'output')  # すべての日付ごと
write_latest_n_days(days, 'output_latest', n=2)  # 最新2日分のみ
Code language: PHP (php)

このコードでは、正規表現を使って日付を取り出しています。正規表現とは、文字列のパターンを表現するための特殊な記法です。ここでは、行の先頭にある「[年/月/日 時:分:秒]」という形式を見つけ出しています。

ただし、このコードこのままではうまく動きませんでした。WhatsAppのログはなぜか時間が1桁のパターンがあったのです。

        # 修正した日付パターン(時間が1桁の場合にも対応)
        day_pattern = re.compile(r'^\[(\d{4}/\d{2}/\d{2}) \d{1,2}:\d{2}:\d{2}\]')Code language: PHP (php)

取り出した日付をキーにして、各行をグループ化します。そして、日付ごとにファイルを作成して保存します。

最新の2日分を取り出す機能は、日付を降順(新しい順)にソートして、先頭から2日分を取り出すだけです。

2. コマンドラインからGUIへの進化

このスクリプトは確かに機能しますが、いくつかの制限があります。特にWhatsAppからエクスポートされるzipファイルを直接処理できない点と、コマンドラインでの操作が必要な点が不便です。ここから、より使いやすいGUIアプリへと発展させていきます。

3. GUIアプリの設計と実装

GUIアプリの設計では、直感的に操作できることを重視しました。ファイル選択ボタン、出力オプションの設定、進捗表示など、使いやすさを考えた要素を取り入れています。

3. GUIアプリの設計と実装

3.1. アプリの基本構造

import os
import re
import zipfile
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from collections import defaultdict
import threading
import shutil
import tempfile

class WhatsAppLogSplitterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("WhatsApp ログ分割ツール")
        self.root.geometry("600x450")
        self.root.resizable(True, True)
        self.root.configure(padx=10, pady=10)
        
        # 作業ディレクトリの準備
        self.temp_dir = tempfile.mkdtemp()
        self.output_dir = os.path.join(os.path.expanduser("~"), "Documents", "WhatsAppLogs")
        os.makedirs(self.output_dir, exist_ok=True)
        
        # UIの構築
        self.setup_ui()

このコードでは、アプリのウィンドウ設定と作業ディレクトリの準備を行っています。tempfile.mkdtemp()は、zipファイルを一時的に解凍するための場所を確保します。冷蔵庫から食材を取り出して並べる作業台のようなものです。

3.2. ユーザーインターフェースの構築

def setup_ui(self):
    # ファイル選択部分
    file_frame = ttk.LabelFrame(self.root, text="入力ファイル")
    file_frame.pack(fill="x", padx=5, pady=5)
    
    self.file_path_var = tk.StringVar()
    file_entry = ttk.Entry(file_frame, textvariable=self.file_path_var, width=50)
    file_entry.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
    
    browse_button = ttk.Button(file_frame, text="参照...", command=self.browse_file)
    browse_button.grid(row=0, column=1, padx=5, pady=5)
    
    # 出力オプション部分
    options_frame = ttk.LabelFrame(self.root, text="出力オプション")
    options_frame.pack(fill="x", padx=5, pady=5)
    
    self.output_all_var = tk.BooleanVar(value=True)
    all_days_check = ttk.Checkbutton(options_frame, text="すべての日付ごとに出力", variable=self.output_all_var)
    all_days_check.grid(row=0, column=0, padx=5, pady=5, sticky="w")
    
    # ...他のUI要素...
Code language: PHP (php)

UIは大きく分けて「ファイル選択」「出力オプション」「出力ディレクトリ」「実行ボタン」「進捗表示」の5つのセクションで構成しています。パーツを組み立てるように、各要素を適切に配置していきます。

3.3. ファイル処理のコア機能

zipファイル処理の実装部分は特に重要です。WhatsAppからエクスポートされたzipファイルを解凍し、チャットログファイルを見つけ出す処理を追加しました。

def extract_zip(self, zip_path):
    # zipファイルの解凍処理
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        # 一時ディレクトリを作成して解凍
        extract_dir = os.path.join(self.temp_dir, "extracted")
        os.makedirs(extract_dir, exist_ok=True)
        zip_ref.extractall(extract_dir)
        
        # チャットログファイルを探す
        for root, _, files in os.walk(extract_dir):
            for file in files:
                if file.endswith(".txt") and ("chat" in file.lower() or "log" in file.lower()):
                    return os.path.join(root, file)
        
        # 見つからない場合は最初のテキストファイルを使用
        for root, _, files in os.walk(extract_dir):
            for file in files:
                if file.endswith(".txt"):
                    return os.path.join(root, file)
    
    raise FileNotFoundError("Zipファイル内にログファイルが見つかりませんでした。")
Code language: PHP (php)

この処理は、宝探しのようなものです。zipファイルという宝箱の中から、チャットログという宝物を探し出します。名前に「chat」や「log」が含まれるテキストファイルを優先的に探し、見つからなければ他のテキストファイルを使用する仕組みです。

3.4. エンコーディング問題への対応

WhatsAppのログファイルは、言語設定によって文字エンコーディングが異なる場合があります。そのため、複数のエンコーディングでの読み込みを試みる処理を追加しました。

def parse_log(self, file_path):
    # 元のコードと同じログ解析処理
    day_pattern = re.compile(r'^\[(\d{4}/\d{2}/\d{2}) \d{2}:\d{2}:\d{2}\]')
    days = defaultdict(list)
    
    try:
        with open(file_path, encoding='utf-8') as f:
            for line in f:
                m = day_pattern.match(line)
                if m:
                    day = m.group(1)
                    days[day].append(line.rstrip())
    except UnicodeDecodeError:
        # UTF-8でエラーが出る場合はShift-JISで試行
        with open(file_path, encoding='shift_jis') as f:
            for line in f:
                m = day_pattern.match(line)
                if m:
                    day = m.group(1)
                    days[day].append(line.rstrip())
    
    return days
Code language: PHP (php)

これは外国語の本を読むときに、まず自分の知っている言語で読めるか試し、ダメなら翻訳ツールを使うようなものです。まずUTF-8で読めるか試し、エラーが出たらShift-JISで再挑戦します。

3.5. 非同期処理による快適なUI体験

GUI操作中にファイル処理が行われると、アプリが固まって操作できなくなります。これを防ぐため、処理部分を別スレッドで実行する非同期処理を実装しました。

def execute(self):
    input_file = self.file_path_var.get()
    
    if not input_file:
        messagebox.showerror("エラー", "入力ファイルを選択してください。")
        return
    
    if not os.path.exists(input_file):
        messagebox.showerror("エラー", "指定されたファイルが存在しません。")
        return
    
    self.progress_bar.start()
    self.progress_var.set("処理中...")
    
    # 別スレッドで処理を実行
    thread = threading.Thread(target=self.process_file, args=(input_file,))
    thread.daemon = True
    thread.start()
Code language: PHP (php)

4. macOS用アプリとしてのパッケージング

コードを書いて動作確認をした後は、macOS用のアプリとしてパッケージ化します。py2appというツールを使うことで、Pythonスクリプトを.appファイルに変換できます。

4.1. setup.pyの作成

"""
macOS用WhatsAppログ分割ツールのビルドスクリプト
"""
import sys
from setuptools import setup

APP = ['app.py']  # GUIアプリのメインファイル名を指定

OPTIONS = {
    'argv_emulation': False,  # これをFalseに変更
    'iconfile': 'app_icon.icns',  # アプリアイコンファイル(必要に応じて作成)
    'plist': {
        'CFBundleName': 'WhatsAppログ分割ツール',
        'CFBundleDisplayName': 'WhatsAppログ分割ツール',
        'CFBundleGetInfoString': 'WhatsAppのチャットログを日付ごとに分割するツール',
        'CFBundleIdentifier': 'com.yourname.whatsapplogwplitter',
        'CFBundleVersion': '1.0.0',
        'CFBundleShortVersionString': '1.0.0',
        'NSHumanReadableCopyright': '© 2025 YourName',
        'NSRequiresAquaSystemAppearance': False,  # ダークモードサポートを追加
        'NSHighResolutionCapable': True,  # Retinaディスプレイのサポートを追加
    },
    'packages': ['tkinter'],
    'includes': ['re', 'zipfile', 'os', 'collections', 'threading', 'shutil', 'tempfile'],
}

setup(
    app=APP,
    name='WhatsAppログ分割ツール',
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)Code language: PHP (php)

このファイルは、アプリの詳細情報とビルドに必要なライブラリのリストを含んでいます。レシピのような役割を果たし、これに基づいてアプリが作られます。

4.2. ビルドコマンド

Python3を使用する場合は、以下のコマンドでアプリをビルドします。

# 必要なパッケージをインストール
pip3 install py2app

# アプリをビルド
python3 setup.py py2appCode language: PHP (php)

macOSのCatalina以降では、システムPythonではなく自分でインストールしたPython3を使うことが推奨されます。その場合は、仮想環境を使うと良いでしょう。

# 仮想環境を作成
python3 -m venv myenv

# 仮想環境を有効化
source myenv/bin/activate

# 必要なパッケージをインストール
pip install py2app
pip install jaraco.text
pip install tkinterdnd2

# アプリをビルド
python setup.py py2app

# ビルドされたアプリの実行
open dist/WhatsAppログ分割ツール.appCode language: PHP (php)

これらのコマンドを実行すると、distフォルダに.appファイルが作成されます。これで完成したアプリを他のmacOSユーザーに配布できるようになりました。

5. GUIアプリ開発のポイント

ここまでの開発を通して、いくつかの重要なポイントが見えてきました。

まず、ユーザーの視点に立った設計が重要です。コマンドラインスクリプトからGUIアプリにする際、単に見た目を変えるだけでなく、使いやすさも考える必要があります。ファイル選択ダイアログ、進捗表示、エラーメッセージなど、ユーザーが迷わない工夫を取り入れました。

次に、例外処理の徹底です。ファイルが存在しない、zipファイル内にログがない、文字エンコーディングの問題など、様々な例外に対応することで、アプリの安定性が大幅に向上します。

最後に、非同期処理の実装です。ファイル処理中もUIが固まらないようにすることで、ユーザー体験が格段に良くなります。特に大きなファイルを処理する場合、この違いは顕著です。

5.1. まとめ

シンプルなコマンドラインスクリプトからスタートして、便利なGUIアプリへと発展させました。ファイル選択の簡易化、zipファイルの直接処理、進捗表示の追加など、様々な機能強化によって使いやすさが向上しています。

またPy2appを使ったmacOS用アプリへのパッケージング方法も紹介しました。この手法は他のPythonスクリプトにも応用できるため、お気に入りのスクリプトをGUIアプリ化する際の参考になるでしょう。

TkinterとPy2appを組み合わせることで、Pythonの手軽さとネイティブアプリの使いやすさを両立できます。初心者にも扱いやすいPythonの特性を活かしつつ、一般ユーザーにも使ってもらえるアプリ作りが可能になるのです。