フォルダにファイルを入れたら、そのタイミングでPythonプログラムを自動で動かしたい。
こういった時に使えるのが、Pythonのwatchdogです。
今回作ったプログラムでは、監視しているwatchフォルダにファイルを入れると自動で検知して、
- ファイルの種類を判定
- ファイル名を変更
- 種類ごとのフォルダへ振り分け
- 日付ごとのフォルダへ移動
という処理をしています。
動画ではCSVファイルを使いましたが、PDFや画像なども同じように検知して処理することが出来ます。
このファイル整理自体も便利なのですが、個人的にはwatchdogの一番使いやすいところは、
かなと思います。
ファイル整理だけではなく、その後に自分で作ったPythonの処理をつなげることが出来るので、知っておくとかなり使いやすいです。
ちなみにwatchdogが、どういったことをやっているかの動画になります。
最初に見るとイメージがつきやすいと思います。
watchdogって何?
watchdogは、指定したフォルダやファイルの変化を監視するためのPythonライブラリです。
今回のイメージとしては、
watchフォルダを監視
→ファイルが追加されたことを検知
→自分で作った処理を実行
→ファイル名を変更してorganizeフォルダへ移動
こういった流れになります。
普通にPythonファイルを実行すると、書かれている処理が終わればプログラムも終了します。
でも今回のプログラムではwatchdogでフォルダを監視し続けて、ファイルが追加されたタイミングを検知して処理を始めています。
そのため自分で毎回Pythonを起動しなくても、監視フォルダへファイルを入れるだけで次の処理を始めることが出来るわけですね。
watchdogをインストール
watchdogはPythonの標準ライブラリではないので、最初にインストールが必要です。
コマンドプロンプトやターミナルで以下を実行してください。
pip install watchdogインストールが出来たら、Python側からObserverやFileSystemEventHandlerを読み込んで使っていきます。
今回のプログラムでやっていること
今回のプログラムは少し長いのですが、処理の流れを分けて考えるとそこまで複雑ではないです。
大きく分けると、
- 監視するフォルダと移動先を決める
- ファイルの種類を判定する
- ファイルのコピーが終わるまで待つ
- 移動先と新しいファイル名を作る
- ファイルを移動する
- watchdogでファイル追加を検知する
- 監視を止めずに動かし続ける
こういった内容です。
今回使用するプログラム全文
import shutil
import time
from datetime import datetime
from pathlib import Path
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
# =============================================
# 設定
# =============================================
WATCH_DIR = Path.cwd() / "watch_folder"
OUTPUT_DIR = Path.cwd() / "organized"
# 同じ名前のファイルが来たときに重複しないよう、時刻まで付けます。
FILENAME_FORMAT = "%Y%m%d_%H%M%S"
# コピー途中のファイルを処理しないための待機設定
CHECK_INTERVAL = 0.5
STABLE_CHECK_COUNT = 3
MAX_WAIT_SECONDS = 30
# 拡張子ごとの分類名
CATEGORY_MAP = {
".jpg": "images",
".jpeg": "images",
".png": "images",
".webp": "images",
".gif": "images",
".bmp": "images",
".pdf": "pdf",
".csv": "csv",
".xlsx": "excel",
".xls": "excel",
".xlsm": "excel",
".docx": "word",
".doc": "word",
".txt": "text",
".mp4": "video",
".mov": "video",
".webm": "video",
".mkv": "video",
".mp3": "audio",
".wav": "audio",
".zip": "archive",
".7z": "archive",
".rar": "archive",
}
# =============================================
# 補助関数
# =============================================
def ensure_directories() -> None:
WATCH_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
def get_category(path: Path) -> str:
return CATEGORY_MAP.get(path.suffix.lower(), "other")
def wait_until_file_is_stable(path: Path) -> bool:
"""コピー中のファイルを触らないよう、サイズが一定になるまで待つ。"""
stable_count = 0
last_size = -1
started = time.time()
while time.time() - started < MAX_WAIT_SECONDS:
if not path.exists():
return False
try:
current_size = path.stat().st_size
except OSError:
time.sleep(CHECK_INTERVAL)
continue
if current_size == last_size:
stable_count += 1
if stable_count >= STABLE_CHECK_COUNT:
return True
else:
stable_count = 0
last_size = current_size
time.sleep(CHECK_INTERVAL)
return False
def build_destination(source: Path) -> Path:
now = datetime.now()
category = get_category(source)
# organized / 種類 / YYYY-MM-DD /
target_dir = OUTPUT_DIR / category / now.strftime("%Y-%m-%d")
target_dir.mkdir(parents=True, exist_ok=True)
base_name = now.strftime(FILENAME_FORMAT)
suffix = source.suffix.lower()
destination = target_dir / f"{base_name}{suffix}"
# 同じ秒に複数ファイルが来ても上書きしない
counter = 1
while destination.exists():
destination = target_dir / f"{base_name}_{counter:02d}{suffix}"
counter += 1
return destination
def organize_file(path: Path) -> None:
if not path.is_file():
return
# 一時ファイルや隠しファイルを簡易除外
if path.name.startswith("~$") or path.name.startswith("."):
return
print(f"[検知] {path.name}")
if not wait_until_file_is_stable(path):
print(f"[スキップ] ファイルが安定しませんでした: {path}")
return
destination = build_destination(path)
try:
shutil.move(str(path), str(destination))
print(f"[整理完了] {path.name} -> {destination.relative_to(Path.cwd())}")
except Exception as exc:
print(f"[エラー] {path.name}: {exc}")
# =============================================
# watchdogイベント
# =============================================
class FileCreatedHandler(FileSystemEventHandler):
def on_created(self, event):
if event.is_directory:
return
organize_file(Path(event.src_path))
def on_moved(self, event):
# OSやアプリによっては一時ファイルからリネームされて配置されることがあるため対応
if event.is_directory:
return
organize_file(Path(event.dest_path))
# =============================================
# メイン
# =============================================
def main() -> None:
ensure_directories()
print("=" * 60)
print("ファイル自動整理ツール")
print("=" * 60)
print(f"監視フォルダ : {WATCH_DIR}")
print(f"整理先 : {OUTPUT_DIR}")
print()
print("watch_folder にファイルを置くと、自動でリネームして整理します。")
print("終了する場合は Ctrl + C を押してください。")
print()
observer = Observer()
observer.schedule(FileCreatedHandler(),
str(WATCH_DIR),
recursive=False)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n終了します...")
observer.stop()
observer.join()
if __name__ == "__main__":
main()
この後は、このプログラムの各部分が何をしているのかを順番に見ていきます。
使っているライブラリの解説
今回のプログラムでは、watchdog以外にもファイル操作や日時取得のためにいくつかのライブラリを使っています。
主に使っているのが、
- shutil
- time
- datetime
- pathlibのPath
- watchdogのFileSystemEventHandler
- watchdogのObserver
このあたりです。
shutil
shutilはファイルやフォルダのコピー・移動などで使うPythonの標準ライブラリです。
今回は最終的にファイルを別のフォルダへ移動するので、その処理で使っています。
time
timeは単純な時間計測をしたいから入れているわけではありません。
ファイルをコピーした直後に処理を始めると、まだコピーが終わっていない状態でPythonがファイルを触ってしまうことがあります。
特に画像などファイルサイズが大きくなると、フォルダ上にはファイルが見えていても、まだコピー中ということがあります。
そこで少し待ってから再確認するためにtimeを使っています。
datetime
今回は移動先のフォルダやファイル名に日付・時刻を使っているので、その取得にdatetimeを使っています。
例えば、
- 年ごとのフォルダ
- 年月日ごとのフォルダ
- 時刻を含めたファイル名
こういった部分ですね。
pathlibのPath
ファイルパスを扱う部分ではPathを使っています。
今回のようにファイル名、拡張子、親フォルダなどを何度も扱うプログラムでは、文字列だけでパスを処理するよりも分かりやすくなります。
FileSystemEventHandlerとObserver
ここがwatchdogのメインになる部分です。
Observerは指定したフォルダを監視する役割です。
そしてフォルダ内でファイルが作られた、移動されたといった変化が起きた時に、そのイベントをFileSystemEventHandler側で受け取って処理します。
イメージとしては、
Observerがフォルダを監視
→何か変化があったことを検知
→FileSystemEventHandler側の処理を実行
という感じですね。
ファイルの種類ごとに振り分ける
今回のプログラムでは、CSVだけではなく画像やPDFなども振り分けられるようにしています。
そのため最初に、
- jpg・jpeg・png・webpなど → images
- pdf → pdf
- csv → csv
というように、拡張子と移動先カテゴリの組み合わせを設定しています。
ただ、ここは全部用意する必要はありません。
自分が使いたいファイルだけに絞って、例えば、
- PNG
- CSV
- Excel
- テキストファイル
だけを設定しても大丈夫です。
ファイルを検知したら拡張子を取得して、設定してあるカテゴリのどれに当てはまるかを確認します。
動画内では大文字・小文字が混ざっていても判定出来るように、拡張子を小文字にしてから比較する形にしています。
これで例えばCSVならCSVフォルダ、画像ならimagesフォルダというように、移動先を決めることが出来ます。
ファイルのコピーが終わるまで待つ
今回のプログラムで、個人的に入れておいた方がいいと思うのがこの処理です。
def wait_until_file_is_stable(path: Path) -> bool:
"""コピー中のファイルを触らないよう、サイズが一定になるまで待つ。"""
stable_count = 0
last_size = -1
started = time.time()
while time.time() - started < MAX_WAIT_SECONDS:
if not path.exists():
return False
try:
current_size = path.stat().st_size
except OSError:
time.sleep(CHECK_INTERVAL)
continue
if current_size == last_size:
stable_count += 1
if stable_count >= STABLE_CHECK_COUNT:
return True
else:
stable_count = 0
last_size = current_size
time.sleep(CHECK_INTERVAL)
return False
フォルダ監視では、ファイルが追加されたこと自体はすぐに検知出来ます。
ただ、
検知した瞬間にファイルのコピーが完全に終わっているとは限りません。例えば大きめの画像ファイルをコピーした場合、コピー途中なのにwatchdog側ではファイルが作られたことを検知することがあります。
その状態で名前変更や移動を始めると、エラーの原因になります。
そこで今回のプログラムではファイルサイズを確認しています。
流れとしては、
- 現在のファイルサイズを取得
- 少し待つ
- もう一度ファイルサイズを取得
- 前回と同じサイズか確認
- 連続してサイズが変わらなければコピー完了と判断
という形です。
動画内では0.5秒ごとに確認して、3回連続でサイズが変わらなければ安定したと判断するようにしています。
1回だけ同じだった場合、たまたま確認したタイミングでサイズが変わっていなかっただけかもしれません。
なので何回か連続で確認するようにしているわけですね。
また、ファイルが途中でなくなった場合や、ファイルへアクセス出来なかった場合にも、そのまま処理を進めないようにしています。
自分しか使わない簡単なプログラムなら、最初に数秒待つだけでも動くことはあります。
ただ、こういったファイル監視のプログラムでは「ファイルが本当に使える状態になったか」を確認しておくと、エラーがかなり減らしやすいです。
移動先フォルダとファイル名を作る
ファイルが安定したら、次に移動先を作ります。
今回のプログラムでは、取得したファイルカテゴリと日時を使ってフォルダを作っています。
例えばCSVなら、
organize/csv/年/年月日/移動したCSVファイル
というような形です。
最初に対象となるフォルダが存在するかを確認して、なければ作成します。
そのうえで日時から新しいファイル名を作り、移動先のフルパスを作成しています。
同じ名前のファイルがあった場合
自動でファイル名を変更する場合、同じ名前が作られる可能性があります。
そのため今回のプログラムでは、移動先に同じファイル名があるかを確認して、すでに存在していれば番号を付けるようにしています。
これで同じタイミングに近いファイルが複数入っても、上書きしないように出来ます。
実際にファイルを整理する処理
ここまでで、
- ファイルの種類を判定する
- コピー完了を確認する
- 移動先を作る
という準備が出来ました。
最後にshutil.move()を使って、元のファイルから作成した移動先へファイルを移動します。
ただし、監視フォルダの中に入ったものを全部処理すると困るので、最初に処理対象かどうかも確認しています。
例えば、
- フォルダそのもの
- 一時ファイル
- ドットから始まるファイル
などは除外するようにしています。
Excelなどを開いた時にも一時的なファイルが作られることがあります。
そういったファイルまで自動整理の対象にすると、意図しない動きになる可能性があります。
自分の環境で「これは処理したくない」というファイルがある場合には、この除外条件を追加していけば大丈夫です。
watchdogでファイル追加を検知する
ここまでの処理は、ファイルを整理するための普通のPythonプログラムです。
プログラムのこの部分ですね。
# =============================================
# watchdogイベント
# =============================================
class FileCreatedHandler(FileSystemEventHandler):
def on_created(self, event):
if event.is_directory:
return
organize_file(Path(event.src_path))
def on_moved(self, event):
# OSやアプリによっては一時ファイルからリネームされて配置されることがあるため対応
if event.is_directory:
return
organize_file(Path(event.dest_path))
今回の重要なところは、
その処理をいつ実行するのかという部分です。
そこでFileSystemEventHandlerを継承したクラスを作って、ファイルが追加された時の処理を設定しています。
基本となるのがon_createdで、監視しているフォルダにファイルが作られた時に呼ばれます。
また、OSやアプリ側の動きによってはファイルが移動・名前変更された形で配置されることもあるので、今回のプログラムではon_moved側でも処理出来るようにしています。
そしてこのイベントが発生したら、先ほど作ったファイル整理の処理を実行します。
ファイル追加を検知
→コピー完了を確認
→カテゴリを判定
→移動先とファイル名を作成
→ファイルを移動
こういう流れになるわけですね。
Observerで監視を開始
最後にObserverへ、
- どのイベント処理を使うか
- どのフォルダを監視するか
- サブフォルダまで監視するか
を設定して監視を開始します。
今回のように監視フォルダ直下だけを見たい場合は、サブフォルダまで再帰的に監視する必要はありません。
逆にwatchフォルダの中にさらにフォルダを作って、その中まで監視したい場合には設定を変更出来ます。
Observerをスタートさせた後は、そのままだとPythonプログラム自体が終了してしまうので、while Trueとtime.sleep(1)を使ってプログラムを動かし続けています。
終了したい時にはCtrl + Cで止めて、KeyboardInterruptを受けたらObserverも終了させる流れです。
watchdogはファイル整理以外にも使える
今回のサンプルでは、ファイル名を変更してフォルダへ整理する処理を作りました。
でもwatchdogで重要なのは、整理処理そのものではないです。
ファイル追加を検知した後に実行する処理は、自分で自由に変更出来ます。
僕が使っている例だと、Chromeなどからダウンロードしたファイルを監視して、日付やファイルの種類ごとに分けるような処理があります。
AIで画像を作る時にも、決まった場所へ画像が出力されたら読み込んで、必要な処理をして別のファイルへ置き換えるような使い方をすることがあります。
仕事であれば、顧客情報のCSVを受け取った時に中身を読み込んで、その内容によって別々のフォルダへ振り分けることも出来ます。
例)画像を自動でWebPへ変換する
サイトやブログで使う画像も分かりやすい例です。
スマホで撮影した画像は、そのままだとファイルサイズがかなり大きいことがあります。
その画像を毎回、
- 画像を開く
- 必要なサイズにリサイズ
- WebPへ変換
- サイト用のフォルダへ保存
と手作業でやるのは面倒ですよね。
そこで画像用の一時フォルダを監視して、そこへ画像を入れたら自動でリサイズ・WebP変換・移動まで行うようにすることも出来ます。
watchdogがやっているのはあくまで、
という部分です。
その後に何をするかは自分で作れるので、
ファイルを入れる
→次の仕事が自動で始まる
という仕組みを作れるのが便利なところかなと思います。
今回はPythonのwatchdogを使って、フォルダへのファイル追加を検知して自動処理する流れを解説しました。
プログラム全体を見ると少し長く感じますが、watchdogの部分だけを考えると、Observerで監視してイベントを受け取り、自分の処理を呼び出しているだけです。
今回なら自分の処理がファイル整理になっていますが、ここをCSVの読み込みや画像変換などに変えれば、別の自動化にもそのまま応用出来ます。
日常の作業で「このファイルを置いた後、毎回同じことをしている」というものがあれば、かなり相性がいいと思います。
かなり応用が出来るライブラリなので、ぜひ活用してみてくださいね。
