ファイル更新を監視してイベントフックする【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でインストールします。
$ pip3 install watchdog
Watchdogでファイルが更新されたらイベントフックするプログラム例
インストールできたら適当な名前でPythonファイルを作成します。 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モジュールのおかげで、やりたいことが簡単に実現できました。これならもっと早く導入しておくべきでしたね。みなさんも機会があればぜひ使ってください。