玩转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>
3万+

被折叠的 条评论
为什么被折叠?



