
简介PyAutoGUI 0.9.26 的源码软件包面向希望用 Python 完成鼠标键盘模拟、桌面自动化与界面测试的开发者也适合初学者借助示例理解跨平台 GUI 控制流程解决重复性桌面操作与脚本化交互问题。包内共 37 个文件以 14 个 Python 源码模块为主辅以 12 个 rst 文档和少量 txt、pkg-info、配置文件及构建脚本压缩包约 53KB源码涉及 Windows、macOS 与 Linux 后端、截图定位、窗口操作和缓动函数等模块文档则覆盖安装、鼠标、键盘、截图、弹窗、速查表与路线图等主题。已有 486 人学习。读者可从中获得可运行的库源码、较完整的 API 文档与基础测试用例便于对照理解坐标定位、截图识别、热键触发及异常保护机制也可作为桌面自动化、数据采集脚本、界面自动化设计与自动化测试项目的参考实现借目录组织与跨平台适配思路少走弯路。1. PyAutoGUI 0.9.26 软件包我为什么还在用这个老版本做桌面自动化桌面自动化接到手里十有八九不是写个脚本跑一次而是要在别人机器上连续跑几十天。这种活我一般不追新版本PyAutoGUI 0.9.26 这个软件包就是长期留在工具箱里的那一份纯 Python、没有编译依赖、API 从 0.9.x 起基本没动过老脚本拷过去还能直接跑。它干的事很朴素——接管鼠标键盘、截屏找图、弹框提示把「人点一遍」变成「脚本点一遍」。适合谁做批量录入、跨系统数据搬运、客户端回归点检、给内部工具补一层 UI 自动化的同学。不适合谁要驱动浏览器 DOM 的去用浏览器自动化那套要高帧率游戏操作的也别指望它它的每次调用都有几十毫秒的固定开销。接下来按「装得上、动得了、认得出、跑得稳」四步拆中间夹一节我踩过的坑。2. 依赖与安装0.9.26 的六件套和一个可选件2.1 先看依赖再谈安装0.9.26 这个版本的一个重要变化是把功能拆成了几个小包主包只做调度。所以你在完全离线的机器上只拷一个pyautogui目录进去import阶段就可能直接崩。常见做法是先把依赖关系理清楚再决定用哪种安装方式。包名在 0.9.26 里负责什么缺了会怎样pyscreeze截图、图像定位、像素取色主库 import 直接失败pillow图像对象的读写pyscreeze 的底层截图返回 None 或抛异常pygetwindow窗口枚举、激活、按标题匹配窗口相关函数不可用随平台变化pymsgboxalert / confirm / prompt 弹框调用弹框时抛 ImportErrorpytweeningmoveTo 的 tween 缓动曲线tween 参数被忽略mouseinfo独立小工具显示鼠标坐标和颜色不影响主库运行opencv-pythonconfidence 图像匹配的实现传 confidence 参数会报错pyperclip剪贴板读写不是强制依赖但中文输入基本离不开它前六个是安装时自动带的后两个要自己补。我一般会额外装 opencv因为locateOnScreen的confidence参数在默认的像素级匹配下是没有的——不装 opencv你只能要求模板图 100% 一致换个主题、换台显示器就找不到。提示检查是否真的装上了别只看pip list。有些机器上有多个 Pythonpip指向的和脚本运行的解释器不是同一个这种情况在 Windows 上尤其常见。2.2 三条安装路径按环境选先给出三条路径从最省事到最可控排列。命令行里的版本号一定要写死写成PyAutoGUI0.9.26不要写pyautogui否则装到的可能是另一个版本行为差异会让你怀疑人生。# 路径一能联网锁定版本直接装最省事 python -m pip install PyAutoGUI0.9.26 # 路径二内网或离线机器先在联网机器把 wheel 拉全再整体拷过去 python -m pip download PyAutoGUI0.9.26 -d ./wheels python -m pip install --no-index --find-links./wheels PyAutoGUI0.9.26 # 路径三手上就是源码 zip要改代码或跑自带示例 unzip PyAutoGUI-0.9.26.zip -d ./src cd ./src/PyAutoGUI-0.9.26 python -m pip install .第一段没什么好说的网络通、索引能访问就能成。第二段的关键是--no-index和--find-links这一对参数--no-index把外网索引彻底关掉--find-links指向本地目录这样 pip 只会在你拷过去的这批文件里挑装了哪些包、什么版本事后完全可复现。第三段用install .而不是直接install PyAutoGUI-0.9.26.zip原因是后者装进 site-packages 的目录名和报错栈里的路径都比较难读出问题时定位成本高。--no-deps我很少用。它确实能跳过依赖解析但跳过的代价是你得自己把 pyscreeze、pillow、pymsgbox、pytweening 一个个按正确版本补上一旦漏装报错信息往往指向一个跟真实原因无关的地方。真要离线用路径二一次性下全更省心。关于 Python 版本我一般把运行环境锁在 3.8 到 3.10 这个区间。理由不是这个库有多挑而是桌面自动化脚本通常还要和一堆老依赖共存跑在最新解释器上时distutils之类的老引用容易给你来个下马威。真要上更高版本先在虚拟环境里跑一遍下面这段自检别直接上生产机。2.3 装完先跑一段自检装完立刻验证别等到业务逻辑写完才发现截图不了。这段代码覆盖四件事版本、分辨率、虚拟桌面边界、截图与 opencv 通路。import sys import pyautogui as pag print(python:, sys.version.split()[0]) print(pyautogui:, getattr(pag, __version__, unknown)) print(主屏尺寸:, pag.size()) # Size(width1920, height1080) print(当前鼠标:, pag.position()) # Point(x..., y...) # Windows 上取虚拟桌面多显示器的边界非 Windows 会走到 except try: import ctypes u ctypes.windll.user32 print(虚拟桌面 x/y/w/h:, u.GetSystemMetrics(76), u.GetSystemMetrics(77), u.GetSystemMetrics(78), u.GetSystemMetrics(79)) except Exception as e: print(虚拟桌面查询跳过:, e) # 截图能力macOS 首次会弹屏幕录制权限框 shot pag.screenshot() print(截图对象:, type(shot).__name__, 尺寸:, shot.size) # 图像匹配能力confidence 依赖 opencv try: import cv2 print(opencv:, cv2.__version__) except ImportError: print(opencv 未安装confidence 参数不可用)逻辑说明一下。pag.size()返回的是主显示器分辨率注意是逻辑分辨率系统缩放 150% 时这个值会比物理像素小后面讲踩坑会再提。position()用来确认坐标原点在左上角。中间那段GetSystemMetrics里76/77 是虚拟桌面的左上角坐标78/79 是宽高——副屏接在左边时76 可能是个负数这个负数就是你后面算截图区域偏移量的依据。最后两段分别验证截图链路和 opencv 链路screenshot()返回的是 PIL 的 Image 对象看类型对不对就知道 pillow 有没有正常加载。参数改动的空间很小但有一个值得注意这段自检里的screenshot()是全屏截取第一次跑如果明显卡顿超过一秒说明你的机器分辨率很高或者显卡驱动有问题后面所有的图像定位都会受这个基础耗时拖累需要提前把region参数纳入设计。整段脚本建议存成env_check.py单独保留换一台机器先跑它能省掉后面很多「到底是环境问题还是代码问题」的纠结。3. 坐标、FAILSAFE 与多屏鼠标控制的三条命脉3.1 坐标系和那个能救命的 FAILSAFEPyAutoGUI 的坐标体系是屏幕绝对坐标原点在左上角x 向右递增y 向下递增单位是逻辑像素。所有移动类函数都接受一个坐标元组moveTo是移动到绝对位置moveRel是相对当前位置偏移。这套东西本身没什么可讲的真正值得单独拎出来说的是FAILSAFE。FAILSAFE默认是 True效果是当鼠标被移动到屏幕左上角(0, 0)位置时pyautogui 的下一次行为调用会抛出FailSafeException脚本中断。这是这个库里唯一一个「把人从失控里捞出来」的机制。我见过太多脚本为了跑得顺把FAILSAFE关掉结果一个死循环点在了删除按钮上——血泪经验交付给别人用的脚本第一行必须是把它打开并且启动前留 2 到 3 秒的缓冲。第二个全局参数是PAUSE默认 0.1 秒含义是每次 pyautogui 调用之后强制停顿这么久。它看起来像是性能杀手实际上是对老式桌面程序的保护——很多录入系统在你连续快速输入时会出现丢键或焦点跳走PAUSE 0.1到0.2之间往往能把这类玄学问题压下去。循环特别紧的批量操作我宁愿把停顿加在业务逻辑的关键节点上也不建议直接把PAUSE设成 0。import time import pyautogui as pag pag.FAILSAFE True # 鼠标甩到左上角即可中断 pag.PAUSE 0.15 # 每次调用后停顿给目标程序留响应时间 time.sleep(2) # 启动缓冲留出时间切窗口、挪鼠标 print(开始执行鼠标移到左上角可中断)这段三行配置看起来平平无奇但包含了三个默认值的取舍。FAILSAFE True是安全阀PAUSE 0.15是对目标程序的礼貌time.sleep(2)是你自己的反应窗口。三个一起用脚本的可中断性就有了。3.2 移动、点击、拖拽、滚轮的最小可用集鼠标这一层常用的就五个函数。下面这段是可抄的骨架覆盖了移动、点击、拖拽和滚轮。import pyautogui as pag # 1. 带缓动的绝对移动duration 是总时长不是每像素耗时 pag.moveTo(400, 300, duration0.4, tweenpag.easeInOutQuad) # 2. 双击clicks 次数interval 是两次点击之间的间隔 pag.click(buttonleft, clicks2, interval0.12) # 3. 相对移动从当前位置往右 120 像素 pag.moveRel(120, 0, duration0.2) # 4. 拖拽按住左键拖到目标点松开 pag.dragTo(800, 600, duration0.6, buttonleft) # 5. 滚轮正数向上负数向下 pag.scroll(-400)先把参数说清楚。duration是这次移动的总耗时duration0.4就是 0.4 秒走完全程跟距离无关所以近距离慢速移动不适合用大 duration反而会显得黏。tween接受 pytweening 里的缓动函数默认是线性的linear换成easeInOutQuad后在人机交互上更像真人有些带行为检测的界面会对瞬间瞬移的鼠标做拦截缓动是个低成本的规避手段。click的interval是多次点击之间的间隔秒数双击场景下0.1到0.15之间比较稳太快会被系统判成两次单击而不是双击。dragTo的button指定拖拽时按住的键从文件管理器拖文件到应用窗口这种操作duration太小会导致目标程序还没反应过来你已经在终点松手了一般0.5以上比较保险。scroll的单位在不同系统上不完全一致Windows 下正负值的语义是「向上 / 向下」但滚动格数会受系统「一次滚动多少行」设置影响。做一个手感上的稳妥做法需要精确滚动到某位置时不要靠 scroll 累积改成点滚动条或者用 Home/End 键会比数格子可靠得多。3.3 多显示器和 DPI 缩放怎么算单屏、100% 缩放的环境下坐标是所见即所得。一旦接上副屏或者系统缩放不是 100%麻烦就来了。我的一般流程是先做一次「一致性对照」把pag.size()的输出和pag.screenshot().size的输出并排看两者一致说明坐标体系和截图体系是同一套不一致就说明系统缩放正在中间搞事这时候所有像素坐标都要打折扣看待。验证代码很短跑一次就知道import pyautogui as pag w, h pag.size() sw, sh pag.screenshot().size print(逻辑尺寸:, (w, h), 截图尺寸:, (sw, sh)) print(是否一致:, (w, h) (sw, sh))如果输出是「逻辑尺寸 (1536, 864)截图尺寸 (1920, 1080)」那就是典型的 125% 缩放截图按物理像素来坐标按逻辑像素来你从截图里量出来的坐标点过去会偏。处理方式有三种按侵入性从低到高排一是把系统缩放临时调回 100% 再跑脚本二是在程序启动时声明 DPI 感知让进程拿到物理像素Windows 上用 ctypes 调SetProcessDpiAwareness三是所有坐标统一走乘法换算代价是每个坐标点都要乘一个系数容易漏。多显示器还有一层pag.size()只返回主屏。副屏在主屏左侧时副屏上的窗口坐标是负数。要覆盖全部屏幕的截图得先按前面自检里那段GetSystemMetrics拿到虚拟桌面边界再把这个边界换算成region传进去。实测下来副屏场景我倾向于「先把目标窗口拖回主屏再操作」这不是偷懒——窗口在副屏、脚本在主屏算坐标是这类脚本最常见的错位来源。4. 键盘输入与图像定位typewrite 的边界和 confidence 的调法4.1 typewrite 只吃单字符中文必须走剪贴板typewrite的实现方式是按字符查一张键盘键位表逐个模拟按下和抬起。这张表里只有 ASCII 的单字符和功能键名enter、tab、esc、f1这类所以传入中文时轻则抛KeyError重则什么都没发生、光标停在那儿不动让人以为程序卡死了。中文场景的正确打开方式是把文字塞进剪贴板再发一次ctrlv。这里有个细节很多人不做粘贴完要把原剪贴板内容还回去否则用户跑一次脚本剪贴板里常年躺着一段莫名其妙的文本。import time import pyperclip import pyautogui as pag def type_text(text: str, restore: bool True): 支持中文的输入走剪贴板粘贴完事还原原剪贴板 old pyperclip.paste() if restore else pyperclip.copy(text) time.sleep(0.1) # 等剪贴板写入生效Linux 上尤其需要 pag.hotkey(ctrl, v) # macOS 换成 command, v time.sleep(0.1) if restore: pyperclip.copy(old) type_text(订单编号 A-2024-0001) pag.typewrite(ABC123, interval0.03) # 纯 ASCII 可以直接打interval是每个字符之间的间隔秒数0.03是比较自然的打字速度设 0 会让某些前端框架的输入监听漏掉字符。两个time.sleep(0.1)不是废话第一个是等剪贴板真正写入X11 下的剪贴板是异步的写太快粘贴到的是旧内容第二个是等目标程序完成粘贴。hotkey用来按组合键按下顺序即参数顺序hotkey(ctrl, a)就是全选。需要按住某个键再按别的键时用keyDown/keyUp显式管理比如按住 shift 点选一段文本pag.keyDown(shift) pag.press(home) pag.keyUp(shift)注意keyDown之后如果中途抛异常那个键会一直处于按下状态后续所有操作都是带修饰键的。养成习惯把keyDown/keyUp包在try/finally里。4.2 locateOnScreen 的参数组合决定了成败图像定位是这类脚本的核心截一小块目标按钮存成 png运行时去屏幕上找它找到就点。函数是locateOnScreen返回一个Box可以从left、top、width、height取也可以按元组下标取找不到返回None。所有参数里真正影响命中率的是四个。参数作用推荐取值与说明confidence匹配相似度阈值0.8~0.9 起步需要 opencv太高容易漏太低容易误点grayscale转灰度再匹配设 True 能提速且对颜色主题变化更宽容region限定搜索区域 (x, y, w, h)强烈建议给全屏搜索每次要几百毫秒image模板图路径必须和运行环境同分辨率、同缩放截取置信度这个参数没有万能值。我的做法是先开confidence0.85跑十次记录命中次数漏了就往下调到 0.8误点就往上调到 0.92。真正决定成败的往往不是阈值而是模板图的质量——模板图里有动态内容时间、用户名、未读数角标时任何阈值都救不了。截图时把鼠标移开避开悬停高亮模板图四周留 4 到 8 像素的纯背景命中率会明显好于紧贴边缘裁剪。import pyautogui as pag box pag.locateOnScreen( btn_submit.png, confidence0.85, grayscaleTrue, region(0, 0, 1280, 800), # 只在左上这块区域找省一半以上时间 ) if box is not None: x, y pag.center(box) # Box 的中心点 pag.click(x, y) else: print(没找到按钮)region给的是绝对屏幕坐标下的矩形宽高别超过屏幕否则部分平台会直接抛异常而不是返回 None。pag.center()是官方提供的便捷函数比手工left width // 2更不容易算错。判断是否存在时用is not None而不是if box:因为Box是命名元组某些边界情况下布尔求值的行为容易让人误判。还有一个容易被忽略的函数是pixelMatchesColor(x, y, (r, g, b), tolerance20)。判断「某个状态灯是不是变绿了」这类场景它比图像匹配快一个数量级也不需要维护模板图。缺点是它对坐标极度敏感一旦界面布局变了就失效所以只适合「界面固定、只判颜色」的窄场景。4.3 把「找到就点」封装成可重试的函数单次locateOnScreen在界面有加载动画的场景下几乎必然失败。业务代码里直接调它等于把「加载慢」这个常态问题交给运气。我一般会封装一层轮询同时把日志打进去出问题能知道是「一直没出现」还是「出现了但点错了位置」。import time import logging import pyautogui as pag log logging.getLogger(auto) def wait_and_click(image, timeout20.0, interval0.5, confidence0.85, regionNone, offset(0, 0)): 轮询找图并点击返回是否成功。 offset: 在中心点基础上的偏移用于点按钮旁边的小箭头 deadline time.time() timeout attempt 0 while time.time() deadline: attempt 1 box pag.locateOnScreen(image, confidenceconfidence, grayscaleTrue, regionregion) if box is not None: x, y pag.center(box) pag.click(x offset[0], y offset[1]) log.info(%s 第 %d 次命中点击 (%d, %d), image, attempt, x, y) return True time.sleep(interval) log.warning(%s 在 %.1fs 内未出现尝试 %d 次, image, timeout, attempt) return Falsetimeout是按业务场景给的本机应用内部跳转5秒足够跨网络请求后的界面刷新给20到30秒。interval不建议低于0.3每次locateOnScreen本身就要几十到几百毫秒密集轮询只是空转 CPU。offset这个参数是为了应对「主按钮和下拉箭头在同一个图里」的情况省得为一个小箭头单独截模板图。5. 排查清单五条踩过的坑和它们的解法5.1 环境与权限相关的三条坑一脚本跑飞了鼠标满屏乱点停不下来。现象是脚本进入循环后不受控按 CtrlC 也没用因为键盘事件被脚本自己抢走了。原因通常有两个FAILSAFE被显式关掉了或者鼠标虽然甩到了左上角但当前执行的是一段纯time.sleep的阻塞代码还没轮到 pyautogui 检查。解法是把FAILSAFE保持为 True并且在每个耗时超过 2 秒的等待拆成小段 sleep中间插入一次pag.position()之类的轻量调用给它检查角落的机会。实在要强杀从任务管理器结束 Python 进程是最快的后悔药。坑二macOS 上截图全黑点击坐标正确但没反应。现象是locateOnScreen永远返回 None或者screenshot()出来是一张纯黑图。原因是新版 macOS 对屏幕录制和辅助功能做了授权隔离终端或 IDE 没有拿到对应权限。解法是到系统设置的隐私与安全性里把「辅助功能」和「屏幕录制」两个开关都给运行脚本的那个程序打开然后完全退出终端再重开——只关窗口不算权限是进程级的。这一条在换机器部署时几乎必踩建议写进部署检查单第一条。坑三Linux 上 import 就报 DISPLAY 相关错误。现象是KeyError: DISPLAY或者 Xlib 的连接错误脚本连 import 都过不去。原因是没有图形会话或者当前是 Wayland 会话而 pyautogui 这一套依赖 X11。解法有两个方向一是切到 X11 会话登录后再跑二是在无头环境里起一个虚拟显示Xvfb把 DISPLAY 指过去代价是这种方式下没有真实窗口只能跑纯计算逻辑或者配合浏览器无头模式。判断当前会话类型echo $XDG_SESSION_TYPE一行命令足够。5.2 坐标、输入与性能相关的两条坑四Windows 上模板图肉眼可见脚本死活找不到。现象是手动截图能看到按钮脚本跑起来locateOnScreen返回 None把 confidence 降到 0.6 也不行。原因八成是 DPI 缩放模板图是在 100% 缩放下截的运行环境是 125% 或 150%同一个按钮的像素尺寸变了相似度直接掉到阈值以下。解法是先跑第 3 章那段「逻辑尺寸 vs 截图尺寸」的对照代码确认然后要么统一缩放要么在程序启动时声明 DPI 感知要么干脆在目标机器上重截模板图——最后这条看着土但在交付场景里是最省事的。坑五循环跑几百次后越来越慢内存一路涨。现象是脚本前几十次正常跑上一段时间单次耗时从 200 毫秒涨到 2 秒任务管理器里内存曲线持续上扬。原因是每次screenshot()都创建一个完整的 PIL Image 全屏对象高分辨率下单个对象就有几 MB循环里的临时引用不及时释放GC 压力越来越大。解法有三层首选给locateOnScreen加region把搜索范围从全屏缩到目标区域耗时会掉一个数量级其次加grayscaleTrue灰度匹配比彩色匹配快最后在循环体末尾显式del掉图像引用必要时手动gc.collect()。如果这些还不够就该考虑换成更轻的截图方案或者干脆用坐标点而不是图像匹配——模板图匹配本质上是在拿 CPU 换适应性。6. 把它做稳重试、日志与打包的三处加固脚本能跑和能交付是两件事。我最后会在三个地方加固代价都不大但能省掉大量「用户说不好用」的来回。第一处是重试。UI 自动化里的失败绝大多数是暂时的窗口没来得及弹出、动画没播完、网络慢了半拍。给关键动作套一层重试装饰器比在每处调用的地方写try/except干净得多。import time import logging import functools log logging.getLogger(auto) def retry(times3, delay1.5, exceptions(Exception,)): def deco(fn): functools.wraps(fn) def wrapper(*args, **kwargs): last None for i in range(1, times 1): try: return fn(*args, **kwargs) except exceptions as e: last e log.warning(%s 第 %d 次失败: %s, fn.__name__, i, e) time.sleep(delay) raise last return wrapper return deco retry(times3, delay1.5) def open_order_page(order_id): wait_and_click(menu_order.png, timeout10) wait_and_click(search_box.png, timeout5) type_text(order_id) wait_and_click(btn_query.png, timeout8)times控制在 3 次以内多了只会延长故障暴露的时间delay给 1.5 秒是因为大多数界面卡顿都在这个量级内。装饰器的好处是被重试的函数只要抛异常就行不用关心返回值约定。第二处是日志。别用print用logging落到文件格式里带上时间戳和函数名。UI 自动化的故障现场转瞬即逝事后能不能还原全看你当时记了什么。第三处是打包用 PyInstaller 生成 exe 时--noconsole会让sys.stdout变成 None某些库在里面打印时会抛AttributeError: NoneType object has no attribute write表现是双击闪退。稳妥做法是先用--console打出稳定版本确认没问题再加-w或者在入口处把sys.stdout/sys.stderr重定向到一个空对象。从那以后我每接一台新机器都先跑一遍第 2 章那段自检确认分辨率、缩放、截图和 opencv 四条通路都通了再谈业务逻辑——这个习惯帮我省掉的排查时间比写业务代码的时间还多。希望帮到你。本文还有配套的精品资源点击获取