Введение
PyWatch — это мой учебный проект, цель которого понять, как работает мониторинг файловой системы. Мы будем отслеживать создание, изменение, перемещение и удаление файлов и папок в реальном времени.
Обычные Python-скрипты работают по запросу: запустил → получил результат → программа завершилась. PyWatch устроен иначе — он «сидит на проводе» и реагирует на события, которые происходят в ОС.
Подготовка окружения
Работаем на macOS в VS Code.
Создаём и активируем виртуальное окружение:
python3 -m venv venv
source venv/bin/activate
Устанавливаем библиотеку watchdog:
pip install watchdog
Если после установки IDE (VS Code) не видит библиотеку, просто перезапустите редактор.
Начало кода
import time
from datetime import datetime as dt
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
Здесь мы подключаем модули:
timeиdatetime— для работы со временем;Pathиз модуляpathlib— для удобной работы с путями;ObserverиFileSystemEventHandler— ключевые классы изwatchdog, которые позволяют следить за событиями файловой системы.
Настройки
WATCH_PATH = "."
Задаём путь, за которым будем следить. Точка (".") означает текущую папку, где запущена программа.
Вспомогательные функции
def stamp():
return dt.now().strftime("%Y-%m-%d %H:%M:%S")
def norm(p: str) -> str:
return str(Path(p).resolve())
stamp()возвращает текущее время в удобном формате для логов.norm(p)преобразует путь в абсолютный, чтобы всегда работать с единообразными строками.
Зачем нужен .resolve()?
Метод .resolve() делает путь «нормальным» для ОС:
- приводит относительные пути к абсолютным (
"./file.txt"→"/Users/user/project/file.txt"), - убирает «.», «..» и другие избыточные элементы,
- на некоторых системах ещё и разворачивает символические ссылки.
Таким образом, если в программу попадут разные варианты одного и того же пути, .resolve() приведёт их к единой строке. Это важно, чтобы не было дублей и ошибок в логике.
Обработчик событий
class MyHandler(FileSystemEventHandler):
def on_created(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [CREATE] {kind} {norm(event.src_path)}")
def on_modified(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [MODIFY] {kind} {norm(event.src_path)}")
def on_deleted(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [DELETE] {kind} {norm(event.src_path)}")
def on_moved(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [MOVE] {kind} {norm(event.src_path)} -> {norm(event.dest_path)}")
Здесь мы создаём класс-наблюдатель.
Что делает этот класс?
FileSystemEventHandler — это базовый класс из библиотеки watchdog. Мы создаём свой класс MyHandler, который от него наследуется, и переопределяем методы:
on_created— вызывается, когда ОС сигнализирует о создании файла/папки,on_modified— при изменении,on_deleted— при удалении,on_moved— при перемещении или переименовании.
Каждый метод получает объект event, в котором лежит:
event.src_path— путь к объекту,event.dest_path— новый путь (только для перемещений),event.is_directory— флаг, указывает, файл это или папка.
Как это работает на уровне ОС?
Когда вы создаёте или удаляете файл, ядро ОС фиксирует событие в файловой системе. У каждой платформы свой механизм:
- macOS использует
FSEvents, - Linux —
inotify, - Windows —
ReadDirectoryChangesW.
Эти системные службы отслеживают изменения и сообщают о них «наблюдателю» (Observer). Watchdog под капотом подключается именно к этим API.
Observer получает событие от ОС и передаёт его обработчику. Обработчик вызывает соответствующий метод (on_created, on_deleted и т.д.), и в нём мы уже решаем, что делать — например, вывести лог в консоль.
Это значит, что программа не «ходит в цикл и проверяет», есть ли изменения, а именно получает готовые сигналы от ОС. Это и делает её быстрой и эффективной.
Чуть-чуть подробнее: как ОС отслеживает изменения в файловой системе
Когда мы пишем код на Python и используем библиотеку watchdog, мы работаем на высоком уровне — просто вызываем Observer и Handler. Но важно понимать, что под капотом всё завязано на механизмах самой операционной системы.
У каждой ОС есть свой собственный интерфейс для отслеживания изменений в файловой системе:
- Linux — inotify
Это часть ядра Linux. При создании файлового дескриптораinotifyядро начинает отправлять события при изменениях в выбранных директориях.
События приходят в очередь: «файл создан», «файл удалён», «файл изменён». Каждое событие содержит тип операции и путь.
Watchdog просто слушает эту очередь и конвертирует её в привычные Python-события (on_created,on_deletedи т.д.). - macOS — FSEvents
Здесь используется сервис ядра под названием File System Events. macOS ведёт журнал изменений для каталогов. Программа подписывается на этот журнал и получает список событий с отметками времени и путями.
В отличие отinotify, FSEvents может объединять сразу несколько событий (например, пакет изменений в папке) и передавать их одной пачкой. - Windows — ReadDirectoryChangesW
Это системный вызов в WinAPI. Программа указывает директорию, и ОС начинает слать уведомления в буфер, когда внутри неё происходят изменения.
Здесь можно подписаться сразу на несколько типов событий: создание, удаление, перемещение, изменение атрибутов.
Общий принцип:
- ОС ведёт внутренний журнал изменений файловой системы.
- Наблюдатель (
Observer) подписывается на этот журнал через системный API. - Когда пользователь или программа создаёт/удаляет/меняет файл, ядро записывает событие в журнал.
- Watchdog получает это событие и вызывает соответствующий метод (
on_created,on_deleted,on_modified,on_moved).
Таким образом, наш код реагирует на реальные сигналы от ядра, а не просто «ходит по кругу и проверяет состояние папки». Это экономит ресурсы и позволяет реагировать мгновенно.
┌─────────────┐ ┌─────────────┐ ┌───────────────┐ ┌────────────────┐
│ ОС ядро │ ---> │ Observer │ ---> │ Handler │ ---> │ Ваш код │
│ (inotify / │ │ (watchdog) │ │ (on_created, │ │ (print, логика │
│ FSEvents / │ │ │ │ on_deleted…) │ │ бэкапы и т.д.)│
│ ReadDir...) │ │ │ │ │ │ │
└─────────────┘ └─────────────┘ └───────────────┘ └────────────────┘Запуск наблюдателя
if __name__ == "__main__":
path = WATCH_PATH
handler = MyHandler()
observer = Observer()
observer.schedule(handler, path, recursive=True)
observer.start()
print(f"Watching {Path(path).resolve()} (Ctrl+C to stop)")
try:
while True:
time.sleep(0.25)
except KeyboardInterrupt:
observer.stop()
observer.join()
Что здесь происходит:
- Создаём экземпляр
MyHandler. - Создаём
Observer— он отвечает за связь с ОС. - «Назначаем» обработчик на нужную папку через
schedule. - Запускаем наблюдателя.
- Программа работает в бесконечном цикле, пока мы её не остановим Ctrl+C.
- При остановке корректно завершаем работу (
observer.stop()иobserver.join()).
Запуск программы
Сохраняем файл, например, как pywatch.py, и запускаем:
python3 pywatch.py
Теперь попробуйте создать или удалить файл в папке, за которой идёт слежение. События сразу появятся в консоли.
Итоговый код
import time
from datetime import datetime
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
WATCH_PATH = "."
def stamp():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def norm(p: str) -> str:
return str(Path(p).resolve())
class MyHandler(FileSystemEventHandler):
def on_created(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [CREATE] {kind} {norm(event.src_path)}")
def on_modified(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [MODIFY] {kind} {norm(event.src_path)}")
def on_deleted(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [DELETE] {kind} {norm(event.src_path)}")
def on_moved(self, event):
kind = "DIR " if event.is_directory else "FILE"
print(f"{stamp()} [MOVE] {kind} {norm(event.src_path)} -> {norm(event.dest_path)}")
if __name__ == "__main__":
path = WATCH_PATH
handler = MyHandler()
observer = Observer()
observer.schedule(handler, path, recursive=True)
observer.start()
print(f"Watching {Path(path).resolve()} (Ctrl+C to stop)")
try:
while True:
time.sleep(0.25)
except KeyboardInterrupt:
observer.stop()
observer.join()