ファイル変更をリアルタイム監視＆ログ出力するツールを作ってみた

## はじめに

初めましての人もそうでない人もこんにちは！
最近コーディング中に「あれ、いつこのファイル変更したっけ？」「どの部分を修正したか忘れちゃった...」なんて経験ありませんか？

そんな悩みを解決するべく、今回はPythonでファイルの変更をリアルタイムで監視して、詳細なログを出力するツールを作成してみました！

ファイルの作成・削除・変更を文字単位で追跡できる超便利なツールになっているので、ぜひ最後までご覧ください！

## 今回作るもの

- ファイルの作成・削除・変更をリアルタイムで監視
- 文字の追加・削除・置換を詳細にログ出力
- CSVファイルで変更履歴を管理
- 対応ファイル形式：`.py`, `.txt`, `.js`, `.html`, `.css`, `.json`

## ディレクトリ構成

```
/
│
├── file_monitor.py      # メインのプログラム
├── logs/
│   └── logs.csv        # ログ収集用ファイル（自動生成）
└── files/              # 監視対象フォルダ（自動生成）
    ├── hogehoge.py
    ├── hogehoge.txt
    └── ...
```

## 環境構築

必要なライブラリをインストールします：

```bash
pip install watchdog pytz
```

## コーディング

メインのプログラムファイル `file_monitor.py` を作成します：

```python
import csv
import os
from datetime import datetime
import pytz
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class FileChangeHandler(FileSystemEventHandler):
    def __init__(self, logger):
        self.logger = logger
        self.file_contents = {}
    
    def on_modified(self, event):
        if not event.is_directory and event.src_path.endswith(('.py', '.txt', '.js', '.html', '.css', '.json')):
            filename = os.path.basename(event.src_path)
            try:
                with open(event.src_path, 'r', encoding='utf-8') as f:
                    new_content = f.read()
                old_content = self.file_contents.get(event.src_path, '')
                if old_content != new_content:
                    self.compare_content(filename, old_content, new_content)
                    self.file_contents[event.src_path] = new_content
            except:
                pass
    
    def compare_content(self, filename, old_content, new_content):
        if len(new_content) > len(old_content):
            added_text = new_content[len(old_content):]
            line_num, col_num = self.get_position(old_content, len(old_content))
            for i, char in enumerate(added_text):
                if char == '\n':
                    self.logger.log(filename, '入力', "追加: '改行'", line_num, col_num + i + 1)
                    line_num += 1
                    col_num = 0
                    i = -1
                else:
                    self.logger.log(filename, '入力', f"追加: '{char}'", line_num, col_num + i + 1)
        elif len(new_content) < len(old_content):
            deleted_text = old_content[len(new_content):]
            line_num, col_num = self.get_position(new_content, len(new_content))
            for char in deleted_text:
                self.logger.log(filename, '削除', f"削除: '{char}'" if char != '\n' else "削除: '改行'", line_num, col_num + 1)
        else:
            for i, (old_char, new_char) in enumerate(zip(old_content, new_content)):
                if old_char != new_char:
                    line_num, col_num = self.get_position(old_content, i)
                    self.logger.log(filename, '置換', f"'{old_char}' → '{new_char}'", line_num, col_num)
    
    def get_position(self, content, index):
        if index >= len(content):
            lines = content.split('\n')
            return len(lines), len(lines[-1]) + 1 if lines else 1
        text_before = content[:index]
        lines = text_before.split('\n')
        return len(lines), len(lines[-1]) + 1
    
    def on_created(self, event):
        if not event.is_directory:
            self.logger.log(os.path.basename(event.src_path), 'ファイル作成', f"ファイル作成: {os.path.basename(event.src_path)}", 0, 0)
            try:
                with open(event.src_path, 'r', encoding='utf-8') as f:
                    self.file_contents[event.src_path] = f.read()
            except:
                pass
    
    def on_deleted(self, event):
        if not event.is_directory:
            self.logger.log(os.path.basename(event.src_path), 'ファイル削除', f"ファイル削除: {os.path.basename(event.src_path)}", 0, 0)
            self.file_contents.pop(event.src_path, None)

class FileLogger:
    def __init__(self):
        os.makedirs("logs", exist_ok=True)
        os.makedirs("files", exist_ok=True)
        
        with open("logs/logs.csv", 'w', newline='', encoding='utf-8') as f:
            csv.writer(f).writerow(['時刻', 'ファイル名', '操作', '内容', '行', '列'])
    
    def log(self, filename, action, content, line=0, column=0):
        timestamp = datetime.now(pytz.timezone('Asia/Tokyo')).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
        with open("logs/logs.csv", 'a', newline='', encoding='utf-8') as f:
            csv.writer(f).writerow([timestamp, filename, action, content, line, column])
    
    def start_monitor(self):
        event_handler = FileChangeHandler(self)
        
        for root, dirs, files in os.walk("files"):
            for file in files:
                if file.endswith(('.py', '.txt', '.js', '.html', '.css', '.json')):
                    filepath = os.path.join(root, file)
                    try:
                        with open(filepath, 'r', encoding='utf-8') as f:
                            event_handler.file_contents[filepath] = f.read()
                    except:
                        pass
        
        observer = Observer()
        observer.schedule(event_handler, "files", recursive=True)
        observer.start()
        
        print("ファイル監視を開始しました。Ctrl+Cで終了します。")
        print("監視対象フォルダ: ./files/")
        print("ログファイル: ./logs/logs.csv")
        
        try:
            import time
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            observer.stop()
            print("\nファイル監視を終了しました。")
        observer.join()

if __name__ == "__main__":
    FileLogger().start_monitor()
```

## 実行してみた

プログラムを実行します：

```bash
python3 file_monitor.py
```

実行すると以下のようなメッセージが表示されます：

```
ファイル監視を開始しました。Ctrl+Cで終了します。
監視対象フォルダ: ./files/
ログファイル: ./logs/logs.csv
```

`files/` フォルダ内で以下のようなテストファイルを作成・編集してみました：

### test.py を作成

```python
print("Hello World")
```

### sample.txt を編集

```
最初のテキスト
追加のテキスト
```

### ログ出力結果

`logs/logs.csv` に以下のようなログが出力されました：

| 時刻 | ファイル名 | 操作 | 内容 | 行 | 列 |
|------|-----------|------|------|----|----|
| 2025-05-30 15:30:01.123 | test.py | ファイル作成 | ファイル作成: test.py | 0 | 0 |
| 2025-05-30 15:30:15.456 | test.py | 入力 | 追加: 'p' | 1 | 1 |
| 2025-05-30 15:30:15.457 | test.py | 入力 | 追加: 'r' | 1 | 2 |
| 2025-05-30 15:30:15.458 | test.py | 入力 | 追加: 'i' | 1 | 3 |
| ... | ... | ... | ... | ... | ... |

## 特徴・機能

### リアルタイム監視
- ファイルの変更を瞬時に検知
- 複数ファイルの同時監視が可能

### 詳細なログ記録
- 文字単位での変更追跡
- 行番号・列番号の正確な位置情報
- 時系列管理

### 多くの操作ログの収集に対応
- **文字追加:** 新しく入力された文字を記録
- **文字削除:** 削除された文字を記録
- **文字置換:** 変更前後の文字を記録
- **ファイル作成/削除:** ファイル操作も記録

## おわりに
今回は Python と watchdog ライブラリを使って、ファイル変更をリアルタイムで監視するツールを作成しました！
文字単位での詳細な変更追跡ができるので、コーディング中の変更履歴をしっかりと記録できます。
開発効率の向上やデバッグ支援にぜひ活用してみてください！

皆さんも試してみてはいかがでしょうか？
今回の記事はいかがだったでしょうか！
またどこかの記事でお会いしましょう！