watchdog でファイルの変更を監視する

watchdog ライブラリを使うと、ファイルやディレクトリの変更をリアルタイムで監視できる。ログ監視、自動ビルド、同期処理などに便利だ。

インストール

pip install watchdog

基本的な使い方

ファイル変更を検知するには、イベントハンドラを定義して Observer に登録する。

import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class MyHandler(FileSystemEventHandler):
    def on_created(self, event):
        print(f'作成: {event.src_path}')
    
    def on_modified(self, event):
        print(f'変更: {event.src_path}')
    
    def on_deleted(self, event):
        print(f'削除: {event.src_path}')
    
    def on_moved(self, event):
        print(f'移動: {event.src_path} -> {event.dest_path}')

# 監視を開始
observer = Observer()
observer.schedule(MyHandler(), path='./watch_dir', recursive=True)
observer.start()

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    observer.stop()

observer.join()

recursive=True を指定すると、サブディレクトリも監視対象になる。

イベントの種類

watchdog は以下のイベントを検知する。

イベント説明
on_createdファイルまたはディレクトリが作成された
on_modifiedファイルまたはディレクトリが変更された
on_deletedファイルまたはディレクトリが削除された
on_movedファイルまたはディレクトリが移動/リネームされた

特定のファイルだけを監視する

パターンマッチングでフィルタリングする場合は PatternMatchingEventHandler を使う。

from watchdog.events import PatternMatchingEventHandler

class MyHandler(PatternMatchingEventHandler):
    def __init__(self):
        super().__init__(
            patterns=['*.py', '*.json'],
            ignore_patterns=['*.pyc', '__pycache__/*'],
            ignore_directories=True
        )
    
    def on_modified(self, event):
        print(f'Python/JSON ファイルが変更: {event.src_path}')

ディレクトリのみを監視する

event.is_directory でファイルとディレクトリを区別できる。

class DirOnlyHandler(FileSystemEventHandler):
    def on_created(self, event):
        if event.is_directory:
            print(f'ディレクトリ作成: {event.src_path}')

実用的な例:ファイル変更時に自動リロード

設定ファイルの変更を検知して自動でリロードする例を示す。

import json
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

config = {}

def load_config():
    global config
    with open('config.json') as f:
        config = json.load(f)
    print(f'設定を読み込みました: {config}')

class ConfigHandler(FileSystemEventHandler):
    def on_modified(self, event):
        if event.src_path.endswith('config.json'):
            load_config()

# 初期読み込み
load_config()

observer = Observer()
observer.schedule(ConfigHandler(), path='.', recursive=False)
observer.start()

イベントのデバウンス

ファイル保存時に複数のイベントが発火することがある。デバウンス処理を入れると、短時間の連続イベントをまとめられる。

import time
from threading import Timer

class DebouncedHandler(FileSystemEventHandler):
    def __init__(self, callback, delay=0.5):
        self.callback = callback
        self.delay = delay
        self.timer = None
    
    def on_modified(self, event):
        if self.timer:
            self.timer.cancel()
        self.timer = Timer(self.delay, self.callback, [event])
        self.timer.start()

def handle_change(event):
    print(f'処理: {event.src_path}')

handler = DebouncedHandler(handle_change, delay=0.5)

コンテキストマネージャで使う

クリーンアップを自動化するには、コンテキストマネージャでラップする。

from contextlib import contextmanager

@contextmanager
def watch_directory(path, handler, recursive=True):
    observer = Observer()
    observer.schedule(handler, path=path, recursive=recursive)
    observer.start()
    try:
        yield observer
    finally:
        observer.stop()
        observer.join()

# 使用例
with watch_directory('./watch_dir', MyHandler()) as observer:
    while True:
        time.sleep(1)