本地Windows剪贴板的持久化扩展操作,支持持久化记录复制到剪贴板的内容,记录其来源以及按照类别存储,主要核心就是监听电脑本身的剪贴板,然后记录内容。
主要支持的格式:文本、图像、html、文件(路径)
项目结构:
saveUserAction\
├─ clipboard_monitor/ ← 监听端:捕获剪贴板并落盘
│ ├─ __main__.py 命令行入口
│ ├─ monitor.py 窗口消息循环 + 保存线程
│ ├─ reader.py 按格式识别并读取剪贴板
│ ├─ storage.py ClipboardStore:写文件 + 追加索引
│ └─ __init__.py 对外导出
├─ server/ ← 查看端:Flask Web 服务
│ ├─ web_server.py 路由与 HTTP 接口
│ ├─ storage.py 读取层:索引、搜索、星标、删除、统计
│ └─ templates/
│ └─ index.html 单页前端(浅色主题)
├─ clipboard_data/ ← 数据目录(两端共用)
│ ├─ 2026-10-28/
│ │ ├─ text/ html/ image/ files/ 按类型分目录
│ │ └─ index.json 当日索引
│ └─ stars.json(首次收藏时才生成)
├─ requirements.txt pywin32 / Pillow / flask
监听端定义:
monitor.py
"""监听 Windows 剪贴板变化并触发保存。 使用 AddClipboardFormatListener 接收 WM_CLIPBOARDUPDATE,不做轮询; 读取动作放到独立线程,避免阻塞窗口消息循环导致事件丢失。 """ from __future__ import annotations import ctypes import queue import threading import time from typing import Callable import win32api import win32clipboard import win32gui from .reader import read_clipboard from .storage import ClipboardStore WM_CLIPBOARDUPDATE = 0x031D def get_foreground_title() -> str: """记录复制发生时所在窗口,便于回溯来源。""" try: return win32gui.GetWindowText(win32gui.GetForegroundWindow()) or "未知窗口" except Exception: return "未知窗口" class ClipboardMonitor: """启动后常驻,任何复制动作都会按类型保存到磁盘。""" def __init__( self, store: ClipboardStore, on_saved: Callable[[dict], None] | None = None, ): self.store = store self.on_saved = on_saved or print_entry self._queue: queue.Queue[None] = queue.Queue() self._worker = threading.Thread( target=self._consume, name="clipboard-saver", daemon=True ) self._hwnd: int | None = None self._class_atom: int | None = None self._last_seq = -1 self._running = True def start(self) -> None: self._worker.start() hinst = win32api.GetModuleHandle(None) wc = win32gui.WNDCLASS() wc.lpfnWndProc = self._wnd_proc wc.lpszClassName = f"ClipboardMonitor_{id(self)}" wc.hInstance = hinst self._class_atom = win32gui.RegisterClass(wc) self._hwnd = win32gui.CreateWindowEx( 0, self._class_atom, "ClipboardMonitor", 0, 0, 0, 0, 0, 0, 0, hinst, None ) if not ctypes.windll.user32.AddClipboardFormatListener(self._hwnd): raise ctypes.WinError() # print(f"剪贴板监听已启动,存储目录: {self.store.root.resolve()}") try: # 用短间隔泵消息,保证 Ctrl+C 能及时响应,同时不轮询剪贴板本身 while self._running: win32gui.PumpWaitingMessages() time.sleep(0.05) except KeyboardInterrupt: pass finally: self.stop() def stop(self) -> None: self._running = False if self._hwnd: ctypes.windll.user32.RemoveClipboardFormatListener(self._hwnd) win32gui.DestroyWindow(self._hwnd) self._hwnd = None def _wnd_proc(self, hwnd, msg, wparam, lparam): if msg == WM_CLIPBOARDUPDATE: self._queue.put(None) return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) def _consume(self) -> None: while True: self._queue.get() try: self._save_current() except Exception as exc: # 单次失败不影响后续监听 print(f"[警告] 读取剪贴板失败: {exc}") def _save_current(self) -> None: seq = win32clipboard.GetClipboardSequenceNumber() if seq == self._last_seq: return item = read_clipboard() if item is None: return self._last_seq = seq entry = self.store.save(item, source=get_foreground_title()) if entry: self.on_saved(entry) def print_entry(entry: dict) -> None: stamp = entry["timestamp"].replace("T", " ") print(f"[{stamp}] {entry['type']:<5s} {entry['path']}") print(f" 来源: {entry['source_app']}") print(f" 预览: {entry['preview'][:80]}")monitor的简单解释
关于监听部分的注册
# 负责开启对应的消费线程,又self.queue()触发,这个触发逻辑是当有新的消息会进行回调函数_wnd_proc的调用,然后向队列写入消息,然后对应的线程发现队列有新的数据会结束阻塞,取出数据进行后面的消费 self._worker.start() # 这个地方的核心意义就是向Windows注册一个独立于剪贴板的信箱,定义回调函数,监听剪贴板变化 hinst = win32api.GetModuleHandle(None) wc = win32gui.WNDCLASS() wc.lpfnWndProc = self._wnd_proc wc.lpszClassName = f"ClipboardMonitor_{id(self)}" wc.hInstance = hinst self._class_atom = win32gui.RegisterClass(wc) self._hwnd = win32gui.CreateWindowEx( 0, self._class_atom, "ClipboardMonitor", 0, 0, 0, 0, 0, 0, 0, hinst, None) if not ctypes.windll.user32.AddClipboardFormatListener(self._hwnd): raise ctypes.WinError() # 这个地方是从定义的信箱取出数据的核心,所以需要常驻,用一个while循环保证 # 这个地方隔一段时间取出数据,然后会清除,执行对应的回调函数,回调函数触发消费流程,进行存储 try: # 用短间隔泵消息,保证 Ctrl+C 能及时响应,同时不轮询剪贴板本身 while self._running: win32gui.PumpWaitingMessages() time.sleep(0.05) except KeyboardInterrupt: pass finally: self.stop()关于触发消费
# 回调函数,向队列插入数据,解除_consume的阻塞状态 def _wnd_proc(self, hwnd, msg, wparam, lparam): if msg == WM_CLIPBOARDUPDATE: self._queue.put(None) return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) def _consume(self) -> None: while True: self._queue.get() try: self._save_current() except Exception as exc: # 单次失败不影响后续监听 print(f"[警告] 读取剪贴板失败: {exc}") def _save_current(self) -> None: seq = win32clipboard.GetClipboardSequenceNumber() if seq == self._last_seq: return item = read_clipboard() if item is None: return self._last_seq = seq entry = self.store.save(item, source=get_foreground_title()) if entry: self.on_saved(entry)其中read_clipboard()取出数据,对应的是reader.py
self.store.save(item, source=get_foreground_title()),对应的是storage.py
剩余的代码
这个地方主要核心就是为了记录比较不好理解的注册与消费部分的逻辑,其他的就是正常的前后端相关curd操作
gitee链接
https://gitee.com/xie-da-da/saveUserAction.git
如果有啥问题,可以交流。其次这个本身是为了本地使用的,没有太多的性能或安全考虑