ファイル更新を監視してイベントフックする【Python x Watchdog】

ファイル更新を監視してイベントフックする【Python x Watchdog】
ファイル更新を監視してイベントフックする【Python x Watchdog】

この記事ではPythonの Watchdog モジュールをつかって、ファイルの変更(作成、削除、更新など)を監視し、ファイルが更新されたら登録した関数を呼び出す方法をご紹介いたします。

はじめに

このサイトはPythonで作った自作の静的サイトジェネレータを使ってMarkdownファイルをHTMLへ変換して書き出してます。 これまで、文章を書き換えるたびにターミナルから手動でコンパイルを実行していました。頻繁にブラウザで見栄えをチェックしようとすると、ターミナルでのコンパイル実行作業がワンクッション挟むため煩わしい部分がありました。

そこでタイトルの通り、ファイルの更新を監視して変更があったらコンパイルできないか考えました。オープンソースの静的サイトジェネレータならば当たり前についている自動コンパイル機能ですが、自前で用意しようとするとちょっと考えちゃいますよね。

最初に考えたのは、該当ファイル群のパスを paths = glob.glob(root_dir + "/**/*.md", recursive=True) で検索しファイルに登録されている更新時間をmtime = os.stat(path).st_mtime で取得して、パスをキーでハッシュ化、csvファイルに保存して前回の更新履歴と比較する方法を試してみました。これを2〜3秒に一度まわしてたのですが、CPU負荷が高くなってしまい効率が悪い感じです。

どうにかならないかと悩んでいたところ、カーネルレベルでファイルの状態を監視してくれるAPIがあることを知りました。Pythonでもたとえば watchdog というライブラリを使えば、その機能を実現できるということでさっそく試してみました。

Watchdogのインストール

みてるでー
みてるでー

pipを使ってWatchdogライブラリをインストールします。ここではPython3を使いましたので、pip3でインストールします。

shell
$ pip3 install watchdog

watchdog · PyPI

Watchdogでファイルが更新されたらイベントフックするプログラム例

インストールできたら適当な名前でPythonファイルを作成します。 watchdog.py という名前にしてしまうと、名前がバッティングして実行できませんのでご注意ください。

ファイルの更新があれば、登録したコールバック関数が呼び出されます。

test_watchdog.py
"""
Hook on_modified event and call a callback function when files is updated.
"""
import time
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler

class WatchdogHandler(PatternMatchingEventHandler):
    def __init__(self, callback, patterns):
        super(WatchdogHandler, self).__init__(patterns=patterns)
        self.callback = callback

    def __callback_handler(self, func,*args):
        return func(*args)

    # def on_moved(self, event):
    #     print(event)
    #     self.__callback_handler(self.callback)

    # def on_created(self, event):
    #     print(event)
    #     self.__callback_handler(self.callback)

    # def on_deleted(self, event):
    #     print(event)
    #     self.__callback_handler(self.callback)

    def on_modified(self, event):
        print(event)
        self.__callback_handler(self.callback)

def watch(path, callback, extensions):
    event_handler = WatchdogHandler(callback, extensions)
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

def say_hello():
    print("hello")

if __name__ == "__main__":
    dir_to_watch = "/somewhere"
    extensions = ["*.md"]
    watch(dir_to_watch, say_hello, extensions)

プログラムはそんなに難しくありませんでした。PatternMatchingEventHandler を実装したハンドラー(WatchdogHandler)をつくり、イベントをフックしたい関数でコールバック関数を実行させてます。

Observerでファイルを監視するディレクトリと拡張子を指定します。

ここでは、/somewhere ディレクトリ下にある.mdファイルの更新を監視するように命じてますね。

on_modified 関数で受け取れる event にはイベントタイプや、ディレクトリか否か、ファイルパスが格納されてます。このように。
<FileModifiedEvent: event_type=modified, src_path='/somewhere/python-watchdog-event.md', is_directory=False>

たとえば event.src_path でファイルのパスを知ることができます。

Watchdogモジュールのおかげで、やりたいことが簡単に実現できました。これならもっと早く導入しておくべきでしたね。みなさんも機会があればぜひ使ってください。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

Python学習にオススメの本をご紹介!
Pandasでデータサイエンスはじめよう!
スクレイピングにオススメの書籍

▼ Beautiful Soup4を使ったWebクローリングをはじめ、表データをpandasやOpenPyXL、matplotでデータ解析、グラフ表示などのスクレイピングのやり方が分かりやすく説明されてます。図解が多いのでPython初心者の方でも読み進められる内容となってます。