【Python】フォルダに入れたファイルを自動処理!watchdogの使い方


Python
投稿日:
【Python】フォルダに入れたファイルを自動処理!watchdogの使い方

フォルダにファイルを入れたら、そのタイミングでPythonプログラムを自動で動かしたい。

こういった時に使えるのが、Pythonのwatchdogです。

今回作ったプログラムでは、監視しているwatchフォルダにファイルを入れると自動で検知して、

という処理をしています。

動画ではCSVファイルを使いましたが、PDFや画像なども同じように検知して処理することが出来ます。

このファイル整理自体も便利なのですが、個人的にはwatchdogの一番使いやすいところは、

「ファイルが追加された」を次の処理のきっかけに出来ること

かなと思います。

ファイル整理だけではなく、その後に自分で作ったPythonの処理をつなげることが出来るので、知っておくとかなり使いやすいです。

ちなみにwatchdogが、どういったことをやっているかの動画になります。

最初に見るとイメージがつきやすいと思います。

クリックで動画を再生

watchdogって何?

watchdogは、指定したフォルダやファイルの変化を監視するためのPythonライブラリです。

今回のイメージとしては、

watchフォルダを監視

→ファイルが追加されたことを検知

→自分で作った処理を実行

→ファイル名を変更してorganizeフォルダへ移動

こういった流れになります。

普通にPythonファイルを実行すると、書かれている処理が終わればプログラムも終了します。

でも今回のプログラムではwatchdogでフォルダを監視し続けて、ファイルが追加されたタイミングを検知して処理を始めています。

そのため自分で毎回Pythonを起動しなくても、監視フォルダへファイルを入れるだけで次の処理を始めることが出来るわけですね。

watchdogをインストール

watchdogはPythonの標準ライブラリではないので、最初にインストールが必要です。

コマンドプロンプトやターミナルで以下を実行してください。

CMDコマンド
pip install watchdog

インストールが出来たら、Python側からObserverFileSystemEventHandlerを読み込んで使っていきます。

今回のプログラムでやっていること

今回のプログラムは少し長いのですが、処理の流れを分けて考えるとそこまで複雑ではないです。

大きく分けると、

  • 監視するフォルダと移動先を決める
  • ファイルの種類を判定する
  • ファイルのコピーが終わるまで待つ
  • 移動先と新しいファイル名を作る
  • ファイルを移動する
  • watchdogでファイル追加を検知する
  • 監視を止めずに動かし続ける

こういった内容です。

今回使用するプログラム全文

コード:python
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
  • PDF
  • CSV
  • Excel
  • テキストファイル

だけを設定しても大丈夫です。

ファイルを検知したら拡張子を取得して、設定してあるカテゴリのどれに当てはまるかを確認します。

動画内では大文字・小文字が混ざっていても判定出来るように、拡張子を小文字にしてから比較する形にしています。

これで例えばCSVならCSVフォルダ、画像ならimagesフォルダというように、移動先を決めることが出来ます。

ファイルのコピーが終わるまで待つ

今回のプログラムで、個人的に入れておいた方がいいと思うのがこの処理です。

コード:python
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プログラムです。

プログラムのこの部分ですね。

コード: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 Truetime.sleep(1)を使ってプログラムを動かし続けています。

終了したい時にはCtrl + Cで止めて、KeyboardInterruptを受けたらObserverも終了させる流れです。

watchdogはファイル整理以外にも使える

今回のサンプルでは、ファイル名を変更してフォルダへ整理する処理を作りました。

でもwatchdogで重要なのは、整理処理そのものではないです。

ファイル追加を検知した後に実行する処理は、自分で自由に変更出来ます。

僕が使っている例だと、Chromeなどからダウンロードしたファイルを監視して、日付やファイルの種類ごとに分けるような処理があります。

AIで画像を作る時にも、決まった場所へ画像が出力されたら読み込んで、必要な処理をして別のファイルへ置き換えるような使い方をすることがあります。

仕事であれば、顧客情報のCSVを受け取った時に中身を読み込んで、その内容によって別々のフォルダへ振り分けることも出来ます。

例)画像を自動でWebPへ変換する

サイトやブログで使う画像も分かりやすい例です。

スマホで撮影した画像は、そのままだとファイルサイズがかなり大きいことがあります。

その画像を毎回、

  • 画像を開く
  • 必要なサイズにリサイズ
  • WebPへ変換
  • サイト用のフォルダへ保存

と手作業でやるのは面倒ですよね。

そこで画像用の一時フォルダを監視して、そこへ画像を入れたら自動でリサイズ・WebP変換・移動まで行うようにすることも出来ます。

watchdogがやっているのはあくまで、

「フォルダに変化があったことを検知する」

という部分です。

その後に何をするかは自分で作れるので、

ファイルを入れる

→次の仕事が自動で始まる

という仕組みを作れるのが便利なところかなと思います。

今回はPythonのwatchdogを使って、フォルダへのファイル追加を検知して自動処理する流れを解説しました。

プログラム全体を見ると少し長く感じますが、watchdogの部分だけを考えると、Observerで監視してイベントを受け取り、自分の処理を呼び出しているだけです。

今回なら自分の処理がファイル整理になっていますが、ここをCSVの読み込みや画像変換などに変えれば、別の自動化にもそのまま応用出来ます。

日常の作業で「このファイルを置いた後、毎回同じことをしている」というものがあれば、かなり相性がいいと思います。

かなり応用が出来るライブラリなので、ぜひ活用してみてくださいね。

プログラミングへの質問はこちら
もし今回のことやプログラミングについて分からないことがあれば、回答していくので、気軽に以下から聞いてくださいね。
>質問募集中です!

※頂いた質問は動画コンテンツとして回答していく予定です。



All Categories