Python小酷库系列:玩转Python文件系统工具库(二)


在“Python小酷库系列:玩转Python文件系统工具库(一)”一节中,我们介绍了使用Python内置库对文件系统的路径、目录与文件的增、删、改、压以及权限的控制等常用操作。本节我们介绍一个可以监听文件系统变化的库watchdog,它在实现自动化任务(如文件同步、热重载、构建工具等)中非常常用。

watchdog的基本使用

1、安装

pip install watchdog

2、基本原理

watchdog 会启动一个后台线程,监视一个目录,并在其中的文件或子目录发生事件时,触发对应的回调方法。watchdog主要包括两个组件:
Observer:监视器,负责在后台监听文件系统事件。
FileSystemEventHandler:事件处理器,用于定义如何响应事件。
FileSystemEventHandler提供了四个事件来分别响应文件系统的变化:
on_created(event):文件或目录被创建
on_deleted(event):文件或目录被删除
on_modified(event):文件或目录被修改
on_moved(event):文件或目录被移动

3、基本使用

下面我们通过一个简单的示例来介绍下watchdog 的基本使用:

import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class MyHandler(FileSystemEventHandler):
    def on_modified(self, event):
        print(f"[修改] {event.src_path}")
        
    def on_created(self, event):
        print(f"[新建] {event.src_path}")
        
    def on_deleted(self, event):
        print(f"[删除] {event.src_path}")

if __name__ == "__main__":
    path = "."  # 当前目录
    event_handler = MyHandler()
    observer = Observer() #Observer 是阻塞线程,需要在后台运行或与主程序协同。
    observer.schedule(event_handler, path, recursive=True)  # recursive=True 表示递归子目录
    observer.start()
    print(f"正在监听 {path} 目录... (按 Ctrl+C 停止)")
    
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop() # 需手动调用 observer.stop() 和 observer.join() 来干净地停止线程。
        print("停止监听")
    observer.join()

watchdog默认使用平台原生事件通知(Native OS File System Events),这些机制通常比轮询(polling)更高效、实时、资源友好,并能以更细致的粒度检测文件/目录的变动。如需兼容旧平台或远程网络挂载文件系统(如 NFS),可以显式使用轮询:

from watchdog.observers.polling import PollingObserver
observer = PollingObserver()

综合示例

1、watchdog结合WebSocket实时监控服务器文件系统变化

在前面的基础上,我们结合FastAPI的WebSocket功能来实现一个对服务器文件系统的实时监控工具。我们使用watchdog监控服务器文件系统的变化,并生成变化日志通过WebSocket实时推送到网页上。
main.py

import asyncio
from fastapi import FastAPI, WebSocket
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from pathlib import Path
import logging

app = FastAPI()
clients = set()

# 日志初始化
log_path = Path("fs_events.log")
logging.basicConfig(filename=log_path, level=logging.INFO, format="%(asctime)s - %(message)s")

# 事件处理器
class WebSocketLoggingHandler(FileSystemEventHandler):
    def on_any_event(self, event):
        msg = f"{event.event_type.upper()} - {event.src_path}"
        logging.info(msg)
        asyncio.run(send_to_all(msg))

# 向所有客户端推送消息
async def send_to_all(message: str):
    if clients:
        await asyncio.gather(*(ws.send_text(message) for ws in clients))

# 启动 watchdog(在子线程中运行)
def start_watch(path="."):
    observer = Observer()
    observer.schedule(WebSocketLoggingHandler(), path=path, recursive=True)
    observer.start()
    observer.join()

@app.on_event("startup")
async def on_startup():
    asyncio.create_task(asyncio.to_thread(start_watch))

# WebSocket 路由
@app.websocket("/ws/logs")
async def websocket_logs(websocket: WebSocket):
    await websocket.accept()
    clients.add(websocket)
    try:
        while True:
            await websocket.receive_text()  # 保持连接
    except:
        clients.remove(websocket)

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>实时日志</title>
    <style>
        body { font-family: monospace; background: #111; color: #0f0; padding: 20px; }
        #log { white-space: pre-wrap; }
    </style>
</head>
<body>
    <h2>实时文件变更日志</h2>
    <div id="log"></div>

    <script>
        const log = document.getElementById("log");
        const ws = new WebSocket("ws://localhost:8000/ws/logs");
        ws.onmessage = (event) => {
            const line = document.createElement("div");
            line.textContent = event.data;
            log.appendChild(line);
            window.scrollTo(0, document.body.scrollHeight);
        };
    </script>
</body>
</html>

2、watchgod+asyncio,更高性能的实现方式

标准的 watchdog 使用的是同步机制,这意味着它与FastAPI这种异步框架一起使用时无法与async def函数协同工作。这时我们可以选用watchgod+asyncio这一轻量级组合,上面的例子可以改写为:
main.py

import asyncio
from fastapi import FastAPI, WebSocket
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from watchgod import awatch
import datetime

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

clients: set[WebSocket] = set()

log_file = Path("fs_events.log")

# 将消息记录到文件
def write_log(msg: str):
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_file.write_text(f"{timestamp} - {msg}\n", append=True)

# 推送到所有连接的 WebSocket 客户端
async def notify_clients(msg: str):
    for client in clients.copy():
        try:
            await client.send_text(msg)
        except Exception:
            clients.remove(client)

# 异步文件系统监听任务
async def watch_files(path="."):
    async for changes in awatch(path):
        for change_type, changed_path in changes:
            msg = f"{change_type.name} - {changed_path}"
            write_log(msg)
            await notify_clients(msg)

@app.on_event("startup")
async def startup_event():
    asyncio.create_task(watch_files())

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    clients.add(websocket)
    try:
        while True:
            await websocket.receive_text()  # 保持连接
    except Exception:
        clients.remove(websocket)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>实时文件日志</title>
    <style>
        body { background: #111; color: #0f0; font-family: monospace; padding: 20px; }
        #log { white-space: pre-wrap; }
    </style>
</head>
<body>
    <h2>文件系统变更监控</h2>
    <div id="log"></div>
    <script>
        const logDiv = document.getElementById("log");
        const ws = new WebSocket(`ws://${location.host}/ws`);
        ws.onmessage = (event) => {
            const line = document.createElement("div");
            line.textContent = event.data;
            logDiv.appendChild(line);
            window.scrollTo(0, document.body.scrollHeight);
        };
    </script>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值