ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

自建电脑操作记录仪:剪贴板、窗口与键鼠轨迹全记录

自建电脑操作记录仪:剪贴板、窗口与键鼠轨迹全记录 刚过去的周五下班前我为了找一条三天前复制过的 URL翻了整整一小时聊天记录和备忘录最后一无所获。那条链接早就被中间几十次新的 CtrlC 顶掉了就像人的记忆会被新的信息覆盖一样。这事让我下决心给电脑装一套“行为记录仪”剪切板历史、前台打开的软件窗口、浏览器访问过的网页、鼠标点过的位置、键盘敲过的按键全部按时间轴落库随时可查。我说的可不是什么玄乎的东西就一句话——把你每天和电脑交互的痕迹记下来需要的时候能“翻旧账”。这个需求其实很常见开发者要找回一段被覆盖的代码编辑要追溯某个文件的改名路径自媒体要复盘自己一天的时间去向哪怕是普通上班族也想搞清楚自己下午那三个小时到底用在了哪。下面的内容是我自己动手拼这套系统的完整过程从剪贴板监听、前台窗口采集、网页历史读取到键鼠轨迹记录再到数据汇总和复盘每一块都附了能直接跑起来的代码和踩过的坑。看完你也能搭一套完全属于自己、数据留在本地的“操作记录仪”。1. 整体需求拆解与方案选型1.1 三类数据源对应三类需求这套系统的核心是三个数据源分别解决不同的问题。第一是剪贴板记录每次复制、剪切的内容包括文字、链接甚至复制的文件路径它解决的是“复制了又没复制”的失忆问题。第二是前台窗口通过高频查看当前激活的窗口知道某一刻你正在用哪个程序、编辑哪个文件、浏览哪个网页它解决的是“今天到底干了什么”的工作回溯问题。第三是键鼠事件记录鼠标移动、点击坐标、滚轮操作和键盘输入它解决的是“我干活效率怎么样”的行为分析问题。这三个数据源的采集方式、数据量级和使用价值差别挺大放一起看更清楚。数据源采集手段记录内容解决的问题剪贴板监听剪贴板变化文本、URL、格式、时间找回被覆盖的复制内容前台窗口轮询焦点窗口窗口标题、进程名、时间还原“某时刻在做什么程序/看什么文件”网页浏览读取浏览器历史库URL、标题、访问时间找回看过的网页、生成浏览时间线键鼠轨迹全局钩子监听鼠标坐标、点击、按键、时间效率统计、行为复盘、热点图可视化我自己实际跑了一周之后最大的体感是剪贴板数据是“救命”用的窗口和网页数据是“复盘”用的键鼠轨迹则是“定性和定量分析”用的。前两个少了会心烦第三个更像锦上添花但对做效率报告来说绝对是好东西。1.2 为什么我选择“工具收集 脚本落库”的组合方案这套系统第一版我犯过一个常见错误一上来就准备用 Python 写一个全功能监控助手监听全局键盘、抢剪贴板链、自己读浏览器历史什么都自己做。结果光是剪贴板监听就踩了一堆坑代码写了一整天还没跑稳定。后来我换了思路能用成熟工具的用成熟工具工具不适合的再用脚本补所有数据最后统一汇到 SQLite 里。这套思路的好处有三个。第一剪贴板历史这块Ditto、CopyQ 这些开源工具十几年迭代断链重连、系统托盘交互、搜索界面都打磨得很完整自己从头写的成本极高。第二前台窗口记录其实不需要什么高深 API一个 Windows 自带接口加上定时轮询就能稳定工作自己实现不难。第三键鼠监听虽然能 Hook 全局但钩子卡死、权限问题、回调阻塞都是大坑先跑起来再逐步优化比一开始就追求“完美方案”实在得多。这里必须补充一句这套组合方案是我基于常规做法选择出来的。市面上的效率工具其实已经覆盖了其中大部分功能但它们的共性问题在于数据各自存各家不让导出也不给你完整时间线。所以“成熟工具负责采集自写脚本负责整合”是兼顾稳定和可控性的合理路径。2. 剪贴板历史把每一次 CtrlC 都做成索引2.1 监听机制与实现原理剪贴板监听的原理其实没有很多人想的那么高深。Windows 允许应用程序在系统的“剪贴板变化通知”上注册一个监听点一旦有程序写入剪贴板系统就会推送更新消息。老式的实现方法是 SetClipboardViewer把自己的窗口挂进一条叫作 Clipboard Viewer Chain 的链子里但这条链会被某些不守规矩的程序截断一旦中途断掉后面的监听者全都会静默失效。很多老牌剪贴板工具偶尔出现“不记录了”的毛病根源往往就在这里。较新一点的 API 是 AddClipboardFormatListener它绕开链式注册直接让窗口接收 WM_CLIPBOARDUPDATE 消息稳定性好得多。不过快速验证需求时我用一个更笨更直接的方案轮询。每隔零点几秒暴力读取一次剪贴板内容和上一次的值比一下变了就入库。速度快起来时和事件监听没本质区别。import time import sqlite3 from datetime import datetime import pyperclip conn sqlite3.connect(clipboard.db) conn.execute(CREATE TABLE IF NOT EXISTS clipboard_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, created_at TEXT )) last_content None while True: try: text pyperclip.paste() except Exception: text None if text and text ! last_content: conn.execute( INSERT INTO clipboard_items (content, created_at) VALUES (?, ?), (text, datetime.now().isoformat(timespecseconds)) ) conn.commit() last_content text time.sleep(0.5)这段代码逻辑简单但有两个细节值得说。第一我特意对剪贴板内容做了去重避免同一段内容反复复制时产生无效记录这在你复读一段文字时很有用。第二我只存了文本内容没有抓取剪贴板里的图片位图和文件列表。图片占空间大文件复制记录在剪贴板里是一组特殊结构纯文本工具抓不全这块让专业工具去处理更合适。2.2 工具选型与配置里的实战细节轮询脚本可以做原型但你天天用的话我还是建议上工具。Windows 上我推荐 Ditto跨平台场景用 CopyQ两者都是开源且久经考验的剪贴板管理工具。Ditto 最香的几个点默认托盘常驻按快捷键随时呼出历史列表支持分组和快速搜索历史里找一条几个月前的代码片段完全没问题存储走 SQLite备份和迁移非常省心。Crtl 呼出面板这个快捷键我也建议设置成全局它基本成了我的第二根手指。CopyQ 的优势则在于跨平台一致性和极强的脚本扩展能力Linux 和 macOS 下体验一致。不管用哪个工具有几个配置项我建议一上手就调整。一是过滤规则很多密码管理器和支付类页面会禁止剪贴板监听工具未必自动识别你得手动添加不监听的窗口关键词比如“Password”“KeePass”之类。二是存储策略Ditto 默认保留条数可以调高到几万但图片类内容尽量限制体积不然数据库膨胀得很快。三是同步和备份剪贴板数据库最好定期导出如果有一天系统盘挂了你会庆幸自己做过备份。这里有一个初级用户最容易忽略的坑剪贴板工具看似什么都能记录其实密码管理器一类的应用会主动清空或混淆剪贴板还有少数软件带反截获功能。这种环境下记录不到内容并不是工具坏了而是对方有意为之。安全意识高的人群也不建议在剪贴板工具里长期保留银行卡号、密码这类敏感文本数据只在本机反而更要管好自己的数据库文件。3. 程序窗口、文件与网页浏览记录3.1 前台窗口轮询最省心的“焦点采集法”记录“正在用什么程序”这件事我一开始考虑过做全局钩子被一个老前辈一句点醒你不需要知道用户每个瞬间的具体操作只要定期看一眼当前激活窗口是谁就够了。轮询焦点窗口的方案采集成本极低实现又可靠这成了我最推荐的入口方案。原理不复杂Windows 每次切换窗口都会有一个前台窗口通过 GetForegroundWindow 能拿到它的句柄再用 GetWindowText 拿窗口标题用 GetWindowThreadProcessId 拿到所属进程 ID。窗口标题里通常就藏着文件或网页信息记事本会显示当前打开的 txt 文件名Word 会显示 docx 路径Chrome 则会显示当前激活标签页的标题。每两秒采样一次就足够还原完整的应用轨迹。Add-Type using System; using System.Runtime.InteropServices; using System.Text; public class FocusCapture { [DllImport(user32.dll)] public static extern IntPtr GetForegroundWindow(); [DllImport(user32.dll)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); [DllImport(user32.dll)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); } while ($true) { $h [FocusCapture]::GetForegroundWindow() $sb New-Object System.Text.StringBuilder 512 [void][FocusCapture]::GetWindowText($h, $sb, 512) $p 0 [void][FocusCapture]::GetWindowThreadProcessId($h, [ref]$p) $proc Get-Process -Id $p -ErrorAction SilentlyContinue $title $sb.ToString() if ($title) { Add-Content -Path focus.log -Value $(Get-Date -Format yyyy-MM-dd HH:mm:ss)|$($proc.ProcessName)|$title } Start-Sleep -Seconds 2 }运行后会得到一行行“时间|进程名|窗口标题”的日志第一版够用了。但要注意两个问题一是尽量让采集脚本以后台任务方式运行别占着前台焦点二是 2 秒采样间隔对一般人足够但如果你频繁在窗口间切换某些停留时间不足 2 秒的窗口会被漏掉。后期我直接把数据写进 SQLite并让脚本常驻日志文件改成数据库查询方便太多也能按分钟聚合出“时间线”。浏览器这块有个细节容易让人误判多标签页场景下Chrome 的前台窗口标题通常只显示当前正在看的那个标签页可窗口切换过去后标题不会立刻刷新轮询到的是“旧标签标题”。想精确知道 10:03 分你实际在看哪个网页光靠窗口标题是不够的得再结合浏览器的历史库。3.2 网页历史直接读取浏览器的 SQLite 数据Chrome、Edge 这类 Chromium 内核浏览器的网页历史本质上就是一个 SQLite 数据库文件。路径都差不多Chrome 在%LOCALAPPDATA%\Google\Chrome\User Data\Default\HistoryEdge 在%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History。这个文件里有一张 urls 表字段包含 url、title、last_visit_time浏览器“历史记录”页面显示的内容就是查这张表。但直接读它有坑浏览器运行时默认锁定数据库文件你用 Python 连接会报 database is locked。常规解法是先把这个文件拷贝一份到临时目录再从副本里读绕开浏览器占用。import os import shutil import sqlite3 from datetime import datetime, timedelta src os.path.join(os.environ[LOCALAPPDATA], Google, Chrome, User Data, Default, History) tmp history_copy.db shutil.copy2(src, tmp) conn sqlite3.connect(tmp) rows conn.execute( SELECT url, title, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT 20 ).fetchall() def chrome_time_to_datetime(ts): # Chrome 时间戳是 1601-01-01 00:00:00 以来的微秒数 return datetime(1601, 1, 1) timedelta(microsecondsts) for url, title, ts in rows: print(f{chrome_time_to_datetime(ts)} {title} {url}) conn.close()Chrome 的 last_visit_time 是 WebKit 格式时间戳单位是微秒从 1601 年 1 月 1 日起算直接做转换才能得到正常时间。这个方法读取的是完整访问历史比窗口标题精确得多也天然支持搜索历史网页。如果你不想自己写代码ActivityWatch 这个开源项目是条捷径。它提供桌面端 watcher再配一个浏览器扩展插件可以自动记录活动窗口和浏览过的网页自带 Web UI 和数据分析。但我的实测感受是ActivityWatch 的浏览器记录依赖扩展记录粒度是按“活动切换到某个标签”来计时的对“实际浏览了哪些页面”的覆盖不如直接读历史库全面。我现在是两边都开ActivityWatch 看趋势读库脚本看细节。4. 键鼠轨迹记录与可视化复盘4.1 全局键鼠监听pynput 让门槛降到最低键鼠轨迹这块我用的方案是 Python 的 pynput 库。它封装了 Windows、macOS、Linux 三个平台的全局鼠标键盘监听接口几行代码就能把鼠标移动、点击、滚轮、按键都记录下来。对个人工具来说这个平衡点很好不用写底层 C 代码性能损耗也在可接受范围。from pynput import mouse, keyboard import sqlite3 import datetime conn sqlite3.connect(input.db) conn.execute(CREATE TABLE IF NOT EXISTS input_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT, event_type TEXT, detail TEXT )) conn.commit() def record(event_type, detail): conn.execute( INSERT INTO input_events (ts, event_type, detail) VALUES (?, ?, ?), (datetime.datetime.now().isoformat(timespecmilliseconds), event_type, detail) ) conn.commit() def on_move(x, y): record(move, f{x},{y}) def on_click(x, y, button, pressed): if pressed: record(click, f{x},{y},{button}) def on_scroll(x, y, dx, dy): record(scroll, f{x},{y},{dx},{dy}) def on_press(key): record(key, str(key)) with mouse.Listener(on_moveon_move, on_clickon_click, on_scrollon_scroll) as m: with keyboard.Listener(on_presson_press) as k: m.join() k.join()这个脚本能直接跑但我在实际用的时候发现几个必须调理的地方第一pynput 的全局钩子在 Windows 下对管理员权限运行的程序无效普通用户权限的脚本监听不到以管理员身份打开的窗口里的按键。解决方案很直白你也用管理员权限启动脚本。第二回调函数里每次直接写 SQLite 并 commit在高频鼠标移动时会造成大量磁盘 IO整个系统都能感到迟钝。正确做法是在回调里只把事件塞进内存队列后台单独一个线程批量写库。import queue import threading event_queue queue.Queue() def record(event_type, detail): event_queue.put((datetime.datetime.now().isoformat(timespecmilliseconds), event_type, detail)) def db_writer(): while True: batch [] while not event_queue.empty(): batch.append(event_queue.get()) if batch: conn.executemany( INSERT INTO input_events (ts, event_type, detail) VALUES (?, ?, ?), batch ) conn.commit() time.sleep(1) threading.Thread(targetdb_writer, daemonTrue).start()这段队列加批量写入的做法是后来加的效果非常明显。鼠标移动事件每秒可能产生几十上百个逐个提交很浪费攒一秒写一次CPU 和磁盘都安静下来。另外还有一个容易被忽略的隐私悖论键盘记录器的技术原理和真正的恶意软件几乎相同你装这东西一定要清楚自己在干什么、数据存哪、谁能看。我把敏感输入比如密码框里的内容单独过滤掉在按键记录里只保留按键种类统计而不记录连续字符串这个取舍要自己心里有数。4.2 把轨迹变成热力图和日活报告采下来的键鼠轨迹是冷冰冰的行只有把它变成图才有感知。我最常用的是点击热力图把一整天的鼠标点击坐标取出来投到屏幕分辨率对应的坐标空间里颜色深浅代表点击密度马上就能看出你主要点了哪些区域。import sqlite3 import matplotlib.pyplot as plt import matplotlib matplotlib.use(Agg) conn sqlite3.connect(input.db) rows conn.execute(SELECT detail FROM input_events WHERE event_typeclick).fetchall() xs [] ys [] for r in rows: parts r[0].split(,) if len(parts) 2: xs.append(int(parts[0])) ys.append(int(parts[1])) plt.figure(figsize(16, 9)) plt.hexbin(xs, ys, gridsize60, cmapinferno, extent(0, 1920, 1080, 0)) plt.colorbar(labelclick density) plt.axis(off) plt.savefig(click_heatmap.png, dpi150, bbox_inchestight)这里有个多屏环境的坑值得记一下Windows 上副屏在主屏左边时鼠标坐标会出现负值热力图 extent 直接按 0 到 1920 画会把副屏数据丢掉。解决办法是把所有坐标先做偏移补偿再映射到拼接后的总屏幕范围。还有如果你用 4K 屏分辨率不是 1920x1080热力图参数要跟着屏幕实际像素调整。除了热力图键盘事件天生适合做活跃度统计。把每小时按键次数拉出来就能看到一天的工作波峰波谷配合前台窗口时间线还能估算出某段时间真正“高效率”的占比。我自己的感受是这类报告不必做得太花哨一张小时级柱状图加一张点击热力图已经足够支撑每周的自我复盘了。5. 数据汇聚把三张表串成完整时间线5.1 数据表设计思路前面每一块都是独立生成数据库的剪贴板一个库、窗口记录一个日志、键鼠事件一个库用起来很割裂。要形成统一的“今日时间线”必须把三类数据聚到一起。我在 SQLite 里设计了三个基础表分别存剪贴板、焦点和输入事件表结构保持简单查询时再用时间轴关联。CREATE TABLE clipboard_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, created_at TEXT ); CREATE TABLE focus_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT, process_name TEXT, window_title TEXT ); CREATE TABLE input_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT, event_type TEXT, detail TEXT );最关键的是统一时间格式。全部用 ISO 8601 的字符串比如2025-01-17T09:30:00排序、比较、分组都方便Python 直接转 datetime 也不费劲。我一开始用过 Unix 时间戳和本地时间字符串混着存查跨天数据时晕头转向后来统一成 ISO 格式才理顺。5.2 用一条 SQL 生成“数字日记”数据统一之后最常用的查询是把一天的剪贴板、焦点切换、键鼠事件按时间排在一起直接生成一份人类可读的时间线。SELECT ts, event_type, detail FROM ( SELECT created_at AS ts, clipboard AS event_type, content AS detail FROM clipboard_items WHERE created_at BETWEEN 2025-01-17 09:00 AND 2025-01-17 18:00 UNION ALL SELECT ts, window AS event_type, process_name || | || window_title AS detail FROM focus_events WHERE ts BETWEEN 2025-01-17 09:00 AND 2025-01-17 18:00 UNION ALL SELECT ts, event_type, detail FROM input_events WHERE ts BETWEEN 2025-01-17 09:00 AND 2025-01-17 18:00 ) ORDER BY ts;这条 SQL 跑出来的每条记录都带时间戳和内容打印出来俨然就是当天的工作流水账。让我真正觉得这套系统值回票价的是下面这个真实发生过的事有一天我复制过一段完整的 Docker Compose 配置顺手又复制了另一个段配置之前的就没了。本来是准备完整起一套服务的结果手滑覆盖后到处搜最后还是打开剪贴板数据库用SELECT content FROM clipboard_items WHERE content LIKE %volumes%把原文捞了回来。那一刻我才明白剪贴板历史的价值不在于炫技而是在关键时刻让你免于重写。数据汇聚之后我还会每周末导出一次汇总生成一个当天所有新记录的 HTML 文件。这样即使电脑后来出问题至少还有一份可检索的“数字日记”留存。数据量上纯文本剪贴板加键鼠事件一天大概几万条SQLite 完全扛得住不必刻意清理。6. 常见问题与避坑实录这套系统跑下来我在维护中也积累了一些排查经验整理成速查表给后面想搭的人参考。现象可能原因解决办法剪贴板历史突然空白剪贴板查看链被其他程序截断或安全软件拦截换用 AddClipboardFormatListener 方案Ditto 内检查监听是否正常给工具加白名单用管理员权限的程序里鼠标按键录不到普通权限脚本无法全局钩住高权限进程脚本也用管理员权限启动鼠标监听导致系统明显变卡回调里直接写数据库造成阻塞回调只入内存队列后台线程批量写库Chrome 历史读取报 database is locked浏览器运行时锁定 History 文件先 copy2 到临时文件再读副本窗口轮询日志里很多进程名相同标题为空或窗口切换太快轮询间隔从 2 秒降到 1 秒对空标题做跳过光标在某些屏幕上不显示轨迹副屏在主屏左侧时坐标是负值做坐标偏移归一化再映射热力图数据库文件越滚越大剪贴板图片、长文本和键鼠详细事件都占体积定期归档保留最近 30 天清空或截断旧数据这里面有几个坑值得多说几句。剪贴板空白的问题最常见也最容易误判我曾经以为是脚本挂了反复重启没效果后来发现是某天装了一个共享剪贴板小工具把系统的 Clipboard Viewer Chain 挤掉了。Ditto 这类老工具在处理断链上难免有死角所以我在轮询脚本里加了一个心跳检测每 30 秒检查一次自己写入的最新时间如果发现超过 5 分钟没产生新记录但剪切板明明变了就报警。这法子土但非常好用。权限问题也很经典。有时你看到键鼠记录零零星星不是采集逻辑错了而是那些窗口跑在管理员权限下普通权限的钩子根本碰不到。Windows 的 UAC 权限隔离对全局键盘钩子有明显的限制这不是你的问题而是系统设计使然。要录全就得用管理员权限跑监听脚本这也是为什么我从来不把这套工具做成“安装即用的黑盒”——你亲自运行脚本你才清楚它要以什么身份访问你的键盘输入。最后一个提醒隐私和合规永远是这个工具的底线。数据全在本机是它的优点但本机数据被同事、家人拿到也容易出问题。我给自己的规矩是键盘输入只记录按键和次数不记录成句的连续字符串剪贴板里明显的密码、卡号一旦检测到就加标记并定期清理数据库文件要用单独的加密盘目录保存。这些不是技术难点但决定了你能不能长久安心用下去。我个人折腾这套系统半年后的体会是它带给我的不是“量化自我”的焦虑反而是种安心感。以前想回忆上午改了个什么文件名、傍晚复制过哪段代码全靠大脑硬拼现在打开时间线十秒钟就能定位。最后再分享一个小技巧把三张表统一时间轴之后每天早晨自动生成一份昨天的“数字日记”存成 Markdown 丢进自己的笔记目录。坚持一个月你会发现自己对工作节奏的了解比任何数据报表都清醒。
返回列表