TypeWell全攻略(一):键盘监听 + 数据采集,把每一次敲击变成可分析的数据
写在前面:为什么要先写数据采集?
如果把 TypeWell 比作一个人,那么:
- 数据采集是眼睛和耳朵 —— 得先“看见”用户按了什么键
- 热力图渲染是脸面 —— 把数据变成好看的界面
- AI 分析是大脑 —— 给出健康建议
没有眼睛耳朵,脸面和大脑都是摆设。所以第一篇,咱们先把“看见”这件事搞定。
开源仓库地址:Gitee TypeWell 雪豹同志
如果对你有帮助,欢迎 Star ⭐️

一、选型:为什么用 keyboard 库?
Python 里监听键盘,主流方案有这几个:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
keyboard |
跨平台、纯 Python、全局钩子 | Windows 需管理员权限 | 桌面工具、热键监听 |
pynput |
也跨平台、API 优雅 | 事件处理稍重 | 自动化脚本 |
tkinter 绑定 |
不用装第三方 | 只能监听自己窗口 | 游戏、编辑器插件 |
| ctypes 硬调 API | 性能极致 | 平台相关、代码复杂 | 底层开发 |
我选 keyboard 的理由特别简单:
import keyboard
keyboard.on_press(on_key_press) # 就这一行,搞定
能一行解决的,绝不用两行。 程序员的基本素养。
二、先看整体:数据采集层长什么样
打开 main.py,找到 KeyboardHeatmapApp 类的初始化:
class KeyboardHeatmapApp(QMainWindow):
def __init__(self):
super().__init__()
# 1. 内存缓存(核心!)
self.cached_data = {}
# 2. 初始化数据库
self.init_db()
# 3. 启动键盘监听
self.start_keyboard_listener()
# 4. 定时器:每 2.5 秒刷一次数据
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_frontend)
self.timer.start(2500)
代码就这么几行,但每一行背后都有故事。咱们一个一个拆。
三、键盘监听:让 Python 听见每一次敲击
3.1 最简单的监听
def start_keyboard_listener(self):
keyboard.on_press(self.on_key_press)
on_press 是个全局钩子。什么叫全局钩子?
不管你的焦点在浏览器、编辑器还是游戏里,只要键盘被按下,这个函数就会被调用。
操作系统会在处理键盘事件的时候,顺便通知你的程序一声。
3.2 回调函数里做什么?
def on_key_press(self, event):
key = self.normalize_key(event.name) # 1. 标准化按键名
self.cached_data[key] = self.cached_data.get(key, 0) + 1 # 2. 更新内存缓存
就两件事:
- 标准化:把
right shift变成shift,把-变成minus - 更新缓存:内存里记一笔,不写磁盘
四、内存缓存:为什么不能直接写数据库?
4.1 错误示范(千万别这么写)
# ❌ 错误示范:每次按键都写数据库
def on_key_press(self, event):
conn = sqlite3.connect('keyboard_heatmap.db')
conn.execute("UPDATE key_usage SET count = count + 1 WHERE key = ?", (key,))
conn.commit()
conn.close()
假设你打字速度是每秒 10 个键,那这一秒里就要:
- 10 次建立数据库连接
- 10 次磁盘 I/O
- 10 次事务提交
程序直接卡成 PPT,磁盘寿命也堪忧。
4.2 正确做法:内存缓存 + 批量写入
# ✅ 正确示范:先攒着,定时写
def on_key_press(self, event):
key = self.normalize_key(event.name)
self.cached_data[key] = self.cached_data.get(key, 0) + 1 # 内存操作,快如闪电
定时器每 2.5 秒调用一次 update_frontend,集中处理:
def update_frontend(self):
if not self.cached_data:
return
# 1. 批量写入数据库
conn = sqlite3.connect('keyboard_heatmap.db')
for key, count in self.cached_data.items():
conn.execute("INSERT OR IGNORE INTO key_usage (key, count) VALUES (?, 0)", (key,))
conn.execute("UPDATE key_usage SET count = count + ? WHERE key = ?", (count, key))
conn.commit()
conn.close()
# 2. 清空缓存
self.cached_data.clear()
性能提升:100 次磁盘写入 → 1 次磁盘写入。
五、按键标准化:为什么名字会乱?
5.1 看看实际返回的键名
| 物理按键 | keyboard 返回的名字 |
|---|---|
| 右 Shift | right shift |
| 左 Shift | left shift |
| 减号 | minus 或 - |
| Esc | escape 或 esc |
| 空格 | space |
如果不做处理,数据库里就会出现:
shiftright shiftleft shift-minus
同一个物理按键,五六个名字,统计直接崩了。
5.2 为什么 TypeWell 要区分左右键?
这里有个重要的设计决定:TypeWell 保留了左右键的区别。
为什么?因为人体工学分析需要知道哪只手在按。左 Shift 是左手小指按的,右 Shift 是右手小指按的。如果把它们统一成 shift,就分不清左右手了。
所以 TypeWell 的做法是:
- 左右键保留原名:
left shift、right shift、left ctrl、right ctrl - 符号键统一命名:
-和minus统一成minus,[和lbracket统一成lbracket
5.3 TypeWell 的标准化函数
def normalize_key(self, key):
key = key.lower()
# 处理特殊符号(这些需要统一,没有左右之分)
if key == 'escape':
return 'esc'
if key == 'backtick' or key == '`':
return 'backquote'
if key == 'minus' or key == '-':
return 'minus'
if key == 'equal' or key == '=':
return 'equal'
if key == 'bracketleft' or key == '[':
return 'lbracket'
if key == 'bracketright' or key == ']':
return 'rbracket'
if key == 'backslash' or key == '\\':
return 'backslash'
if key == 'semicolon' or key == ';':
return 'semicolon'
if key == 'apostrophe' or key == "'":
return 'quote'
if key == 'comma' or key == ',':
return 'comma'
if key == 'period' or key == '.':
return 'period'
if key == 'slash' or key == '/':
return 'slash'
# 功能键(没有左右之分)
if key == 'print screen':
return 'printscreen'
if key == 'scroll lock':
return 'scrolllock'
if key == 'pause break':
return 'pause'
if key == 'page up':
return 'pageup'
if key == 'page down':
return 'pagedown'
# 左右键保留原样返回,不做统一
# 比如 left shift、right shift、left ctrl、right ctrl 都会原样保留
return key
注意:前端
script.js里有一个几乎一模一样的normalizeKey函数。这是故意的——前后端必须用同一套命名规则,否则数据对不上。
5.4 为什么左右键要保留?
看一眼前端 fingerMap 的定义就明白了:
const fingerMap = {
'left_pinky': ['q', 'a', 'z', '1', 'tab', 'caps', 'left shift', 'left ctrl'],
'right_pinky': ['p', ';', '/', '0', '-', '=', 'backspace', 'enter', 'right shift', 'right alt'],
// ...
};
左右 Shift 分配给不同的手指:
left shift→ 左手小指right shift→ 右手小指
如果把它们统一成 shift,就不知道是哪根小指在受苦了。

六、数据库设计:简单到不能再简单
def init_db(self):
conn = sqlite3.connect('keyboard_heatmap.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS key_usage
(key TEXT PRIMARY KEY, count INTEGER DEFAULT 0)''')
conn.commit()
conn.close()
就两个字段:
key:标准化后的按键名(主键)count:累计敲击次数
为什么这么简单?因为 TypeWell 只关心当前的热力图状态,不需要历史趋势。如果需要按天统计,以后再加表。
设计原则:先跑起来,再优化。
七、程序退出时数据会丢吗?
这是个好问题。
7.1 风险分析
如果用户直接关窗口,内存缓存里的数据还没写进数据库,不就丢了吗?
7.2 TypeWell 的应对
def closeEvent(self, event):
self.is_closing = True
if self.timer.isActive():
self.timer.stop()
event.accept()
等等,这里没有写数据库?
仔细一想,其实不需要。因为定时器每 2.5 秒刷一次盘,用户关窗口的时候,最多丢 2.5 秒的数据。对于热力图这种统计型应用,2.5 秒的数据丢失影响微乎其微。
如果非要较真,可以再加一次强制刷盘:
def closeEvent(self, event):
self.update_frontend() # 强制写入
super().closeEvent(event)
但 TypeWell 没这么做——够用就好。
八、线程安全:有没有坑?
这是一个隐藏的坑。
8.1 问题在哪
keyboard.on_press在独立线程里回调- PyQt 主线程在跑 UI 和定时器
- 两个线程同时操作
self.cached_data
# 监听线程执行
def on_key_press(self, event):
self.cached_data[key] = self.cached_data.get(key, 0) + 1 # 写
# 主线程执行
def update_frontend(self):
for key, count in self.cached_data.items(): # 读
# 写数据库
self.cached_data.clear() # 清空(写)
理论上,这会有竞争条件。比如主线程正在遍历 cached_data,监听线程突然往里加了个新键,遍历就可能出错。
8.2 TypeWell 为什么没出事
- 监听线程只写,主线程只在刷盘时读+清空
- 读写冲突的时间窗口极短
- 即使丢一两次计数,热力图整体趋势不受影响
8.3 如果想写得更健壮
import threading
class KeyboardHeatmapApp:
def __init__(self):
self.lock = threading.Lock()
self.cached_data = {}
def on_key_press(self, event):
with self.lock:
self.cached_data[key] = self.cached_data.get(key, 0) + 1
def update_frontend(self):
with self.lock:
data_to_flush = self.cached_data.copy()
self.cached_data.clear()
# 用 data_to_flush 写数据库
TypeWell 为了简洁没加,但你要知道这个坑存在。
九、完整流程串联
现在把整个数据采集层串起来看:
用户按键
↓
[keyboard.on_press 回调](监听线程)
↓
normalize_key() 标准化
↓
cached_data 字典 +1(内存操作)
↓
【每隔 2.5 秒】
↓
[主线程定时器触发 update_frontend]
↓
遍历 cached_data,批量写入 SQLite
↓
清空 cached_data
↓
读取数据库最新数据(下篇讲)
↓
推给前端渲染
十、踩坑总结
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 性能差 | 打字卡顿 | 内存缓存 + 批量写入 |
| 键名混乱 | 同一个键多个名字 | normalize_key 标准化(符号统一,左右保留) |
| 退出丢数据 | 刚关程序,最后几秒的数据没了 | 2.5 秒刷一次,影响极小 |
| 线程安全 | 多线程操作同一字典 | 理论上要加锁,实际没出事 |
| 管理员权限 | Windows 监听失败 | 以管理员身份运行 |
写在最后
这一篇的核心就三句话:
- 监听:
keyboard.on_press注册回调 - 缓存:按键数据先攒在内存里
- 刷盘:定时批量写入数据库
代码都在 main.py 里,加起来不到 100 行。但就是这 100 行,决定了 TypeWell 能不能流畅地听见每一次敲击。
如果觉得文章不错,欢迎点赞评论收藏,更多内容,敬请关注!
更多推荐


所有评论(0)