PySide Python

watchdog と PySide でディレクトリ監視を UI で制御する

watchdog を PySide で利用し、
ディレクトリの監視を UI で制御してみます。

watchdog について

インストール

pip install watchdog

今回作るのは下記のようなものです。

watchdog が指定したディレクトリを監視し、
そのディレクトリ内でフォルダやファイルの作成・更新・移動・削除があった場合にそれを検知して知らせるところまでを実装します。

実装

UI の作成

import sys
import os

from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt


class UI(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(UI, self).__init__(parent)

        self.resize(QtCore.QSize(500, 100))

        root_Area = QtWidgets.QHBoxLayout()
        self.setLayout(root_Area)

        # パス入力欄
        self.path_Line = QtWidgets.QLineEdit()
        root_Area.addWidget( self.path_Line)

        # 開始ボタン
        self.start_Btn = QtWidgets.QPushButton('開始')
        self.start_Btn.clicked.connect(self.start)
        root_Area.addWidget(self.start_Btn)

        # 停止ボタン
        self.stop_Btn = QtWidgets.QPushButton('停止')
        self.stop_Btn.clicked.connect(self.stop)
        root_Area.addWidget(self.stop_Btn)


    # 監視開始
    def start(self):

        # 文字列の取得
        path = self.path_Line.text()

        # パスが存在する場合
        if os.path.isdir(path):
            pass


    # 監視の停止
    def stop(self):
        pass


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ui = UI()
    ui.show()
    sys.exit(app.exec_())

かなりシンプルな UI にしました。

ラインエディットに入れたディレクトリを監視する内容にしていきます。

監視の実装

とりあえず全体のコードがこちら。

import sys
import os

from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt

# from watchdog.observers import Observer # 通常のオブザーバー
from watchdog.observers.polling import PollingObserver
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, FileSystemEventHandler


class UI(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(UI, self).__init__(parent)

        self.resize(QtCore.QSize(500, 100))

        root_Area = QtWidgets.QHBoxLayout()
        self.setLayout(root_Area)

        # パス入力欄
        self.path_Line = QtWidgets.QLineEdit()
        root_Area.addWidget( self.path_Line)

        # 開始ボタン
        self.start_Btn = QtWidgets.QPushButton('開始')
        self.start_Btn.clicked.connect(self.start)
        root_Area.addWidget(self.start_Btn)

        # 停止ボタン
        self.stop_Btn = QtWidgets.QPushButton('停止')
        self.stop_Btn.clicked.connect(self.stop)
        root_Area.addWidget(self.stop_Btn)

        # self.__observer = Observer() # 通常のオブザーバーの作成
        self.__observer = PollingObserver()

    # 監視開始
    def start(self):

        # 文字列の取得
        path = self.path_Line.text()

        if os.path.isdir(path):

            #  起動中のスレッドを停止してから新たにObserver作成
            if self.__observer.is_alive():
                self.__observer.stop()
                self.__observer.join()

            # オブザーバーの作成とイベントハンドラへ接続
            # self.__observer = Observer() # 通常のオブザーバーの作成
            self.__observer = PollingObserver()
            self.__observer.schedule(WatchdogEvent(), path, True)
            self.__observer.start()
            print('監視開始')


    # 監視の停止
    def stop(self):
        if self.__observer.is_alive():
            self.__observer.stop()
            self.__observer.join()
            print('監視終了')



# 監視中の挙動
class WatchdogEvent(FileSystemEventHandler):


   # 新規作成された場合 のイベント
   def on_created(self, event):
       src_path = event.src_path
       print('created', src_path)

   # 更新された場合 のイベント
   def on_modified(self, event):
       src_path = event.src_path
       print('modified', src_path)

   # 移動させた際のイベント
   def on_moved(self, event):
       src_path = event.src_path
       print('moved', src_path)

   # 削除した際のイベント
   def on_deleted(self, event):
       src_path = event.src_path
       print('deleted', src_path)




if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ui = UI()
    ui.show()
    sys.exit(app.exec_())

監視中のディレクトリ内のファイルを編集するとそれに応じた内容が出力されるはずです。

オブザーバー

Observer と PollingObserver が監視を行うクラスです。
違いは後述しますが、基本的にはどちらかひとつを使用します。上記のコードは PollingObserver を利用しています。

# self.__observer = Observer() # 通常のオブザーバーの作成
self.__observer = PollingObserver()

下記のインポート文は使用するオブザーバーのみで構いません。
恐らく PollingObserver を使用するケースの方が多いのではないでしょうか。

# from watchdog.observers import Observer # 通常のオブザーバー
from watchdog.observers.polling import PollingObserver

主要メソッド

.is_alive()スレッドが生きているかどうかを判別する。
.start()監視の開始
.stop()監視の停止
.join()スレッドが終了するまで待つ。停止したあとはこれを実行しておく。

イベントハンドラ

on_created作成時のイベント
on_modified更新時のイベント
on_moved移動時のイベント
on_deleted消去時のイベント

イベントハンドラは名前のままです。

event.src_path で変更のあった ファイル/フォルダ を取得できます。

通常のオブザーバーとプーリングオブザーバー

先ほどのコードはプーリングオブザーバーで監視するコードです。
オブザーバーを変えるとイベント内容が変わります。

ノーマルなオブザーバーのコード

import sys
import os

from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt

from watchdog.observers import Observer
#from watchdog.observers.polling import PollingObserver
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, FileSystemEventHandler


class UI(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(UI, self).__init__(parent)

        self.resize(QtCore.QSize(500, 100))

        root_Area = QtWidgets.QHBoxLayout()
        self.setLayout(root_Area)

        # パス入力欄
        self.path_Line = QtWidgets.QLineEdit()
        root_Area.addWidget( self.path_Line)

        # 開始ボタン
        self.start_Btn = QtWidgets.QPushButton('開始')
        self.start_Btn.clicked.connect(self.start)
        root_Area.addWidget(self.start_Btn)

        # 停止ボタン
        self.stop_Btn = QtWidgets.QPushButton('停止')
        self.stop_Btn.clicked.connect(self.stop)
        root_Area.addWidget(self.stop_Btn)

        self.__observer = Observer() # 通常のオブザーバーの作成
        #self.__observer = PollingObserver()

    # 監視開始
    def start(self):

        # 文字列の取得
        path = self.path_Line.text()

        if os.path.isdir(path):

            #  起動中のスレッドを停止してから新たにObserver作成
            if self.__observer.is_alive():
                self.__observer.stop()
                self.__observer.join()

            # オブザーバーの作成とイベントハンドラへ接続
            self.__observer = Observer() # 通常のオブザーバーの作成
            #self.__observer = PollingObserver()
            self.__observer.schedule(WatchdogEvent(), path, True)
            self.__observer.start()
            print('監視開始')


    # 監視の停止
    def stop(self):
        if self.__observer.is_alive():
            self.__observer.stop()
            self.__observer.join()
            print('監視終了')



# 監視中の挙動
class WatchdogEvent(FileSystemEventHandler):


   # 新規作成された場合 のイベント
   def on_created(self, event):
       src_path = event.src_path
       print('created', src_path)

   # 更新された場合 のイベント
   def on_modified(self, event):
       src_path = event.src_path
       print('modified', src_path)

   # 移動させた際のイベント
   def on_moved(self, event):
       src_path = event.src_path
       print('moved', src_path)

   # 削除した際のイベント
   def on_deleted(self, event):
       src_path = event.src_path
       print('deleted', src_path)




if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ui = UI()
    ui.show()
    sys.exit(app.exec_())

基本的には PollingObserver() だった部分が Observer() に変わっただけです。

ファイルをコピーした際の挙動

「test.txt」 をコピーして 「test - コピー.txt」を作ります。

Observer の場合

created D:\test\test - コピー.txt
modified D:\test\test - コピー.txt

create イベントと modified イベントが発生します。

PollinngObserver の場合

created D:\test\test - コピー.txt
modified D:\test

同様に create イベントと modified イベントが発生します。
が、modified はファイルではなくディレクトリに対して発生します。

Photoshop で上書き保存したときの挙動

「test.png」をフォトショップで上書き保存していきます。

Observer の場合

modified D:\test\test.png
created D:\test\psB361.tmp
modified D:\test\psB361.tmp
deleted D:\test\test.png
moved D:\test\psB361.tmp

フォトショップで png データを上書き保存する場合、一時データの tmp ファイルを作って上書き保存を行っています。

上記の出力結果から png データの部分だけを抜き出してみます。

modified D:\test\test.png
deleted D:\test\test.png

上書きする png ファイルに関するイベントはこの2つしか発生していません。
編集した後、png データを削除して終了しています。

そして、1行目の modified イベントで取得できるのは 上書き前のデータです。
その後の deleted イベントでも上書きしたデータは取得できません。

イベント内で src_path をコピーするコードを書くことで検証できます。

shutil.copy(src_path , 'D:/hoge')

Observer による監視ではフォトショップの上書き保存のイベントは受け取れない、ということです。

Observer は異なる処理が同時に起きた場合にイベントの発生漏れが起きます。

PollinngObserver の場合

そこででてくるのが PollingObserver の選択肢です。

deleted D:\test\test.png
created D:\test\test.png
modified D:\test

.tmp の一時データのイベントは受け取れませんが、上書きのイベントに関しては正確に受け取れます。

フォトショップの上書き保存はそのまま上書きしているのではなく、

  1. 元のデータを削除
  2. 新規でデータを作成

という方法で保存されています。
上書きしているように見えますが、実際には同名ファイルを削除した後に新規で同名のファイルを作成します。

Observer による監視では 2 の新規作成イベントが発生しなかった ということです。

Observer と PollingObserver は目的によって使い分けてください。

watchdog の処理負荷

負荷の程度は監視量で変わります。
メモリの負荷は気にするレベルではないです。

watchdog は指定したフォルダより下層にある全てのディレクトリを監視します。

この総量が多いほど CPU に負荷をかけます。

そのため、CPU の過負荷を防ぐにはできるだけ下層のディレクトリを監視する必要があります。

-PySide, Python