记录一下PyInstaller + GUI 脚本执行踩坑全记录

最近在做一个 PyInstaller 打包 + tkinter GUI 的项目,踩了不少坑,整理出来给佬们避雷。 背景 项目结构大致是:GUI 主程序负责界面和交互,动态执行一个打包进去的 Python 脚本, 脚本的 sys.stdout / sys.stderr 重定向到 GUI 的文本框。 看...
记录一下PyInstaller + GUI 脚本执行踩坑全记录
记录一下PyInstaller + GUI 脚本执行踩坑全记录

最近在做一个 PyInstaller 打包 + tkinter GUI 的项目,踩了不少坑,整理出来给佬们避雷。


背景

项目结构大致是:GUI 主程序负责界面和交互,动态执行一个打包进去的 Python 脚本,
脚本的 sys.stdout / sys.stderr 重定向到 GUI 的文本框。
看起来简单,但联调过程中几乎每一个环节都出了幺蛾子。


Q1 PermissionError / RuntimeError:无法获取脚本内容

现象:打包后运行报 PermissionError: Permission denied
RuntimeError: 无法获取脚本内容

原因PyInstaller onefile 模式下,_MEIPASS 是只读内存映射,
open() 直接读取其中的文件会失败。

解法:改用嵌入脚本方案,把脚本内容写成一个 Python 模块
embedded_script.py),在打包时作为普通模块引入,不在运行时读文件。


Q2 退出码 -1,但没有任何错误详情

现象:脚本执行失败,捕获到退出码 -1,日志里没有任何异常信息。

原因sys.exit() 抛出的是 SystemExit,它继承自 BaseException
而非 Exception,普通的 except Exception 捕获不到,异常被静默丢弃。

解法

try:
    exec(script_code)
except BaseException as e:
    if isinstance(e, SystemExit):
        error_msg = f"[SystemExit] sys.exit({e.code})"
    else:
        error_msg = traceback.format_exc()

Q3 logging 报 AttributeError:‘GuiWriter’ 没有 isatty

现象AttributeError: 'GuiWriter' object has no attribute 'isatty'

原因logging.StreamHandler 内部会调用 stream.isatty() 判断是否为终端,
自定义的 GuiWriter 没有实现这个方法。

解法:在 GuiWriter 类里补一个:

def isatty(self):
    return False

Q4 TypeError:string argument expected, got ‘bytes’

现象:脚本运行时报类型错误,看起来跟字节/字符串混用有关。

原因:脚本里有一段 Windows UTF-8 兼容处理:

if sys.platform == 'win32' and hasattr(sys.stdout, 'buffer'):
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

这段代码把 sys.stdout 包了一层 TextIOWrapper,但外层 GuiWriter
已经接管了 sys.stdout,两者的类型不一致,写入时就炸了。

解法:在 generate_embedded_script.py 中,生成脚本内容时把这段替换掉:

_win_utf8_block = """if sys.platform == 'win32' and hasattr(sys.stdout, 'buffer'):
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
"""
content = content.replace(_win_utf8_block, "")

Q5 batch 脚本报错:‘ansion’ 不是内部或外部命令

现象:PowerShell 执行 batch 文件时,某条命令被截断,
报类似 'ansion' 不是内部或外部命令 的错误(expansion 被截成了 ansion)。

原因:batch 里用了 call set "VAR=%%VAR:--onedir=%%" 做字符串替换,
遇到参数中含有分号 ; 时会截断;加上 PowerShell 对 !VAR! 有特殊解析,
变量扩展结果与预期不一致。

解法:放弃变量拼接,直接在 batch 中写出完整参数列表,不做运行时字符串替换。


Q6 Excel 文件路径 ./exam_info_df.xlsx 找不到

现象:打包后运行,脚本找不到 Excel 文件,但开发环境没问题。

原因:冻结环境下 Path.cwd() 指向 exe 所在目录,不是项目源码目录,
相对路径失效。

解法:区分冻结环境和开发环境,优先从 _MEIPASS 取文件:

import sys
from pathlib import Path

if getattr(sys, 'frozen', False):
    base = Path(sys._MEIPASS)
else:
    base = Path(__file__).parent

excel_path = base / "exam_info_df.xlsx"

或者通过 CLI 参数 --excel-path 传入绝对路径,更灵活。


Q7 WebDriver 版本不匹配

现象SessionNotCreatedException,WebDriver 与浏览器版本不兼容。

原因:Edge 浏览器静默自动更新,项目里打包的 msedgedriver.exe 版本落后了。

解法:去官方下载页面下载与当前浏览器版本匹配的驱动,
替换 edgedriver_win64/msedgedriver.exe

下载地址:Microsoft Edge WebDriver | Microsoft Edge Developer


速查表

现象 根因 方案 PermissionError 读脚本 _MEIPASS 只读 改用嵌入脚本模块 退出码 -1 无详情 SystemExit 不被 except Exception 捕获 改用 except BaseException isatty() 属性错误 StreamHandler 调用了 isatty() GuiWriter 中补实现 stdout 类型冲突 TextIOWrapperGuiWriter 冲突 生成脚本时移除包装代码 batch 命令截断 分号与 !VAR! 的特殊解析 不做变量拼接,直接写完整参数 Excel 路径找不到 cwd() ≠ 项目目录 用 _MEIPASS 或绝对路径参数 WebDriver 不兼容 浏览器自动更新后驱动落后 匹配版本重新下载驱动

踩完这些坑最大的感受是:PyInstaller 打包后,一切"理所当然"的运行时假设都要重新验证——
工作目录、文件读写权限、标准流对象的接口、异常捕获的范围,一个都不能漏。

佬们如果遇到类似问题欢迎评论区交流 :grinning:

1 个帖子 - 1 位参与者

阅读完整话题

来源: linux.do查看原文