Python启动Windows程序的五种方法及窗口自动化实战 📅 发布时间:2026/9/13 10:04:06 👁 浏览次数: 1. 整体思路打开Windows程序的5条路每条路的定位都不一样先交代下我自己的背景从2014年就开始拿Python写各种自动化脚本从最早的网页爬虫、桌面文件批量处理到后来专门做Windows桌面软件的自动化测试。这几年下来单是怎么用Python把一个.exe文件跑起来这个问题就被不同同事问过不下几十次。很多人觉得这还不简单不就是双击一下或者敲个命令吗但对写代码的人来讲这里面的讲究其实挺多的。先说一个很多人容易忽略的事实Python打开Windows可执行程序这件事不是一条路走到黑的不同场景要选不同的方法。比如你只是想临时跑一下某个命令行工具跟你要启动一个带界面的软件然后自动操作它的窗口用到的方案完全是两码事。而如果你是想走Windows窗口自动化这条路那能把程序启动起来只是第一步后续你还要拿到进程的控制权、等待窗口出现、甚至向窗口发送消息所以这一步选错方向后面会很别扭。回到标题里提到的几个方法市面上能查到的主流方案有这几种os.system()最古老、最粗暴适合不求返回值、不求控制的临时调用os.startfile()Windows系统才有的方法模拟的是资源管理器里双击文件的行为subprocess.run()Python 3.5之后最常用的拿来主义方案适合需要等待程序执行完、拿返回值的场景subprocess.Popen()自动化爱好者真正的老朋友它不会傻等程序退出你可以启动完程序之后该干嘛干嘛ctypes调用Windows API的ShellExecuteW属于抄近路方案能实现很多现成接口做不到的细节比如以管理员权限运行后面我会把每一种方法都拆开来讲配合实际代码和适用场景。但先说一个总的原则如果你打算做Windows窗口自动化请直接无条件优先考虑subprocess.Popen()其它方法了解一下原理、知道什么时候应急就够了。原因我后面会在3.2小节里详细解释这里先卖个关子。2. 五种主流打开方法详细拆解2.1 os.system()最老派的调用方式能用但别依赖先看这段代码import os os.system(notepad.exe)没错就是这么简单。os.system()的实现原理本质上是在当前Python进程下面又开了一个子shell然后把字符串当作命令丢给这个shell去执行。Python主程序会在这里卡住等notepad关闭之后才会执行后面的代码。它有以下几个明显的短板拿不到程序的标准输出。如果目标程序是控制台程序打印了一堆内容Python这边根本接收不到这些输出直接糊在终端里或者直接丢了。拿不到稳定的返回值。虽然os.system()会返回一个整数但这个整数在不同操作系统上的含义不一样Windows平台下返回的是进程的退出码但如果是通过shell间接启动的这个值经常被包装过一层用起来很别扭。存在安全问题。字符串命令是原样丢给shell解析的如果你的命令里拼接了外部传入的内容等于给了shell注入的机会。我对这个方法的定位是临时在脚本里顺手启动点什么纯属应急手段。比如你写了个脚本处理完一批文件之后想顺手打开记事本让用户核对一下那用os.system()完全没问题。但凡涉及到参数、返回值、环境别用它。2.2 os.startfile()Windows专属模仿双击行为的正解os.startfile()是Windows平台独有的函数Python的os模块在Windows系统下才会提供它。它的行为非常特殊它模拟的是你在资源管理器里双击文件这个动作。所以你传给它什么系统就用对应的默认程序把它打开。典型用法import os # 打开记事本 os.startfile(notepad.exe) # 打开一个txt文件会用默认编辑器打开 os.startfile(D:\\temp\\report.txt) # 打开一个网址会用默认浏览器打开 os.startfile(https://www.example.com) # 打开一个目录会在资源管理器里弹出这个文件夹 os.startfile(D:\\temp)看到没有这个东西的用途其实相当广泛。特别是对于打开文件、目录、URL这种场景os.startfile()比其它方法都顺手因为它天然就带上了系统文件关联这个逻辑。你不需要手动指定用什么程序打开系统自己会判断。另外它还有一个隐藏功能os.startfile()在部分Python版本里是支持返回参数operation的就是文件操作方式比如常见的open、edit、explore、find等。不过说实话实际开发中用到operation的场景极少我用了一年多也就遇到过一两次用explore的需求。需要注意的一点os.startfile()在新版Python中建议从os模块直接导入老代码里有从os import startfile这种写法倒不是说不行只是看起来不够规范。另外os.startfile()同样拿不到子进程的任何输出和返回值因为从系统的角度来看那个程序已经不是你的子进程了它飘在外面自由活动。在自动化场景中os.startfile()的价值在于某些特别顽固的程序你用subprocess.Popen()启动时它总有点水土不服但用os.startfile()像真人双击一样去启动反而就好使了。我遇到过个别加壳的国产软件就是这样后面讲排查技巧的时候会再提到。2.3 subprocess.run()标准答案适合启动后等结果Python官方文档其实早就推荐用subprocess模块替代os.system()了。在subprocess模块里subprocess.run()是对新手最友好、代码量最少的封装方法。看一下最常见的使用方式import subprocess # 启动记事本但waitTrue默认会等notepad关闭后继续执行 result subprocess.run([notepad.exe]) print(返回值:, result.returncode)这里有个非常关键的点需要记住subprocess.run()默认是会阻塞等待子进程结束的。什么叫阻塞等待就是你执行这一行代码Python程序就停在这一行不往下走了直到记事本被你关掉subprocess.run()才会返回一个CompletedProcess对象然后你才能拿到returncode。这在某些场景下其实是优点你想等一个安装程序跑完再去检查安装结果你想运行一个命令行工具需要拿到它的最终退出码你想执行一个批处理等它全部执行完再继续但如果你要做窗口自动化这个特性就成了致命缺点。你想启动一个软件然后马上自动化去操作它的窗口结果subprocess.run()卡在这里后面的自动操作代码永远执行不到。那不是把路堵死了吗所以记住一条使用标尺只要你的程序启动完之后还需要做别的事情就别用subprocess.run()。尤其是窗口自动化这一条直接划重点。subprocess.run()还有一点值得说的是capture_outputTrue参数加上它之后可以把子进程的标准输出和标准错误都捕获回来再通过result.stdout和result.stderr来读取。不过要注意Windows下很多程序输出的编码是GBKPython默认的解码方式经常对不上乱码问题后面4.4小节会专门讲。2.4 subprocess.Popen()窗口自动化的正主终于讲到重头戏了。subprocess.Popen()是subprocess模块的底层核心类run()本质上就是Popen()的简化封装。为什么说窗口自动化一定要用Popen()因为它有一个和run()完全不同的行为它是非阻塞的。import subprocess # 启动记事本这一行执行后不会等待代码会立刻继续往下走 p subprocess.Popen([notepad.exe]) # 这一行会立刻打印出来不需要等notepad关闭 print(进程PID:, p.pid)看到区别了吧。用Popen()启动一个程序Python主流程完全不停下来Popen()对象p就代表了那个子进程你可以随时问它还活着吗poll()、退出码是多少returncode、强行杀掉你行不行terminate()/kill()。用Popen()来启动需要做窗口自动化的程序整个过程是这样的subprocess.Popen()启动目标程序拿到进程对象p其中包含了PID进程标识符Python不等待继续执行后续代码用pywinauto或win32gui这类库去查找目标窗口如果窗口还没出现就等一会儿再查后面4.2会说具体怎么做找到窗口之后就能发送菜单命令、填写文本框、点击按钮了这才是打开程序和窗口自动化能够衔接起来的正确姿势。换个角度说如果你的Python脚本一启动程序就死等在那里那窗口自动化就永远走不到找窗口这一步自动化就成了空谈。Popen()还有一堆参数可以精确控制启动行为比如cwd指定子进程的工作目录env指定子进程的环境变量stdin/stdout/stderr重定向输入输出creationflagsWindows下可以用这个传CREATE_NEW_CONSOLE之类的标志后面第三章我会专门拿cwd和env来讲实际案例这两个参数在自动化实战里踩坑率极高。2.5 ctypes调用ShellExecuteW走Windows API后门先声明一下这个方法不算日常主力但属于进阶加餐。为什么会有这个东西因为有时候前面几种方法都搞不定某些程序尤其是需要提权或者特殊方式打开的情况。下面这段代码通过ctypes直接调用Windows的ShellExecuteW函数可以做到很多subprocess做不了的事情import ctypes import sys def shell_execute(path, params, work_dir, show1, operationopen): 调用Windows ShellExecuteW show: 1正常显示窗口, 0隐藏窗口, 3最大化, 6最小化 operation: open/edit/explore/runas等 result ctypes.windll.shell32.ShellExecuteW( None, # 父窗口句柄None表示桌面 operation, path, params, work_dir, show ) # 返回值大于32表示成功 if result 32: raise RuntimeError(fShellExecuteW failed, code{result}) return result # 以管理员权限运行某程序runas操作会触发UAC弹窗 # shell_execute(D:\\software\\setup.exe, operationrunas) # 隐藏窗口方式运行某程序 # shell_execute(C:\\tools\\worker.exe, show0)这里边的门道在于operation参数open正常打开edit用默认编辑器打开explore在资源管理器中打开目录runas以管理员权限运行会触发Windows的UAC用户账户控制弹窗print用默认打印机打印我为什么说它在自动化里有一席之地是因为有些程序在前台有交互窗口你在做无人值守自动化的时候不想让那个窗口弹出来干扰用户可以用show0隐藏窗口来启动。这个能力subprocess.Popen()默认给不了除非你去配合creationflags和STARTF_USESHOWWINDOW一顿操作但代码复杂度一下就上去了。不过还是要多说一句ShellExecuteW拿不到子进程的PID返回的是类似操作是否成功的状态码。所以如果窗口自动化需要精确控制目标进程这个方法只能当一个辅助手段主力依然是Popen()。3. 实操过程与核心环节实现3.1 启动参数、工作目录、环境变量一步到位说完了五种方法接下来把这些方法放到真实的自动化场景里来打磨一下。假设你现在要自动化启动一个程序比如一个内部开发的ERP客户端ErpClient.exe它正常情况下是双击启动的但你用Python启动它的时候经常出问题。先别急着写代码问问自己三个问题第一个问题它要不要带参数大部分专业软件是支持启动参数的。比如很多烧录工具支持指定配置文件路径很多测试工具支持指定测试用例编号。传参数用subprocess列表形式就行千万不要拼成一个字符串import subprocess # 正确参数分开写 p subprocess.Popen([ D:\\apps\\ErpClient\\ErpClient.exe, --config, D:\\config\\erp_test.ini, --mode, auto ]) # 错误容易在路径带空格时炸掉 # p subprocess.Popen(D:\\apps\\ErpClient\\ErpClient.exe --config D:\\config\\erp_test.ini --mode auto)为什么推荐分开写Windows的路径大量包含空格比如C:\Program Files\...如果把整条命令当一个字符串传给Popen()它会拿这个完整的字符串去找可执行文件十有八九报错。列表形式的好处是Python会帮你处理好引号问题空格不会被错误拆分。第二个问题它的工作目录在哪这是一个踩坑率极高的参数。很多Windows程序看起来是绿色版双击能正常运行但当你用代码启动时程序疯狂报错找不到配置文件或者找不到它依赖的DLL。为什么因为很多程序的逻辑是相对路径它默认会在当前工作目录下寻找自己需要的资源。用双击方式启动时当前工作目录是程序所在目录没问题。但当你用Python启动时如果没指定cwd当前工作目录就是你的Python脚本所在的目录甚至是你命令行所在的路径程序当然找不到东西了。解决办法很简单用subprocess.Popen()的cwd参数import subprocess exe_dir D:\\apps\\ErpClient p subprocess.Popen( [ErpClient.exe], cwdexe_dir, # 指定程序的工作目录等于模拟在那里双击 )所以只要是启动一个需要加载自身目录下资源的程序记得把cwd设置成它所在的目录。这一条我几乎每次做自动化都要强调一遍。第三个问题它要不要特殊的环境变量某些程序依赖特定的系统环境变量比如JAVA_HOME、PATH里有没有某个工具的路径。Python启动子进程时子进程默认会继承父进程也就是Python这个进程的环境变量。如果你在命令行里手动启动没问题但Python脚本启动就有问题多半是你脚本运行的环境比如在某个IDE里跑的跟命令行环境不是同一套。要自定义环境变量也很简单import os import subprocess # 继承当前环境变量的基础上追加自定义项 env os.environ.copy() env[ERPCONFIG] D:\\config\\erp_test.ini env[PATH] rD:\\tools\\ffmpeg\\bin; env[PATH] p subprocess.Popen( [ErpClient.exe], cwdrD:\\apps\\ErpClient, envenv )注意这里一定要先os.environ.copy()再改因为不能直接拿os.environ改那个会影响到Python本身的环境。一旦你覆盖了env参数子进程就不会继承你当前的任何环境变量完全按你给的这一份来所以得先把原有内容复制过来再追加。3.2 拿到PID和进程句柄为窗口自动化铺路现在你的程序已经通过Popen()正常启动了接下来要跟窗口自动化对接。这一步的核心是通过PID把进程和窗口关联起来。常见做法是Popen()返回的进程对象里直接有pid属性import subprocess import time import win32gui import win32process # 启动程序 p subprocess.Popen([rD:\\apps\\ErpClient\\ErpClient.exe]) print(目标进程PID:, p.pid) # 轮询等待目标窗口出现最多等30秒 def find_window_by_pid(pid, timeout30): start_time time.time() result None while time.time() - start_time timeout: def enum_callback(hwnd, windows): if win32process.GetWindowThreadProcessId(hwnd)[1] pid: windows.append(hwnd) return True windows [] win32gui.EnumWindows(enum_callback, windows) if windows: # 取第一个匹配的顶层窗口 result windows[0] break time.sleep(0.5) return result hwnd find_window_by_pid(p.pid) if hwnd: print(找到窗口句柄:, hwnd) # 窗口自动化后续操作就可以通过hwnd来发消息、点按钮了 else: print(超时未找到窗口)这里为什么要拿PID而不用进程名因为PID更精确。假设系统里同时开着好几个用Chrome内核的软件进程名叫什么之类的根本分不清但PID是唯一的而且是Popen()直接给你的完全不用猜。这段代码还有一个关键机制轮询等待窗口出现。程序启动到窗口显示出来通常需要几百毫秒到几秒期间你要反复去查询而不是死等固定秒数。固定sleep(5)这种写法的坏处是机器快的时候白等好几秒机器慢的时候5秒窗口还没弹出来代码照样挂了。轮询则可以说是既高效又稳妥。在写窗口自动化脚本时我一直建议把这个find_window_by_pid函数当成基建沉淀下来几乎所有自动化脚本都能复用。拿到窗口句柄hwnd之后后面无论是用pywinauto直接驱动还是用win32gui.SendMessage手动发消息都游刃有余了。3.3 用os.startfile()做真人双击的特殊场景从方法上讲os.startfile()似乎已经算不上自动化的正路子但它在某些特殊场景下的表现真的让我印象深刻。大约两年前我帮一个客户做老软件的自动化回归测试。有个古董级医疗系统数据录入界面是Delphi写的通过subprocess.Popen()启动后窗口半天出不来或者出来之后状态栏一直显示初始化中但只要人工双击它一切正常。后来排查发现这个软件启动时那个exe会先检查自己是不是由资源管理器直接发起的换句话说它做了一堆跟UI环境相关的初始化。用subprocess.Popen()启动时它父进程是Python初始化路径走不通而用os.startfile()启动时虽然调用者是Python但Windows底层走的是ShellExecute路径行为上跟资源管理器双击极度接近软件就能正常进入主界面。我记得当时改了三行代码import os exe_path rD:\\legacy\\HIS\\HisMain.exe os.startfile(exe_path)就这么简单。程序正常启动了自动化链条也走通了。所以我在给团队培训时总会强调先掌握Popen()但在哪里都排查不出问题的时候不妨想想os.startfile()这个双击模拟器。它处理不了进程控制但在启动兼容性上是出了名的好使。3.4 完整窗口自动化流程串讲把前面这些碎片拼起来给你看一个完整的最小闭环。假设我们要自动化启动计算器Windows自带的calc.exe然后等它窗口出现最后把它最小化import subprocess import time import win32gui import win32con # Step 1: 启动程序 p subprocess.Popen([calc.exe]) print([1] 进程已启动PID:, p.pid) # Step 2: 轮询等待窗口 hwnd None for _ in range(60): # 最多等30秒 def enum_proc(hwnd, result): if win32gui.IsWindowVisible(hwnd): # 简单校验下窗口标题避免抓到别的窗口 title win32gui.GetWindowText(hwnd) if 计算器 in title or Calculator in title: result.append(hwnd) return True matches [] win32gui.EnumWindows(enum_proc, matches) if matches: hwnd matches[0] break time.sleep(0.5) if hwnd is None: raise RuntimeError(30秒内没有找到计算器窗口) print([2] 窗口已出现句柄:, hwnd) # Step 3: 对窗口做操作这里演示最小化 win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) print([3] 窗口已最小化) # Step 4: 测试结束礼貌关闭程序 win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) print([4] 已发送关闭消息)这个脚本已经是一个标准的自动化雏形了。流程就是启动 → 等窗口 → 操作窗口 → 关闭。你可以在Step 3那里换成任何你想做的操作比如点击菜单、输入文本、点击按钮。这就是标题里说的Windows窗口自动化第一步的意义——第一步就是先把程序用正确的方式启动起来后面的窗口操作全都要建立在这个基础上。4. 常见问题与排查技巧实录4.1 路径带空格导致FileNotFoundError很多新手在Windows上写自动化遇到第一个报错就是这个。你写subprocess.Popen(C:\Program Files\SomeApp\app.exe)然后抛出一个FileNotFoundError一脸懵逼。原因上面提过路径里有空格而且你没用列表形式传参。字符串会被当成一个完整路径去找文件而C:\Program这个路径根本不存在。排查的办法很简单import subprocess exe_path rC:\Program Files\SomeApp\app.exe p subprocess.Popen([exe_path]) # 用列表包起来顺便说一句写Windows路径时尽量用原始字符串前面加r或者把反斜杠换成双反斜杠或者用正斜杠。否则\t、\n这些转义符会冷不丁跳出来恶心你一下。4.2 程序一闪而过窗口根本没停留控制台程序尤其常见。你启动它一瞬间一个黑色窗口闪现就没了什么也看不清。这种情况通常是程序运行出错但它退出得太快没有留给你观察的机会。出现这个现象有两个要注意的点第一确认你的程序是不是subprocess.run()启动的。如果是那是正常现象程序执行完就退了窗口自然就关了。想看输出记得加capture_outputTrue然后把stdout和stderr打印出来。第二如果用的是Popen()启动并且窗口真的秒退那大概率是程序本身启动时参数不对或者运行环境缺东西。建议先用命令行手动运行一次比方说打开cmd切到程序目录直接敲app.exe --param看看报什么错再针对性处理。窗口自动化场景下如果主窗口一直没出现不要硬等用前面那个轮询函数配合超时超时之后就主动把进程杀掉打印日志避免脚本卡死。4.3 需要管理员权限才能运行的程序Windows的UAC机制让很多自动化脚本师头疼。有些程序右键以管理员身份运行才能正常工作但你用subprocess.Popen()启动它时UAC弹窗直接拦路而你的脚本可能跑在无人值守环境里根本没人点是。有几种思路一是用前面讲的ShellExecuteW的runas操作它会触发UAC弹窗需要有人点确定ctypes.windll.shell32.ShellExecuteW(None, runas, exe_path, , , 1)二是把整个Python脚本本身以管理员权限运行。也就是说你右键以管理员身份运行你的脚本那Python启动的子进程也会继承管理员权限自然就不会再触发UAC弹窗了。还有一种更偏向生产环境的做法写一个计划任务在任务计划程序里勾选使用最高权限运行让计划任务去启动这个程序。这种方式不需要交互适合定时自动化任务。4.4 子进程输出乱码中文全变问号Windows中文版的控制台输出默认是GBK编码而Python 3的字符串用的是Unicode两者不对齐就会出现乱码。你用subprocess.run(..., capture_outputTrue)拿到stdout之后直接打印大概率一团乱码或者全是问号。处理方案也很直白拿到原始字节后手动解码import subprocess result subprocess.run( [ping, 127.0.0.1], capture_outputTrue ) # 优先用GBK解码解码失败再退回UTF-8 try: text result.stdout.decode(gbk) except UnicodeDecodeError: text result.stdout.decode(utf-8, errorsignore) print(text)最近两三年的新版Python环境没有以前那么频繁遇到但老程序仍然是重灾区。我的习惯是封装一个小函数smart_decode(bytes_data)统一处理stdout和stderr的解码避免每次都要写try-except。4.5 启动的进程一直在后台看不到窗口这类问题分两种情况。一种是无窗口程序就是设计上就没有图形界面跑在后台纯属正常不做处理。另一种是有窗口的程序被某种原因隐藏了或者启动窗口的进程跟你启动的不是同一个。第二种情况最典型的例子是大部分软件启动时会出现一个引导程序或者启动器它检查完更新之后真正的主程序是它另外拉起来的。你抓到的是引导程序的窗口句柄等更新结束引导程序退出主程序窗口才出现。如果继续傻等就会一直找不到正确的窗口。处理方法也比较直接用EnumWindows枚举所有可见窗口遍历它们的进程名目标锁定真正的主程序进程名。不要死等第一次出现的窗口而是不断刷新直到符合条件的窗口出现再操作。这需要你对目标软件的启动流程比较熟知道主窗口的特征类名、标题、进程名。5. 最后的个人经验和习惯做了这么多年Windows自动化我自己已经形成了一套比较固定的启动程序习惯趁这个机会分享给大家。简单说就是能用Popen就不碰run能用PID就不碰进程名能轮询就不死等能传列表就不拼字符串。这几条说起来简单但每一条都是用一堆失败的夜里换回来的。另外还有一个容易被忽略的小习惯就是启动程序之后一定要记得记录日志。我会把启动时间、PID、命令行参数、cwd、环境变量里改了哪些值全部打印到日志文件里。这样后面出问题的时候排查路径会顺很多。自动化脚本都是跑在无人值守的环境里的你没法在旁边盯着看这时候日志就是你的眼睛。如果你刚接触Windows窗口自动化这篇文章讲到的方法够你消化一阵子了。先把启动程序这一步玩明白后面不管是用pywinauto还是win32gui去操作窗口都会顺手很多。自动化这条路上第一步迈稳了后面都是越走越开阔的。