# アプリケーションの先頭に追加
import os
os.environ['tk_nomenus'] = "1" # tkinterメニューバーを無効化
import re
import zipfile
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from collections import defaultdict
import threading
import shutil
import tempfile
import datetime
# TkDNDをインポートする
from tkinterdnd2 import TkinterDnD, DND_FILES
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.original_file_name = ""
# UIの構築
self.setup_ui()
# ドラッグアンドドロップの設定
self.root.drop_target_register(DND_FILES)
self.root.dnd_bind('<<Drop>>', self.handle_drop)
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)
# ドラッグ&ドロップの案内ラベル
drop_label = ttk.Label(file_frame, text="ここにファイルをドラッグ&ドロップすることもできます")
drop_label.grid(row=1, column=0, columnspan=2, padx=5, pady=5)
# 出力オプション部分
options_frame = ttk.LabelFrame(self.root, text="出力オプション")
options_frame.pack(fill="x", padx=5, pady=5)
# ラジオボタンのための変数
self.output_option = tk.StringVar(value="latest") # デフォルトは最新
# ラジオボタン
all_days_radio = ttk.Radiobutton(
options_frame,
text="すべての日付ごとに出力",
variable=self.output_option,
value="all")
all_days_radio.grid(row=0, column=0, padx=5, pady=5, sticky="w")
latest_days_radio = ttk.Radiobutton(
options_frame,
text="最新の日付のみ出力",
variable=self.output_option,
value="latest")
latest_days_radio.grid(row=1, column=0, padx=5, pady=5, sticky="w")
ttk.Label(options_frame, text="最新の日数:").grid(row=1, column=1, padx=5, pady=5)
self.days_var = tk.StringVar(value="7")
days_spinbox = ttk.Spinbox(options_frame, from_=1, to=30, textvariable=self.days_var, width=5)
days_spinbox.grid(row=1, column=2, padx=5, pady=5)
# 実行ボタン
execute_button = ttk.Button(self.root, text="実行", command=self.execute)
execute_button.pack(pady=10)
# 進捗表示部分
progress_frame = ttk.LabelFrame(self.root, text="進捗状況")
progress_frame.pack(fill="both", expand=True, padx=5, pady=5)
self.progress_var = tk.StringVar(value="待機中...")
progress_label = ttk.Label(progress_frame, textvariable=self.progress_var)
progress_label.pack(pady=5)
self.progress_bar = ttk.Progressbar(progress_frame, orient="horizontal", mode="indeterminate")
self.progress_bar.pack(fill="x", padx=5, pady=5)
# ログ表示部分
log_frame = ttk.Frame(progress_frame)
log_frame.pack(fill="both", expand=True, padx=5, pady=5)
scrollbar = ttk.Scrollbar(log_frame)
scrollbar.pack(side="right", fill="y")
self.log_text = tk.Text(log_frame, height=10, yscrollcommand=scrollbar.set)
self.log_text.pack(side="left", fill="both", expand=True)
scrollbar.config(command=self.log_text.yview)
def handle_drop(self, event):
"""ドラッグ&ドロップされたファイルを処理する"""
# ドロップされたファイルのパスを取得
file_path = event.data
# WindowsやMacでの特殊な形式に対応
if file_path.startswith('{') and file_path.endswith('}'):
file_path = file_path[1:-1]
# 前後の空白や引用符を削除
file_path = file_path.strip('" ')
# ファイルパスを設定
self.file_path_var.set(file_path)
# 自動的に実行処理を開始
self.execute()
def browse_file(self):
file_path = filedialog.askopenfilename(
title="WhatsApp ログファイルまたはzipファイルを選択",
filetypes=[("テキストファイル", "*.txt"), ("Zipファイル", "*.zip"), ("すべてのファイル", "*.*")]
)
if file_path:
self.file_path_var.set(file_path)
def log(self, message):
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
def get_output_folder_name(self, input_file):
"""入力ファイルから出力フォルダ名を生成"""
# 現在の日時を取得
now = datetime.datetime.now().strftime("%Y%m%d")
# フォルダ名を生成(例:「20230501_WhatsApp Chats」)
return f"{now}_WhatsApp Chats"
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()
def process_file(self, input_file):
try:
# ログをクリア
self.log_text.delete(1.0, tk.END)
self.log(f"ファイル処理開始: {input_file}")
# 元のファイル名を保存(拡張子なし)
self.original_file_name = os.path.splitext(os.path.basename(input_file))[0]
# 「WhatsApp Chat - 」を除去
self.original_file_name = self.original_file_name.replace("WhatsApp Chat - ", "")
# 特殊文字を置換
self.original_file_name = re.sub(r'[\\/:*?"<>|]', '_', self.original_file_name)
# 入力ファイルの種類に応じた処理
if input_file.lower().endswith(".zip"):
self.log("Zipファイルを解凍中...")
log_file = self.extract_zip(input_file)
else:
log_file = input_file
self.log(f"ログファイル: {log_file}")
# ログ解析
self.log("ログファイルを解析中...")
days = self.parse_log(log_file)
self.log(f"解析完了: {len(days)} 日分のログが見つかりました")
# 出力フォルダ名を生成
folder_name = self.get_output_folder_name(input_file)
input_file_dir = os.path.dirname(input_file)
output_dir = os.path.join(input_file_dir, folder_name)
os.makedirs(output_dir, exist_ok=True)
self.log(f"出力フォルダを作成: {output_dir}")
# 選択されたオプションに応じて出力
option = self.output_option.get()
if option == "all":
self.log(f"すべての日付ごとにファイルを出力中: {output_dir}")
self.write_per_day(days, output_dir)
else: # latest
try:
n_days = int(self.days_var.get())
except ValueError:
n_days = 7
self.days_var.set("7")
self.log(f"最新 {n_days} 日分のファイルを出力中: {output_dir}")
self.write_latest_n_days(days, output_dir, n=n_days)
self.log("処理完了")
self.log(f"処理が完了しました。出力先: {output_dir}")
# 出力フォルダを開く
# macOSの場合
if os.path.exists("/usr/bin/open"):
os.system(f'open "{output_dir}"')
# Windowsの場合
elif os.name == 'nt':
os.system(f'explorer "{output_dir}"')
except Exception as e:
self.log(f"エラーが発生しました: {str(e)}")
self.root.after(0, lambda: messagebox.showerror("エラー", f"処理中にエラーが発生しました:\n{str(e)}"))
finally:
# 進捗表示を停止
self.root.after(0, self.progress_bar.stop)
self.root.after(0, lambda: self.progress_var.set("完了"))
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ファイル内にログファイルが見つかりませんでした。")
def parse_log(self, file_path):
# 修正した日付パターン(時間が1桁の場合にも対応)
day_pattern = re.compile(r'^\[(\d{4}/\d{2}/\d{2}) \d{1,2}:\d{2}:\d{2}\]')
days = defaultdict(list)
current_day = None # 現在処理中の日付
encodings = ['utf-8', 'shift_jis'] # 試す文字コードのリスト
for encoding in encodings:
days.clear()
current_day = None
try:
with open(file_path, encoding=encoding) as f:
for line in f:
line = line.rstrip()
m = day_pattern.match(line)
if m: # 新しいメッセージの開始行
current_day = m.group(1)
days[current_day].append(line)
elif current_day: # 前のメッセージの続き
# 最後のメッセージに追加
days[current_day][-1] = days[current_day][-1] + '\n' + line
# エラーなく読めた場合はループを抜ける
break
except UnicodeDecodeError:
# エラーが発生した場合は次の文字コードで試行
continue
return days
def write_per_day(self, days, output_dir):
# 日付ごとの出力処理
for day, lines in days.items():
output_file = os.path.join(output_dir, f"{day.replace('/', '-')}_{self.original_file_name}.txt")
with open(output_file, 'w', encoding='utf-8') as f:
for line in lines:
f.write(line + '\n')
self.log(f"出力: {output_file} ({len(lines)} 行)")
def write_latest_n_days(self, days, output_dir, n=7):
# 最新N日分の出力処理
sorted_days = sorted(days.keys(), reverse=True)
for day in sorted_days[:n]:
output_file = os.path.join(output_dir, f"{day.replace('/', '-')}_{self.original_file_name}.txt")
with open(output_file, 'w', encoding='utf-8') as f:
for line in days[day]:
f.write(line + '\n')
self.log(f"出力: {output_file} ({len(days[day])} 行)")
def cleanup(self):
# 一時ディレクトリの削除
try:
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
except:
pass
# アプリの実行
def main():
# TkinterDnDを使用
root = TkinterDnD.Tk()
app = WhatsAppLogSplitterApp(root)
# アプリ終了時のクリーンアップ処理
def on_closing():
app.cleanup()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
if __name__ == "__main__":
main()
show less