news 2026/9/28 2:12:07

Python键鼠监听库怎么选?pynput实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python键鼠监听库怎么选?pynput实战指南

很多人第一次搜“Python 键鼠库”的时候,看到 pyautogui、pynput、keyboard、mouse 这一堆名字会直接懵掉。这不是你一个人的问题——这些库的功能边界有大量重叠,而网上的教程又常常只讲其中一个,于是很多人装了这个库发现监听功能缺失,装了那个库又发现控制不好用。这篇文章我从实际项目的角度出发,把“监听”这条主线讲透:怎么选、怎么装、怎么用,以及真正跑起来以后会遇到哪些坑。如果你是做自动化工具、效率脚本或者想给自己的设备加一些趁手的快捷键,这篇应该能帮你省不少时间。

1. 先想清楚需求:选监听还是控制,选哪个库

1.1 “驱动库”这个说法其实不太准确

标题里写了“键鼠驱动库”,但严格讲,pynput、pyautogui 这些并不是传统意义上的驱动。驱动通常指操作系统底层和硬件之间那层软件,而 Python 的键鼠库全部工作在用户态,它们是间接调用操作系统提供的接口来实现功能的。

以 pynput 为例,它在 Windows 上底层走的是 Win32 Hook(SetWindowsHookEx),在 macOS 上使用 Quartz Event Tap,在 Linux 上依赖 X11 的 Record 扩展。什么意思呢?就是说这些库不是自己发明了一套捕捉输入的方式,而是向系统申请“我帮你转发一份输入事件副本给我”。这也是为什么它们不需要装驱动文件、不需要管理员权限(某些特定场景除外),一个 pip install 就能用。

理解这一点很重要,因为它决定了后续排查问题的方向。比如你在 Linux 的 Wayland 会话下发现监听不到任何按键,那不是库坏了,而是 Wayland 的安全模型根本不允许普通程序全局监听输入,这时候换 X11/XWayland 会话就能解决。先想明白底层原理,才能不被各种报错带偏。

1.2 控制与监听是两条不同的技术路线

很多初学者以为“能控制键鼠的库”一定也能监听键鼠,其实不一定。这两个需求在实现层面是完全不同的方向:

  • 控制(模拟输入):向系统投递一条输入事件,让它以为用户真的按了某个键。代表库是 pyautogui,pynput 的 Controller 也属于这一类。
  • 监听(捕获输入):从系统中读取正在发生的输入事件。代表库是 pynput 的 Listener,keyboard 库也支持。

pyautogui 在监听方面几乎没有建树,它最多能通过截屏识别像素来判断屏幕状态;keyboard 库在 Windows 和 Linux 上监听能力很强,但 macOS 支持不完整;pynput 是少见的“监听和控制都做得比较均匀”的库。所以选库之前先问自己一句:你到底是要“读”还是要“写”?

1.3 四个主流库横向对比

我整理了一张表,基本覆盖了日常绝大多数选型场景:

库监听控制WindowsmacOSLinux典型用途
pynput完整完整完整完整X11下完整跨平台监听、全局热键
pyautogui弱完整完整完整完整屏幕自动化、UI操作
keyboard完整完整最完善不完善完整Windows环境快捷热键
mouse完整完整完整不完善完整纯鼠标监听控制

我最终的选型结论是:如果只能选一个库,选 pynput。理由有三个——第一,它是纯 Python 实现,API 设计干净,Listener 和 Controller 的抽象非常直观;第二,跨平台支持最均衡,Windows、macOS、Linux 都有维护;第三,它同时提供监听和控制,项目做到后来发现需要模拟输入时,不用再引入一个新库。

2. 安装没有想象中简单:环境、权限与验证

2.1 虚拟环境是第一步,别省

我知道很多人图省事直接pip install pynput装到全局环境,这在临时脚本里问题不大,但一旦你同时维护两三个项目,依赖冲突迟早找上门。键鼠库不像 numpy 那种大头依赖,但它的版本差异确实会带来行为变化——比如 pynput 早期版本在 macOS 上对特殊按键的命名就不太一样。

我习惯在项目开始时先建一个干净的环境:

python3 -m venv venv source venv/bin/activate # Linux/macOS 用这条 venv\Scripts\activate # Windows 用这条

注意 Windows 下如果激活命令执行失败,大概率是 PowerShell 执行策略拦住了脚本,可以在管理员 PowerShell 里先跑一句Set-ExecutionPolicy -ExecutionPolicy RemoteSigned,或者改用cmd激活。这个坑虽然和键鼠库无关,但初学者经常在这里卡住,先提前避掉。

2.2 pip 安装与依赖解析

激活环境后安装 pynput 只需要一行:

pip install pynput

如果下载慢,可以临时指定国内镜像源:

pip install pynput -i https://pypi.tuna.tsinghua.edu.cn/simple

pynput 的依赖非常轻,它依赖 pyobjc(macOS 上)、pywin32(Windows 上)这些系统桥接库,安装时 pip 会自动处理。不过有一点值得注意:在 macOS 上,如果 Python 是从 python.org 官网安装的,那没问题;如果是用 Homebrew 装的 Python,偶尔会遇到 pyobjc 编译不过的情况。这时候最快的办法是换回官网版 Python,或者用 conda 环境。

2.3 系统权限是最大的坑

安装完不等于能跑。键鼠监听涉及系统级输入事件,主流操作系统都做了安全限制:

  • Windows:普通用户权限下可以监听大部分应用,但如果你要监听“以管理员身份运行”的进程(比如某些游戏、IDE),你的 Python 脚本也必须以管理员身份运行,否则事件会漏。这是一个非常容易被忽略的边界问题。
  • macOS:在“系统设置 → 隐私与安全性 → 辅助功能”里,需要把你的 Python 解释器(或者终端应用)加入白名单。这个权限不会在安装时自动申请,等你第一次运行监听脚本,会发现事件一个都收不到,控制台却不报任何错误。
  • Linux:X11 会话下基本没有权限问题,Wayland 会话下几乎无法全局监听。有些发行版还依赖 python-xlib 库,如果 import 阶段报错,先pip install python-xlib补上。

macOS 权限那个问题尤其阴险,因为它不报错。我第一次在 Mac 上跑监听脚本时,以为是代码写错了,折腾了半天才发现是辅助功能权限没开。如果你在 macOS 上遇到“监听无反应”,先别改代码,去设置里勾选权限再重跑一次。

2.4 怎么验证安装成功了

写一个最小监听脚本,跑通就算环境没问题:

from pynput import keyboard def on_press(key): print(f"按下: {key}") if key == keyboard.Key.esc: return False with keyboard.Listener(on_press=on_press) as listener: listener.join()

按任意键,终端能打印出按键信息,按 Esc 退出,说明整条链路是通的。如果这一步能跑通,后面所有问题都是代码层面的;如果跑不通,先回到权限和环境排查,不要急着往下写功能。

3. Listener 的运行机制:线程、回调和异常

3.1 Listener 是一个线程,不是魔法

pynput 的Listener对象创建后,它会启动一个独立的后台线程,由这个线程去接收操作系统转发过来的输入事件。你通过参数传进去的on_press、on_release这些回调函数,并不是主线程里被调用的,而是在这个监听线程里执行的。

理解线程模型有多关键?我举一个实际场景:你在on_press回调里写了一个time.sleep(10),表面上看只是处理变慢了,实际上整个输入监听都会被拖住。因为回调是同步执行的,监听线程就一个,你阻塞了回调就等于阻塞了所有后续事件的接收。键盘输入还好,鼠标连点场景下你会明显感觉到事件延迟。

正确做法是回调里只做轻量处理——收集事件、入队、触发信号,把耗时操作丢到别的线程去。后面写完整项目的时候我会再展示这个结构。

3.2 回调是同步执行,异常会杀死监听器

还有一个容易被坑的点:如果你在回调里写了一个没处理的异常,比如访问了不存在的变量,这个异常会直接冒泡到监听线程里,导致监听器静默退出。你的脚本可能还在运行,但事件已经收不到了,而且什么提示都没有。

我习惯在所有回调里用 try/except 兜底,至少保证监听器不会因为一个脏数据就死掉:

def on_press(key): try: # 你的处理逻辑 pass except Exception: # 记录异常,但不要让它杀死监听线程 pass

如果你确实需要捕获监听器的异常,pynput 的Listener还有一个exception_handler参数,可以统一处理监听线程里的异常。不过日常使用中,回调内部兜底已经足够了。

3.3 suppress 参数到底是什么意思

Listener(..., suppress=True)这个参数常被误解。它的作用是:当监听器捕获到输入事件时,是否阻止该事件继续传递给系统里的其他应用。

打个比方,正常监听就像在快递柜旁边安了个摄像头,快递该投递还投递;suppress=True则是在快递柜前面装了个拦截闸门,快递先被你的程序拿走,其他人就拿不到了。这在写全局屏蔽某些按键、游戏辅助、无障碍工具时非常有用。

需要特别说明:suppress在 Windows 和 macOS 上实现得很干净,但在 Linux 上,由于底层 X11 Record 接口的工作方式,抑制事件的支持并不完整。如果你依赖这个特性做跨平台工具,一定要在目标平台上提前验证。

4. 键盘与鼠标事件监听:完整代码与逐行拆解

4.1 键盘监听的基本写法

键盘监听的核心就是keyboard.Listener,传入按下的回调函数和松开的回调函数:

from pynput import keyboard def on_press(key): # 普通字符键,比如字母 a、数字 1 if hasattr(key, "char") and key.char is not None: print(f"字符键: {key.char}") else: # 特殊键,比如 Esc、F1、Ctrl print(f"功能键: {key.name}") def on_release(key): print(f"松开: {key}") if key == keyboard.Key.esc: return False # 返回 False 会终止监听 with keyboard.Listener( on_press=on_press, on_release=on_release ) as listener: listener.join()

这里有个细节值得展开:key对象在不同情况下类型不一样。对于普通字符键,它是一个KeyCode对象,带有char属性,比如key.char == "a";对于 Shift、Ctrl、Esc 这类特殊键,它是keyboard.Key枚举,没有char属性,但可以通过key.name拿到字符串名称。

所以每写一个监听回调,我都会先做类型判断。直接把key拿去比较可能会遇到类型不一致的问题,比如key == "a"永远是 False,因为key是KeyCode对象,不是字符串。正确写法是key.char == "a"或者getattr(key, "char", None) == "a"。

4.2 组合键:自己拼不如用 HotKey

监听单个按键很容易,但做全局热键就复杂了。你要自己记状态:Ctrl 有没有被按住、Alt 按下了没有、最后是不是按了 H。pynput 为此封装了一个HotKey类:

from pynput import keyboard def on_activate(): print("全局热键 Ctrl+Alt+H 触发了") def for_canonical(f): return lambda key: f(listener.canonical(key)) hotkey = keyboard.HotKey( keyboard.HotKey.parse("<ctrl>+<alt>+h"), on_activate ) with keyboard.Listener( on_press=for_canonical(hotkey.press), on_release=for_canonical(hotkey.release) ) as listener: listener.join()

这段代码里有几个点必须讲明白:

  • keyboard.HotKey.parse("<ctrl>+<alt>+h")会把字符串解析成一个组合键定义。尖括号里的内容是修饰键,小写字母是普通键。支持的修饰键包括<ctrl>、<alt>、<shift>、<cmd>。
  • listener.canonical(key)是一个规范化方法。因为同一个物理按键在不同键盘布局、不同操作系统上可能对应不同的内部编码,canonical会把它转成一个标准形式,这样才能和组合键定义里预期的值做准确比较。
  • for_canonical这个包装函数的目的,是把hotkey.press和hotkey.release改成接收经过canonical处理后的键值,保证匹配逻辑一致。

这一段如果不理解,直接复制使用也没有问题,但你会踩“热键偶尔触发偶尔不触发”的坑。原因通常就是少了canonical这一层规范化。

4.3 鼠标监听:坐标、点击、滚轮

鼠标监听和键盘监听的结构几乎一样,也是三个可选回调:移动、点击、滚动。

from pynput import mouse def on_move(x, y): print(f"鼠标移动到了 ({x}, {y})") def on_click(x, y, button, pressed): action = "按下" if pressed else "松开" print(f"{action} {button} 于坐标 ({x}, {y})") def on_scroll(x, y, dx, dy): print(f"滚轮滚动 于坐标 ({x}, {y}) 滚动增量 ({dx}, {dy})") with mouse.Listener( on_move=on_move, on_click=on_click, on_scroll=on_scroll ) as listener: listener.join()

on_move会在鼠标每移动一个像素时触发,坐标(x, y)是屏幕绝对坐标,原点在屏幕左上角。on_click里的button是一个Button枚举,可能是Button.left、Button.right或Button.middle。on_scroll的dx和dy表示滚轮的方向和步进。

这里有个性能问题值得提醒:on_move的触发频率非常高,如果鼠标在桌面上滑动一圈,这个回调可能被调用几十上百次。如果你在里面做了重活,比如写入数据库或者统计路径,CPU 会直接被吃满。实际项目里我一般会在回调里做节流(throttle)——记录上一次处理时间,间隔小于某个阈值就跳过。

4.4 组合一个后台热键工具

把键盘监听、热键、线程模型结合起来,做一个可独立运行的后台工具。这个工具的作用是:监听全局热键 Ctrl+Alt+H,触发后执行一个耗时任务,按 Ctrl+Alt+Q 退出程序。

import threading import time from pynput import keyboard def run_task(): """耗时任务,单独线程执行""" print("任务开始...") time.sleep(3) print("任务完成") def on_activate_h(): print("检测到 Ctrl+Alt+H,启动任务") threading.Thread(target=run_task, daemon=True).start() def on_activate_q(): print("检测到 Ctrl+Alt+Q,退出程序") listener.stop() def for_canonical(f): return lambda key: f(listener.canonical(key)) hotkey_h = keyboard.HotKey( keyboard.HotKey.parse("<ctrl>+<alt>+h"), on_activate_h ) hotkey_q = keyboard.HotKey( keyboard.HotKey.parse("<ctrl>+<alt>+q"), on_activate_q ) listener = keyboard.Listener( on_press=for_canonical(hotkey_h.press), on_release=for_canonical(hotkey_h.release) ) # 这里把 q 的 press 也接进去,两个热键复用一个监听器 listener.on_press = lambda key: ( hotkey_h.press(listener.canonical(key)), hotkey_q.press(listener.canonical(key)) ) listener.start() print("监听已启动,按 Ctrl+Alt+H 执行任务,Ctrl+Alt+Q 退出") try: while listener.is_alive(): time.sleep(0.5) except KeyboardInterrupt: pass finally: listener.stop() print("程序已退出")

这个例子里有一个设计决策:耗时任务丢到threading.Thread里去跑,而不是直接在on_activate_h里面 sleep。这样做的原因前面已经说过——回调阻塞会导致监听线程卡住,后续所有键鼠事件都会堆积,程序会感觉“很卡”。

5. 从“听到了”到“用起来”:事件驱动架构怎么搭

5.1 事件分发:别把业务逻辑焊死在回调里

监听回调一旦复杂起来,最忌讳的就是把所有业务逻辑都堆在on_press里面。比如你既想记录日志,又想更新状态栏,还想触发一个外部程序,全写在一个回调函数里,代码很快就没法维护了。

我的做法是引入一个简单的事件分发层。监听回调只做一件事:把事件投递给一个队列或注册表,由专门的处理函数去消费。够用的方案是建一个中心化的EventHandler:

import queue import threading event_queue = queue.Queue() def on_press(key): event_queue.put(("press", key)) def worker(): while True: event_type, key = event_queue.get() if event_type == "press": handle_press(key) event_queue.task_done() def handle_press(key): # 统一在这里处理业务逻辑 pass threading.Thread(target=worker, daemon=True).start()

这样一来,监听回调本身永远是轻量的,业务逻辑集中在一个 worker 里处理,逻辑清晰也好调试。如果你做过 GUI 开发,会发现这个模式和 UI 线程里的消息循环非常像。

5.2 回调里坚决不做重活

我再强调一次这个原则,因为它值得重复:监听回调里绝对不要做任何可能阻塞的操作。写文件、发网络请求、打开数据库、执行外部命令,这些都应该挪到别的线程或队列里。

背后的原因很实际:监听线程不处理完当前回调,就不会接收下一个事件。你在回调里写了subprocess.run(["some_command"]),假设这个命令执行要 5 秒,那么这 5 秒内用户的任何键盘鼠标操作都会被忽略。对于工具类软件来说,这种体验是不可接受的。

如果实在要用线程,建议用线程池而不是每次手动threading.Thread。任务密集时,手动创建线程会带来不必要的开销和不可控的并发数。concurrent.futures.ThreadPoolExecutor是更好的选择:

from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=2) def on_activate(): executor.submit(run_task)

5.3 日志、异常与优雅退出

一个长期运行的后台监听工具,日志系统必须从第一天就做好。我之前写过一个监听脚本,跑了一下午,中途崩了一次,因为没有日志,完全不知道崩在哪里。后来我养成了一个习惯:所有回调函数入口先写一行调试日志,异常处理里也写异常信息。

import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", filename="tool.log" ) def on_press(key): try: logging.debug(f"on_press: {key}") # 业务处理 except Exception as e: logging.error(f"on_press 异常: {e}", exc_info=True)

优雅退出也值得规划。程序如果长时间监听,你不可能靠按 Ctrl+C 来终止,因为终端可能都被别的进程占用了。设计一个明确的退出热键——就像前面例子里的 Ctrl+Alt+Q——然后在退出回调里调用listener.stop(),这是最可靠的方式。

6. 实测踩坑记录(附排查思路)

6.1 闭包传参的经典陷阱

我写过一段代码,想为三个不同的按键注册三个回调,每个回调打印自己的按键名。第一版是这么写的:

keys = ["a", "b", "c"] callbacks = [] for key in keys: callbacks.append(lambda: print(key)) # 调用所有回调 for cb in callbacks: cb() # 结果打印的是 c、c、c

结果三个回调全部打印了c。原因很经典:Python 的闭包捕获的是变量本身,而不是变量当时的取值。循环结束后key的值是"c",所有 lambda 引用的都是同一个key。

解决办法是用默认参数把当前值绑定到函数上:

callbacks.append(lambda k=key: print(k))

这个坑在监听回调里特别容易遇到——当你想在热键回调里传入不同的上下文参数时,很容易写出这种永远取到最后一份数据的代码。排查思路也简单:如果多个回调表现得完全一样,先怀疑闭包变量捕获。

6.2 回调里写文件导致进程卡死

还有一个案例:我在监听回调里做统计,每收到一次键盘事件就打开一个文件追加一行记录。脚本在小规模测试时没有问题,但连续跑了几小时后,程序开始变慢,最后直接卡死。

原因有两个层面。第一,频繁打开关闭文件带来不必要的 IO 开销;第二,更糟糕的是 Python 的print和文件写入都涉及到全局锁,监听线程一直抢着写文件,会影响主线程的其他操作。

正确做法是:回调里把事件放进一个队列,专门的写日志线程负责批量落盘。或者用logging模块配合一个异步 handler,它内部已经帮你做了队列缓冲,不需要自己再造轮子。总之核心原则还是那句话:回调里不碰 IO。

6.3 macOS 辅助功能权限失效前后

在 macOS 上,我遇到过一种诡异情况:辅助功能权限已经给了,监听脚本也能正常运行,但重启电脑后,权限突然失效,监听收不到任何事件。

排查过程是这样的:先确认代码没变,再确认终端应用仍然在辅助功能列表里,最后发现问题是权限列表里的 Python 解释器路径变了。因为我用虚拟环境跑脚本,激活虚拟环境后python指向的是venv/bin/python,但有时我用系统自带 Python 启动了脚本,两个路径对应的权限状态不一样。

解决办法是:把权限列表中你实际使用的 Python 解释器路径加进去,并且如果换了 Python 版本或换了解释器,重新检查一次辅助功能列表。这个坑没有代码解法,完全是系统设定层面的问题,记录下来是为了让大家知道方向。

6.4 监听器静默退出,怎么自动拉起

监听器可能因为各种原因退出,除了回调异常,还可能遇到系统层面的资源回收问题。程序本身不崩溃,但监听线程就悄无声息地死了。这种问题最麻烦,因为没有任何报错。

我的方案是给监听器加一个看门狗循环:定期检查listener.is_alive(),如果发现监听线程死了,就重新创建监听器:

listener = keyboard.Listener(on_press=on_press) listener.start() while True: time.sleep(3) if not listener.is_alive(): print("监听线程意外退出,正在重启...") listener = keyboard.Listener(on_press=on_press) listener.start()

这个方案要注意一点:重启后的监听器里注册的回调必须引用同一个事件队列或处理器,否则重启之后业务逻辑可能还会二次注册,导致事件被重复处理。用之前说的队列方案,这个问题就不存在——回调永远只是往同一个队列里丢事件,谁消费都一样。

7. 监听只是开始:用同一套库实现控制

监听写顺手之后,项目的下半场通常是模拟控制。pynput 里 Controller 和 Listener 是一对孪生兄弟,共用同一套按键和按钮枚举。

from pynput.keyboard import Controller as KeyController from pynput.mouse import Controller as MouseController, Button import time keyboard = KeyController() mouse = MouseController() # 模拟键盘输入 keyboard.type("Hello, automation!") # 模拟按下回车 keyboard.press(keyboard.Key.enter) keyboard.release(keyboard.Key.enter) # 模拟鼠标点击 mouse.position = (960, 540) # 屏幕中心 time.sleep(0.1) mouse.click(Button.left) # 模拟拖拽 mouse.move(100, 50) mouse.press(Button.left) mouse.move(200, 150) mouse.release(Button.left)

这里有几个细节需要留意。keyboard.type模拟输入的字符串时,特殊字符的映射依赖于当前键盘布局,如果你的脚本里有非英文字符,建议小范围测试确认输出正确。mouse.position = (x, y)是瞬间移动鼠标,而mouse.move(dx, dy)是相对当前位置的增量移动。拖拽操作必须按照“按下 → 移动 → 松开”的完整序列执行,缺了任何一步,系统可能无法将它识别为一次有效的拖拽。

组合使用监听和控制,能实现很多有意思的工具。比如做一个“按 F2 自动填入当前时间”的小脚本:监听器捕获 F2 按下事件,Controller 模拟输入一段格式化后的时间字符串。这本质上就是很多效率工具的核心逻辑,只是现在只需要几十行 Python 就能实现。

写到这里,这套从安装、监听到控制的技术栈基本已经闭环了。对我来说,键鼠自动化最有价值的地方不在于某个具体的功能,而在于它提供了一种“程序可以感知并响应真实用户操作”的能力。如果你只是想给自己的开发环境加一些顺手的小工具,从监听开始,慢慢加上控制,会比一上来就追求复杂的自动化框架更容易出成果。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:11:44

嵌入式硬件调试五大核心方法与实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:11:03

游泳溺水识别数据集实战:COCO转YOLO与YOLOv8训练指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:10:53

ESP32-C3 JTAG调试为何必须用ESP-Prog

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:10:02

STM32智能鸽子驯养系统:从电路到实物的全流程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:08:27

Deepsort+OpenCV实现ROI区域行人测速统计系统详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:07:39

DCM+MCP:在MCU上构建可验证因果AI执行体

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华