简介: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 | 窗口枚举、激活、按标题匹配 | 窗口相关函数不可用(随平台变化) |
| pymsgbox | alert / confirm / prompt 弹框 | 调用弹框时抛 ImportError |
| pytweening | moveTo 的 tween 缓动曲线 | tween 参数被忽略 |
| mouseinfo | 独立小工具,显示鼠标坐标和颜色 | 不影响主库运行 |
| opencv-python | confidence 图像匹配的实现 | 传 confidence 参数会报错 |
| pyperclip | 剪贴板读写 | 不是强制依赖,但中文输入基本离不开它 |
前六个是安装时自动带的,后两个要自己补。我一般会额外装 opencv,因为locateOnScreen的confidence参数在默认的像素级匹配下是没有的——不装 opencv,你只能要求模板图 100% 一致,换个主题、换台显示器就找不到。
提示:检查是否真的装上了,别只看
pip list。有些机器上有多个 Python,pip指向的和脚本运行的解释器不是同一个,这种情况在 Windows 上尤其常见。
2.2 三条安装路径,按环境选
先给出三条路径,从最省事到最可控排列。命令行里的版本号一定要写死,写成PyAutoGUI==0.9.26,不要写pyautogui,否则装到的可能是另一个版本,行为差异会让你怀疑人生。
# 路径一:能联网,锁定版本直接装(最省事) python -m pip install "PyAutoGUI==0.9.26" # 路径二:内网或离线机器,先在联网机器把 wheel 拉全,再整体拷过去 python -m pip download "PyAutoGUI==0.9.26" -d ./wheels python -m pip install --no-index --find-links=./wheels "PyAutoGUI==0.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(width=1920, height=1080) 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 坐标系和那个能救命的 FAILSAFE
PyAutoGUI 的坐标体系是屏幕绝对坐标,原点在左上角,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, duration=0.4, tween=pag.easeInOutQuad) # 2. 双击:clicks 次数,interval 是两次点击之间的间隔 pag.click(button="left", clicks=2, interval=0.12) # 3. 相对移动,从当前位置往右 120 像素 pag.moveRel(120, 0, duration=0.2) # 4. 拖拽:按住左键拖到目标点,松开 pag.dragTo(800, 600, duration=0.6, button="left") # 5. 滚轮:正数向上,负数向下 pag.scroll(-400)先把参数说清楚。duration是这次移动的总耗时,duration=0.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,重则什么都没发生、光标停在那儿不动,让人以为程序卡死了。
中文场景的正确打开方式是把文字塞进剪贴板,再发一次ctrl+v。这里有个细节很多人不做:粘贴完要把原剪贴板内容还回去,否则用户跑一次脚本,剪贴板里常年躺着一段莫名其妙的文本。
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", interval=0.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 | 模板图路径 | 必须和运行环境同分辨率、同缩放截取 |
置信度这个参数没有万能值。我的做法是先开confidence=0.85跑十次,记录命中次数;漏了就往下调到 0.8,误点就往上调到 0.92。真正决定成败的往往不是阈值,而是模板图的质量——模板图里有动态内容(时间、用户名、未读数角标)时,任何阈值都救不了。截图时把鼠标移开,避开悬停高亮,模板图四周留 4 到 8 像素的纯背景,命中率会明显好于紧贴边缘裁剪。
import pyautogui as pag box = pag.locateOnScreen( "btn_submit.png", confidence=0.85, grayscale=True, 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), tolerance=20)。判断「某个状态灯是不是变绿了」这类场景,它比图像匹配快一个数量级,也不需要维护模板图。缺点是它对坐标极度敏感,一旦界面布局变了就失效,所以只适合「界面固定、只判颜色」的窄场景。
4.3 把「找到就点」封装成可重试的函数
单次locateOnScreen在界面有加载动画的场景下几乎必然失败。业务代码里直接调它,等于把「加载慢」这个常态问题交给运气。我一般会封装一层轮询,同时把日志打进去,出问题能知道是「一直没出现」还是「出现了但点错了位置」。
import time import logging import pyautogui as pag log = logging.getLogger("auto") def wait_and_click(image, timeout=20.0, interval=0.5, confidence=0.85, region=None, offset=(0, 0)): """轮询找图并点击,返回是否成功。 offset: 在中心点基础上的偏移,用于点按钮旁边的小箭头 """ deadline = time.time() + timeout attempt = 0 while time.time() < deadline: attempt += 1 box = pag.locateOnScreen(image, confidence=confidence, grayscale=True, region=region) 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 环境与权限相关的三条
坑一:脚本跑飞了,鼠标满屏乱点停不下来。
现象是脚本进入循环后不受控,按 Ctrl+C 也没用,因为键盘事件被脚本自己抢走了。原因通常有两个: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,把搜索范围从全屏缩到目标区域,耗时会掉一个数量级;其次加grayscale=True,灰度匹配比彩色匹配快;最后在循环体末尾显式del掉图像引用,必要时手动gc.collect()。如果这些还不够,就该考虑换成更轻的截图方案,或者干脆用坐标点而不是图像匹配——模板图匹配本质上是在拿 CPU 换适应性。
6. 把它做稳:重试、日志与打包的三处加固
脚本能跑和能交付是两件事。我最后会在三个地方加固,代价都不大,但能省掉大量「用户说不好用」的来回。
第一处是重试。UI 自动化里的失败绝大多数是暂时的:窗口没来得及弹出、动画没播完、网络慢了半拍。给关键动作套一层重试装饰器,比在每处调用的地方写try/except干净得多。
import time import logging import functools log = logging.getLogger("auto") def retry(times=3, delay=1.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(times=3, delay=1.5) def open_order_page(order_id): wait_and_click("menu_order.png", timeout=10) wait_and_click("search_box.png", timeout=5) type_text(order_id) wait_and_click("btn_query.png", timeout=8)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 四条通路都通了,再谈业务逻辑——这个习惯帮我省掉的排查时间,比写业务代码的时间还多。希望帮到你。
本文还有配套的精品资源,点击获取