Python游戏自动化脚本开发:从模拟操作到图像识别的完整实践
1. 项目概述为什么选择Python做游戏自动化如果你玩过一些需要重复“肝”任务的游戏或者想在游戏里实现一些辅助功能但又不想碰那些风险极高的外挂那么用Python写一个后台自动化脚本可能就是最优雅的解决方案。这听起来有点“灰色”但它的核心逻辑其实很纯粹模拟人的操作代替你完成那些枯燥、重复的点击和按键。我之所以选择Python而不是C或者易语言这类更“底层”的工具原因很简单——快。Python的语法足够简洁库生态极其丰富从控制鼠标键盘到识别屏幕图像都有成熟的轮子可用。这意味着你可以把精力集中在自动化逻辑的设计上而不是耗费大量时间去造轮子或者处理复杂的内存读写。这个项目的目标是构建一个运行在你电脑后台的“影子助手”。它不会修改游戏内存数据不注入DLL仅仅是通过系统API模拟输入和读取屏幕信息这在很多游戏的规则界定里处于一个相对模糊但通常被默许的“宏”或“辅助工具”范畴。当然具体是否违规一定要仔细阅读你所玩游戏的用户协议。从技术上说我们将打造一个脚本它能自动登录游戏、完成日常任务、循环刷副本、甚至基于简单的图像识别来应对一些动态场景。整个过程就像有一个不知疲倦的伙伴在帮你操作而你可以安心地去处理其他事情。2. 核心思路与技术选型模拟与识别的艺术游戏自动化的核心无外乎两点“输入”和“感知”。“输入”是向游戏发送指令比如鼠标点击、键盘按键“感知”是获取游戏的状态比如判断某个按钮是否出现、血条是否健康。基于这两点我们的技术栈就清晰了。2.1 输入模拟pyautogui与pynput的抉择对于输入模拟pyautogui是绝大多数人的首选。它功能全面一句pyautogui.click(x, y)就能完成点击pyautogui.write(hello)就能打字对新手极其友好。但它有一个致命缺点它是“阻塞式”的。一旦你调用了pyautogui.moveTo()你的鼠标指针就会真的、以肉眼可见的速度移动到目标位置在此期间整个脚本会暂停等待。这在需要快速反应或隐秘后台运行的场景下是灾难性的。因此对于需要更精细、更快速、或真正后台化即不干扰你前台正常使用电脑的操作我强烈推荐pynput。pynput允许你直接调用操作系统底层的输入事件可以实现“瞬间移动”和“异步监听”。比如你可以用pynput.mouse.Controller()直接将鼠标光标“跳”到某个坐标没有任何动画过程。更重要的是pynput.keyboard.Listener可以让你监听全局快捷键实现一键启动或暂停脚本而无需把脚本窗口置于前台。实操心得对于简单的、不要求速度的自动化如自动填写表单pyautogui够用且简单。但对于游戏自动化尤其是需要争分夺秒或希望完全后台化的场景pynput是更专业的选择。我自己的项目几乎全部基于pynput。2.2 屏幕感知从坐标到图像的飞跃最基础的感知是坐标定位。你可以写死一个坐标(100, 200)让鼠标去点。但游戏窗口一旦移动脚本就失效了。所以我们需要动态定位。这就引出了图像识别。opencv-python简称cv2是图像处理的工业标准。在游戏自动化中我们主要用它来做模板匹配。原理很简单提前截取一张游戏内小按钮的图片比如“挑战”按钮作为模板。脚本运行时不断截取当前屏幕画面用cv2.matchTemplate函数在屏幕截图中寻找这个模板。找到后函数会返回匹配位置我们就可以计算出这个按钮在当前屏幕上的精确坐标然后让鼠标点击过去。这种方法比坐标硬编码健壮得多。只要游戏UI不变无论窗口在屏幕哪里脚本都能找到它。你还可以通过设置匹配阈值来应对游戏画面轻微的亮度、色差变化。2.3 控制逻辑稳定性的关键有了“手”pynput和“眼”opencv我们还需要一个“大脑”来指挥。这个大脑就是控制逻辑。这里切忌使用死循环加固定延时time.sleep(5)这种简单粗暴的方式。因为网络延迟、电脑性能波动都会导致每个操作步骤的实际耗时不同。一个健壮的控制逻辑应该是“状态驱动”或“事件驱动”的。例如我们不应该写“点击登录按钮然后等待5秒再点击开始游戏”。而应该写“点击登录按钮然后循环检测直到‘开始游戏’按钮出现在屏幕上再点击它如果超过30秒还没出现则报错重试。” 这中间等待的time.sleep应该用短间隔的检查如time.sleep(0.5)来代替这样脚本响应更快也更稳定。3. 环境搭建与核心工具实战工欲善其事必先利其器。一个清晰的开发环境能避免很多后期麻烦。3.1 Python环境与IDE选择我推荐使用 Python 3.8 或以上版本。安装时务必勾选“Add Python to PATH”这是很多新手踩坑的第一步。集成开发环境IDE上VSCode 和 PyCharm 都是优秀的选择。VSCode 更轻量通过安装 Python 插件和 Pylance 插件就能获得很好的代码提示和调试体验。PyCharm 是专业重型武器其调试功能和项目管理更为强大社区版对个人免费完全够用。对于包管理强烈建议使用虚拟环境。在项目根目录下执行python -m venv venv创建一个虚拟环境然后激活它Windows下是venv\Scripts\activate。这样安装的所有库如pynput,opencv都只在这个项目内生效不会污染全局环境也便于后期打包和迁移。3.2 核心库的安装与“避坑指南”打开终端确保已激活虚拟环境安装我们需要的核心库pip install pynput opencv-python pillow numpy这里有几个关键点opencv-python这个包名安装的是 OpenCV 的核心模块。如果你需要一些额外的贡献模块一般游戏自动化不需要可以安装opencv-contrib-python但体积会大很多。PillowPython 图像处理库有时pyautogui或截图功能会依赖它一并安装避免问题。pynput在 macOS 上的权限在 macOS 上运行pynput需要辅助功能权限。你需要在“系统偏好设置” - “安全性与隐私” - “辅助功能”中给你的终端或 IDE 打上勾。否则会报错。pynput在 Linux 上的依赖在 Linux 上可能需要安装python3-xlib等库具体根据发行版使用apt或yum安装。3.3 第一个脚本让鼠标动起来让我们写一个最简单的“Hello World”来验证环境。这个脚本将把鼠标移动到屏幕中央并点击一下。import time from pynput.mouse import Controller, Button # 创建鼠标控制器 mouse Controller() # 获取屏幕尺寸这里需要其他库我们简单写个坐标 # 假设屏幕中心是 (960, 540) screen_center (960, 540) print(3秒后鼠标将移动到屏幕中心并点击...) time.sleep(3) # 移动鼠标pynput是瞬间移动 mouse.position screen_center time.sleep(0.5) # 稍作停顿让用户看到效果 # 点击左键 mouse.click(Button.left, 1) print(操作完成)运行这个脚本你会看到鼠标“唰”地一下跳到屏幕中心并点击。这说明你的输入模拟环境已经就绪了。4. 图像识别核心用OpenCV找到游戏按钮图像识别是自动化脚本的“眼睛”。我们来实现一个完整的流程截屏 - 匹配模板 - 计算坐标 - 点击。4.1 准备模板图片首先在游戏里把你想要点击的按钮截取下来。比如一个“领取奖励”的按钮。用系统截图工具如 Windows 的 Snipping Tool截取确保只包含按钮本身边缘背景越少越好保存为claim_reward.png放在你的项目目录下。4.2 编写图像识别与点击函数import cv2 import numpy as np import pyautogui # 这里仅用于截图操作仍用pynput from pynput.mouse import Controller, Button import time mouse Controller() def find_and_click(template_path, threshold0.8, offset_x0, offset_y0): 在屏幕上查找模板图片如果找到则点击其中心。 :param template_path: 模板图片路径 :param threshold: 匹配阈值0.8表示80%相似度以上认为匹配 :param offset_x: 点击点相对于模板中心的X偏移 :param offset_y: 点击点相对于模板中心的Y偏移 :return: 找到返回True否则返回False # 1. 读取模板图片 template cv2.imread(template_path, cv2.IMREAD_COLOR) if template is None: print(f错误无法读取模板图片 {template_path}) return False h, w template.shape[:2] # 获取模板高度和宽度 # 2. 截取当前屏幕 screenshot pyautogui.screenshot() # 使用pyautogui截图返回PIL图像 screenshot cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) # 转换为OpenCV格式 # 3. 进行模板匹配 result cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result) # 4. 判断是否匹配成功 if max_val threshold: # 计算模板中心在屏幕上的坐标 center_x max_loc[0] w // 2 offset_x center_y max_loc[1] h // 2 offset_y print(f找到目标位置({center_x}, {center_y})相似度{max_val:.2f}) # 5. 移动并点击 mouse.position (center_x, center_y) time.sleep(0.1) # 点击前稍作延迟模拟人手 mouse.click(Button.left, 1) time.sleep(0.3) # 点击后等待UI响应 return True else: # print(f未找到目标最高相似度{max_val:.2f}) # 调试时可开启 return False # 使用示例寻找并点击“领取奖励”按钮最多尝试10次每次间隔1秒 def claim_reward(): for i in range(10): print(f尝试第 {i1} 次寻找奖励按钮...) if find_and_click(claim_reward.png, threshold0.85): print(奖励领取成功) return True time.sleep(1) print(未找到奖励按钮可能已领取或界面不对。) return False if __name__ __main__: claim_reward()4.3 关键参数解析与调优阈值 (threshold)这是最重要的参数。值越高越接近1匹配要求越严格越不容易误点值越低则越宽松但也可能点到错误的东西。通常从0.8开始调试。如果游戏UI有透明、发光等特效可能需要适当降低到0.75。偏移 (offset_x,offset_y)有时按钮的可点击区域并不在正中心。比如一个很长的按钮你可能希望点击它的左半部分。这时就可以通过偏移量来调整最终点击位置。匹配方法 (cv2.TM_CCOEFF_NORMED)我们使用的是归一化相关系数匹配法它对光照变化有一定抗性是最常用的方法之一。如果效果不好可以尝试cv2.TM_SQDIFF_NORMED归一化平方差匹配法但注意其判断逻辑是值越小越匹配。实操心得不要指望一个模板走天下。游戏在不同场景下同一个按钮的亮度、色调可能有细微差别。一个稳健的做法是为同一个功能准备2-3张在不同场景下截取的模板图依次匹配只要有一个成功就算成功。5. 构建健壮的任务循环与状态机一个完整的自动化脚本不是一堆函数的堆砌而是一个有状态、能处理异常的小系统。5.1 基础任务循环框架下面是一个模拟日常任务流程的框架包含了登录、领奖励、刷副本、下线等状态。import time import random from enum import Enum class GameState(Enum): LOGIN 1 IN_LOBBY 2 CLAIM_DAILY 3 BATTLE 4 LOGOUT 5 ERROR 99 class GameAutoBot: def __init__(self): self.state GameState.LOGIN self.running True self.error_count 0 self.max_errors 5 def run(self): 主循环 print(游戏自动化脚本启动...) while self.running and self.error_count self.max_errors: try: self._state_action() time.sleep(1) # 主循环基础间隔 except Exception as e: print(f状态 {self.state} 执行出错: {e}) self.error_count 1 self.state GameState.ERROR time.sleep(5) if self.error_count self.max_errors: print(错误次数过多脚本停止。) print(脚本运行结束。) def _state_action(self): 根据当前状态执行对应操作 if self.state GameState.LOGIN: self._do_login() elif self.state GameState.IN_LOBBY: self._check_lobby() elif self.state GameState.CLAIM_DAILY: self._claim_daily_rewards() elif self.state GameState.BATTLE: self._enter_battle() elif self.state GameState.LOGOUT: self._do_logout() elif self.state GameState.ERROR: self._recover_from_error() def _do_login(self): print(正在执行登录...) # 这里调用之前写的 find_and_click 函数点击登录按钮 if find_and_click(login_button.png): print(登录按钮点击成功等待进入大厅...) time.sleep(5) # 等待加载实际应用中应改为检测“进入游戏”按钮 # 假设检测到大厅界面 if find_and_click(lobby_indicator.png, threshold0.7): self.state GameState.IN_LOBBY print(登录成功进入大厅状态。) else: print(未检测到大厅界面登录可能失败。) self.error_count 1 else: print(未找到登录按钮。) self.error_count 1 def _check_lobby(self): print(在大厅中检查可执行任务...) # 优先领取每日奖励 if find_and_click(daily_reward_icon.png): self.state GameState.CLAIM_DAILY print(发现每日奖励前往领取。) return # 如果没有奖励则开始刷副本 print(无每日奖励准备开始战斗。) self.state GameState.BATTLE def _claim_daily_rewards(self): print(处理每日奖励...) if find_and_click(claim_all_button.png): time.sleep(2) # 领取后尝试关闭奖励窗口 find_and_click(close_dialog.png) print(每日奖励领取完成。) self.state GameState.IN_LOBBY # 返回大厅状态 def _enter_battle(self): print(准备进入战斗...) if find_and_click(battle_entry.png): print(进入战斗选择界面。) time.sleep(2) # 选择第一个副本 if find_and_click(first_dungeon.png): print(副本选择成功等待加载...) time.sleep(5) # 这里开始战斗循环 self._battle_loop() # 战斗结束后返回大厅 self.state GameState.IN_LOBBY def _battle_loop(self): 模拟一场战斗内的自动化操作 print(战斗开始) for i in range(10): # 假设战斗有10个回合或动作 # 1. 释放技能1 find_and_click(skill_1.png) time.sleep(random.uniform(1.0, 1.5)) # 加入随机延迟更拟人 # 2. 检测是否需要使用药品例如血条低于50% # if self._check_health_low(): # find_and_click(health_potion.png) # time.sleep(1) # 3. 检测战斗是否结束出现“胜利”图标 if find_and_click(victory_flag.png, threshold0.8): print(战斗胜利) time.sleep(3) find_and_click(confirm_victory.png) # 点击确认 break time.sleep(2) # 回合间隔 else: print(战斗循环结束可能超时。) def _do_logout(self): print(执行下线操作...) find_and_click(settings_menu.png) time.sleep(1) find_and_click(logout_button.png) time.sleep(2) self.running False print(已下线。) def _recover_from_error(self): 错误恢复策略 print(尝试从错误中恢复...) # 尝试点击游戏窗口使其激活 pyautogui.click(100, 100) # 假设点击某个安全位置 time.sleep(2) # 尝试返回大厅 if find_and_click(home_button.png): self.state GameState.IN_LOBBY self.error_count 0 # 重置错误计数 print(成功返回大厅错误恢复。) else: # 如果连主页都找不到可能游戏崩溃了这里可以尝试重启游戏客户端 print(恢复失败错误计数增加。) self.error_count 1 if __name__ __main__: bot GameAutoBot() bot.run()5.2 状态机设计的优势这个框架的核心是“状态机”。脚本在任何时刻都处于一个明确的状态如登录、大厅、战斗。每个状态只负责处理该状态下的逻辑完成后明确指定下一个状态。这样做的好处非常明显逻辑清晰调试时你非常清楚脚本卡在哪个环节。易于扩展要增加一个新功能比如“参加公会活动”只需增加一个新的状态GUILD_EVENT和对应的_do_guild_event方法然后在_check_lobby里添加触发条件即可。容错性强在ERROR状态有统一的恢复逻辑。每个操作都有成功/失败的判断失败达到一定次数会触发错误处理避免脚本无限卡死。6. 高级技巧与性能优化当基础功能实现后我们可以让脚本更智能、更高效、更隐蔽。6.1 随机化操作对抗检测机制简单的固定坐标点击和固定时间间隔很容易被游戏服务器检测出是机器行为。引入随机性至关重要。import random import time def human_like_click(x, y, buttonleft): 拟人化点击移动轨迹和点击间隔随机化 mouse Controller() current_x, current_y mouse.position # 将移动路径分成多段模拟人手移动 steps random.randint(3, 8) for i in range(steps): # 计算分段点 intermediate_x current_x (x - current_x) * (i1) / steps intermediate_y current_y (y - current_y) * (i1) / steps # 加入随机偏移让路径不完全是直线 intermediate_x random.randint(-5, 5) intermediate_y random.randint(-5, 5) mouse.position (intermediate_x, intermediate_y) time.sleep(random.uniform(0.02, 0.08)) # 每小段移动的延迟 # 到达目标后可能还有微小抖动 time.sleep(random.uniform(0.05, 0.2)) mouse.position (x random.randint(-2, 2), y random.randint(-2, 2)) time.sleep(random.uniform(0.05, 0.15)) # 执行点击 btn Button.left if button left else Button.right mouse.click(btn, 1) # 点击后停留随机时间 time.sleep(random.uniform(0.1, 0.4))对于等待时间也应用随机延迟# 不要用固定时间 time.sleep(5) # 改用随机时间 time.sleep(random.uniform(4.5, 6.5))6.2 区域截图与多分辨率适配全屏截图 (pyautogui.screenshot()) 速度慢且占用资源。如果操作区域固定可以只截取屏幕的一部分。import pyautogui # 定义游戏窗口区域 (left, top, width, height) game_region (100, 100, 800, 600) def region_screenshot(region): 截取指定区域 screenshot pyautogui.screenshot(regionregion) return cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) # 在模板匹配函数中使用区域截图 screenshot region_screenshot(game_region) # 注意此时模板匹配返回的坐标是相对于区域(region)的需要加上 region[0] 和 region[1] 才是屏幕绝对坐标。为了适配不同分辨率的屏幕一个更优的方案是使用“相对坐标”或“比例坐标”。在脚本初始化时动态检测游戏窗口的位置和大小。def find_game_window(window_title_partial): 通过窗口标题关键字查找游戏窗口返回其位置和大小 import win32gui # Windows API需要 pip install pywin32 def callback(hwnd, windows): if win32gui.IsWindowVisible(hwnd): title win32gui.GetWindowText(hwnd) if window_title_partial.lower() in title.lower(): rect win32gui.GetWindowRect(hwnd) windows.append((hwnd, rect)) return True windows [] win32gui.EnumWindows(callback, windows) if windows: hwnd, (left, top, right, bottom) windows[0] # 取第一个匹配的窗口 width right - left height bottom - top return left, top, width, height else: return None # 使用 game_region find_game_window(游戏名称) if game_region: print(f找到游戏窗口区域{game_region}) else: print(未找到游戏窗口使用默认区域或退出。)6.3 使用线程处理监听与主循环我们希望脚本在后台运行时能通过一个快捷键如F2来随时暂停/继续。这需要用到多线程。from pynput import keyboard import threading class BotWithHotkey: def __init__(self): self.paused False self.listener None self.bot_thread None def on_press(self, key): try: if key keyboard.Key.f2: # 监听F2键 self.paused not self.paused status 已暂停 if self.paused else 已继续 print(f[{status}] 脚本状态切换。) except AttributeError: pass def start_bot(self): 你的主自动化逻辑函数 while True: if not self.paused: # 这里替换成你实际的状态机循环 print(脚本正在运行...) time.sleep(1) else: time.sleep(0.5) # 暂停时降低CPU占用 def run(self): # 启动键盘监听线程 self.listener keyboard.Listener(on_pressself.on_press) self.listener.start() print(热键监听已启动按 F2 暂停/继续脚本。) # 在主线程中启动机器人逻辑也可以开新线程 self.start_bot() # 等待监听线程结束通常不会 self.listener.join() if __name__ __main__: bot BotWithHotkey() bot.run()7. 实战问题排查与经验沉淀即使框架再完善在实际运行中也会遇到各种稀奇古怪的问题。这里记录几个最常见的问题和解决思路。7.1 图像匹配失败的原因与对策问题现象可能原因排查与解决思路始终匹配不到相似度很低1. 模板图片与屏幕画面差异过大如UI更新、皮肤不同。2. 截图区域错误没包含目标。3. 游戏分辨率/缩放比例改变。1.重新截取模板确保模板是最新、最典型的。2.调整阈值适当降低threshold如从0.8调到0.7。3.调试输出在匹配失败时打印max_val值并保存当前截图进行人工比对。4.使用多种匹配方法尝试cv2.TM_SQDIFF_NORMED。匹配到错误的位置1. 阈值过低。2. 模板特征太简单如纯色小圆点容易在其他地方误匹配。1.提高阈值。2.优化模板截取更具独特性的区域包含一些文字或复杂图案。3.多特征验证匹配成功后在目标点附近再匹配另一个关联元素如按钮上的文字进行二次确认。匹配速度慢1. 全屏截图分辨率太高。2. 模板图片过大。1.使用区域截图只截取游戏窗口。2.缩小模板尺寸在能清晰识别的前提下模板越小匹配越快。3.降低检查频率非关键状态不要频繁进行全图匹配。7.2 脚本运行时前台无法操作这是因为pynput或pyautogui控制了鼠标键盘。解决方案是使用“后台模式”但这通常需要更底层的Windows API调用如SendInput并针对特定游戏窗口发送消息实现复杂且不稳定。一个更实用的“妥协”方案是使用虚拟机在虚拟机如VMware, VirtualBox里运行游戏和脚本这样宿主机可以完全自由操作。规划“安全时间”将脚本设计为“间歇性工作”。例如执行5分钟任务然后time.sleep(60)休眠1分钟在这1分钟内你可以快速操作电脑。使用副显示器如果有多显示器将游戏窗口和脚本放在副显示器上运行。7.3 游戏更新导致脚本失效这是自动化脚本的宿命。应对策略模块化设计将图片模板路径、关键坐标等配置项集中放在一个config.py文件或字典里。一旦UI更新你只需要更新这个配置文件而不是翻遍所有代码。建立模板库将不同功能的模板图片分类存放并记录其对应的游戏版本号。添加版本检测脚本启动时可以尝试匹配一个游戏内的版本号标识如登录界面右下角的版本号文字如果匹配失败则提示用户“游戏可能已更新请检查模板”。7.4 日志记录与调试一个没有日志的脚本就像在黑暗中调试。务必加入日志系统。import logging from datetime import datetime def setup_logger(): logger logging.getLogger(GameBot) logger.setLevel(logging.DEBUG) # 文件处理器记录所有级别的日志 fh logging.FileHandler(fbot_log_{datetime.now().strftime(%Y%m%d)}.log, encodingutf-8) fh.setLevel(logging.DEBUG) # 控制台处理器只记录INFO及以上级别 ch logging.StreamHandler() ch.setLevel(logging.INFO) formatter logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) fh.setFormatter(formatter) ch.setFormatter(formatter) logger.addHandler(fh) logger.addHandler(ch) return logger logger setup_logger() # 在代码中使用 logger.info(脚本启动。) try: find_and_click(some_button.png) except Exception as e: logger.error(f点击按钮失败: {e})这样无论是日常运行还是排查问题你都可以通过查看日志文件清晰地知道脚本每一步做了什么在哪里出错。8. 从脚本到可执行文件打包与分发当你写好一个稳定的脚本后可能想分享给朋友或在不同电脑上使用但又不想让对方配置Python环境。这时就需要打包成可执行文件.exe。PyInstaller是目前最常用的工具。安装它pip install pyinstaller。假设你的主程序文件是main.py在项目根目录下打开命令行执行pyinstaller --onefile --windowed --iconmy_icon.ico --add-data template_images;template_images main.py参数解释--onefile打包成单个exe文件分发方便。--windowed运行时不显示控制台黑窗口适合后台脚本。如果需要看打印信息则去掉此参数。--icon指定exe文件的图标。--add-data这是关键我们的脚本依赖template_images文件夹下的所有模板图片。这个参数将文件夹复制到exe的同级目录。格式是源路径;目标路径Windows用分号Linux/Mac用冒号。打包后的目录结构dist/ main.exe (你的可执行文件) template_images/ (需要手动复制到和exe同一目录) login_button.png claim_reward.png ...踩坑实录打包后最常见的错误是“找不到图片模板”。这是因为代码中使用的是相对路径claim_reward.png。在开发时图片在当前目录但打包后exe运行时有一个临时解压目录你的图片不在那里。解决方案是使用以下代码来获取正确的资源路径import sys import os def resource_path(relative_path): 获取资源的绝对路径。打包到exe后也能正确找到资源。 if hasattr(sys, _MEIPASS): # 运行在PyInstaller创建的临时环境中 base_path sys._MEIPASS else: # 运行在正常的开发环境中 base_path os.path.abspath(.) return os.path.join(base_path, relative_path) # 使用方式 template_path resource_path(os.path.join(template_images, claim_reward.png))将代码中所有引用图片路径的地方都改用resource_path函数处理再次打包问题就能解决。打包是一个反复试错的过程尤其是涉及多个依赖库时。如果遇到奇怪的DLL错误可以尝试在干净的虚拟环境中安装依赖并打包。