操作系统提供的剪贴板是一个临时数据交换区域,通常只保留用户最后一次复制或剪切的内容。当用户复制新内容后,旧内容会被静默覆盖,如果没有手动保存,往往只能重新复制。利用SQLite可以把每一次剪贴板内容持久化到本地数据库,形成一个可查询、可清理的历史记录。本文实现一个轻量级剪贴板历史工具,使用Python负责监听和交互,SQLite负责存储与检索。

一、设计剪贴板历史的数据模型
剪贴板内容可能很短,也可能包含整段代码、URL或长文本。存储时不能只保存内容本身,还需要记录内容类型、写入时间以及一个可以唯一标识内容的哈希值。这样设计有两个好处:一是可以通过哈希值建立唯一索引,配合INSERT OR IGNORE实现自动去重;二是查询完整记录时无需每次计算哈希,直接利用索引即可定位。
下面是一张核心表的建表语句。content_hash字段使用SHA-256生成,能有效降低长文本建立唯一索引时的空间开销。created_at默认写入本地时间,方便按时间范围做清理。
CREATE TABLE IF NOT EXISTS clip_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'text',
source TEXT,
content_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE INDEX IF NOT EXISTS idx_created_at ON clip_history(created_at DESC);
如果只保存纯文本,可以将content_type固定为text;如果还需要保存图片路径、文件路径或URL分类,可以在写入时根据内容前缀判断。比如以http://或https://开头的文本标记为url,其余标记为text。后续扩展图片时,可以存储图片保存路径,并把content_type设为image。
二、监听剪贴板并写入SQLite
剪贴板监听有两种常见实现方式。第一种是轮询,即每隔固定时间读取一次剪贴板内容,如果与上一轮不同就写入数据库;第二种是在Windows平台使用Win32 API监听剪贴板变更消息,实时性更高,但平台绑定较强。为了保持代码简单且易于跨平台运行,本文采用轮询方式,使用pyperclip库读取剪贴板。
写入逻辑的核心是哈希去重。每次读取到新文本后,先计算SHA-256摘要,再执行INSERT OR IGNORE。由于content_hash列存在唯一约束,相同内容重复复制时不会插入新行,只会忽略本次写入。
import time
import hashlib
import sqlite3
import pyperclip
DB_PATH = "clipboard_history.db"
def get_connection():
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
return conn
def save_clipboard(text):
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
content_type = "url" if text.startswith(("http://", "https://")) else "text"
with get_connection() as conn:
conn.execute(
"INSERT OR IGNORE INTO clip_history(content, content_type, content_hash) VALUES(?, ?, ?)",
(text, content_type, digest)
)
def watch_clipboard(interval=0.5):
last_text = pyperclip.paste()
while True:
try:
current = pyperclip.paste()
if current and current != last_text:
save_clipboard(current)
last_text = current
except pyperclip.PyperclipException:
pass
time.sleep(interval)
if __name__ == "__main__":
watch_clipboard()
轮询间隔设为0.5秒对文本复制已经足够,如果希望接近实时,可以降低到0.2秒。需要注意,过短的间隔会增加CPU占用,尤其在使用某些Linux桌面环境时,pyperclip底层需要调用xclip或wl-paste,每次调用都有进程启动成本。如果运行在Windows,可以结合win32clipboard实现事件驱动,代码会复杂一些,但能减少无意义的轮询。
三、查询历史与自动清理策略
当历史记录积累到几千条甚至几万条后,简单的LIKE查询仍然可以工作,但性能会逐渐下降,因为SQLite需要扫描大量TEXT字段。SQLite内置的FTS5全文索引模块可以显著改善文本检索体验。FTS5会对文本进行分词并建立倒排索引,使用MATCH语句查询时不再执行全表扫描。
下面创建FTS5虚拟表,并通过触发器在每次插入普通表后自动同步数据。content_hash列标记为UNINDEXED,因为哈希值通常不需要参与全文匹配。
CREATE VIRTUAL TABLE IF NOT EXISTS clip_fts USING fts5(
content,
content_type,
content_hash UNINDEXED,
tokenize='unicode61'
);
CREATE TRIGGER IF NOT EXISTS clip_history_ai AFTER INSERT ON clip_history BEGIN
INSERT INTO clip_fts(rowid, content, content_type, content_hash)
VALUES (new.id, new.content, new.content_type, new.content_hash);
END;
查询时可以执行SELECT rowid, content FROM clip_fts WHERE clip_fts MATCH '数据库';,如果希望按时间排序,可以联合普通表一起查询。FTS5默认对中文分词支持取决于tokenize配置,unicode61对中文会按连续字符处理,短词检索效果一般。如果对中文搜索要求较高,可以引入jieba等分词方案,或者退而使用LIKE加created_at索引限制在最近5000条内查询。
清理策略也同样重要。可以保留最近5000条记录,也可以按时间删除30天前的数据。举例来说,执行DELETE FROM clip_history WHERE id NOT IN (SELECT id FROM clip_history ORDER BY id DESC LIMIT 5000);可以控制总行数。需要注意,当开启FTS5触发器后,DELETE操作不会自动清理虚拟表中的旧数据,需要在删除时手动执行DELETE FROM clip_fts WHERE rowid IN (...),或者额外创建AFTER DELETE触发器保持两边一致。
四、安全隐私与性能优化
剪贴板内容非常敏感,经常包含密码、验证码、密钥、地址等个人信息。用SQLite保存历史虽然方便,但数据库文件本身必须被视为敏感文件。建议将数据库文件放在当前用户目录下,并在Linux或macOS中通过chmod 600限制访问权限。如果可能,还可以使用应用层加密,将content字段加密后再写入,读取时再解密。这样即使数据库文件被拷贝,内容也不会直接暴露。
性能优化方面,开启WAL模式后写入操作会先写入WAL文件,再合并到主数据库,能减少写锁冲突。对于长期运行的程序,可以定期执行PRAGMA optimize;或VACUUM;回收删除记录后留下的空闲页。数据库体积主要由长文本和索引组成,如果经常复制大段代码,可以在写入前对content做压缩,或者只保留前若干KB用于预览,完整内容存到独立文件。
这个项目还可以继续扩展:例如增加一个命令行查询入口,支持按关键词查看最近历史;增加系统托盘菜单,点击选项复制指定记录;支持图片剪贴板时保存图片文件和缩略图路径;甚至可以设置敏感应用黑名单,当检测到来自密码管理器的内容时不写入历史。SQLite的优势在于无需部署服务、单文件即可完成数据持久化,非常适合这类本地轻量工具。