资讯详情

资讯详情

Python 3.7 + PyInstaller 3.6 封装实战:从踩坑到优化

1. 项目缘起与整体设计思路1.1 为什么会有这个封装需求手里攒了一堆Python小工具有给运营同事用的数据清洗脚本有给测试组用的日志分析器还有自己平时写的一些自动化小玩意。这些东西在自己电脑上跑得好好的一旦要交给别人用问题就来了——对方电脑上没装Python或者版本不对或者缺了一堆第三方库。总不能每次都让人家先装个Python环境再pip install一堆依赖吧那体验太差了。所以就需要把Python程序封装成独立的可执行文件让对方双击就能用。这个需求在Windows环境下尤其常见因为大部分非技术同事用的都是Windows系统。封装工具选来选去PyInstaller是目前最成熟、社区最活跃的方案没有之一。它能把Python解释器、依赖库、以及你的代码打包成一个exe文件用户拿到就能直接运行。这个项目记录的就是我在Python 3.7环境下使用PyInstaller 3.6版本进行程序封装的全过程包括中间踩过的坑、遇到的错误信息、以及最终的解决方案。适合所有需要把Python脚本交付给非技术用户的朋友参考不管你是刚接触封装的新手还是已经用过几次但总被各种报错折磨的老手这篇内容应该都能帮你省下不少查资料的时间。1.2 技术选型的考量为什么是Python 3.7加PyInstaller 3.6这个组合这不是随便选的。Python 3.7在当年是一个稳定性极高的版本很多生产环境至今还在用它的兼容性好第三方库支持完善。而PyInstaller 3.6是当时比较成熟的一个版本对Python 3.7的支持很到位不像早期版本那样动不动就出各种奇怪的兼容性问题。有人可能会问为什么不用更新的版本原因很简单——新版本不一定稳定。PyInstaller的更新有时候会引入新的bug特别是在处理某些特定库的时候。3.6这个版本经过了大量用户的验证社区里能找到的解决方案也最多。当你遇到问题时搜索PyInstaller 3.6 报错能出来的结果比搜索最新版本的多得多。另一个考量是打包体积。PyInstaller默认会把整个Python解释器和所有依赖都打进去出来的exe动辄几十兆甚至上百兆。通过合理的配置和裁剪可以把这个体积降下来。这个后面会详细讲。1.3 整体封装流程概览整个封装流程可以分成几个阶段环境准备、依赖分析、打包配置、执行打包、测试验证、问题修复。每个阶段都有它的关键点和容易出错的地方。环境准备阶段要确保你的Python环境和PyInstaller版本匹配同时要把项目依赖整理清楚。依赖分析阶段要搞清楚你的程序到底用了哪些库哪些是必须打包进去的哪些是可以排除的。打包配置阶段要写好spec文件或者命令行参数控制打包的行为。执行打包就是运行命令等待结果。测试验证要在干净的机器上测试确保没有遗漏依赖。问题修复就是针对各种报错逐个解决。这个流程看起来简单但实际操作中每个阶段都可能出问题。比如依赖分析没做好打包出来的exe在你自己电脑上能跑换台电脑就报缺少DLL。比如打包配置写错了把不该打的东西打进去了exe体积暴涨。比如测试验证不充分用户拿到手才发现某个功能用不了。2. 环境准备与依赖梳理2.1 Python环境的确认与清理在开始打包之前第一件事是确认你的Python环境是干净的。什么叫干净就是你的项目依赖是明确的没有一堆乱七八糟的全局包干扰。我见过太多人打包失败最后发现是因为全局环境里装了几百个包PyInstaller在分析依赖的时候把一些不相关的东西也打进去了导致各种冲突。推荐的做法是使用虚拟环境。在项目目录下创建一个独立的虚拟环境只安装项目真正需要的包。这样PyInstaller分析出来的依赖就是准确的不会有多余的东西。python -m venv venv venv\Scripts\activate pip install -r requirements.txt pip install pyinstaller3.6这几条命令做完你的环境就准备好了。注意PyInstaller要装在虚拟环境里面不要装在全局。装完之后可以用pyinstaller --version确认一下版本是不是3.6。注意如果你的项目里有C扩展的库比如numpy、pandas这些虚拟环境的Python版本要和打包目标机器的架构一致。32位和64位不能混。2.2 项目依赖的精确梳理依赖梳理是很多人忽略的一步但它极其重要。你需要明确知道你的程序导入了哪些包这些包又依赖了哪些包。最笨但最可靠的方法是看你的import语句把所有import的包列出来然后逐个确认。更专业一点的做法是用pipreqs这个工具它可以扫描你的项目代码自动生成requirements.txt。pip install pipreqs pipreqs ./ --encodingutf-8生成的requirements.txt就是你的项目真正依赖的包列表。但要注意pipreqs有时候会漏掉一些动态导入的包比如通过__import__或者importlib导入的。这些需要你手动补充。还有一个坑是隐式依赖。有些包在运行时会动态加载一些东西PyInstaller静态分析的时候发现不了。比如某些库会读取同目录下的配置文件或者数据文件这些文件如果不打包进去运行时就会报错。解决办法是在spec文件里用datas参数把这些文件显式包含进来。2.3 PyInstaller的安装与版本确认PyInstaller 3.6的安装很简单一条pip命令的事。但安装完之后要确认几件事版本号对不对依赖的pyinstaller-hooks-contrib版本是否兼容以及是否和你的Python版本匹配。pip show pyinstaller这条命令会显示PyInstaller的版本、安装位置、依赖项等信息。确认版本是3.6依赖项没有冲突。如果之前装过其他版本的PyInstaller建议先卸载干净再装3.6。pip uninstall pyinstaller pip install pyinstaller3.6有时候卸载不干净会残留一些文件导致新版本装上去之后行为异常。如果遇到奇怪的问题可以手动去site-packages目录下把pyinstaller相关的文件夹和文件都删掉再重新安装。3. 核心打包配置与参数详解3.1 命令行参数的核心选项PyInstaller的命令行参数很多但常用的就那么几个。先看一个典型的打包命令pyinstaller --nameMyTool --onefile --windowed --iconapp.ico --clean main.py逐个解释这些参数的含义--name指定生成的exe文件名。不指定的话默认用你的脚本文件名。建议取一个有意义的名字不要用中文不要有空格。--onefile把所有东西打包成一个单独的exe文件。这是最常用的模式方便分发。但缺点是启动速度会慢一些因为每次运行都要先解压。如果你追求启动速度可以用--onedir模式生成一个文件夹里面包含exe和一堆依赖文件。文件夹模式启动快但分发的时候要整个文件夹一起给。--windowed表示这是一个GUI程序运行的时候不显示控制台窗口。如果你的程序是命令行工具就不要加这个参数否则你看不到输出。反过来如果你的程序是GUI的不加这个参数会弹出一个黑色的控制台窗口很难看。--icon指定exe的图标。图标文件必须是.ico格式不能是.png或者.jpg。网上有很多在线转换工具可以把png转成ico。--clean在打包之前清理临时文件和缓存。建议每次都加上避免旧缓存导致的奇怪问题。3.2 spec文件的编写与定制命令行参数能搞定大部分场景但有些高级需求必须通过spec文件来实现。spec文件是PyInstaller的配置文件本质上是一个Python脚本PyInstaller会执行它来获取打包配置。用pyinstaller --nameMyTool main.py命令打包一次之后会在当前目录生成一个MyTool.spec文件。这个文件就是自动生成的配置你可以直接编辑它。一个典型的spec文件长这样# -*- mode: python ; coding: utf-8 -*- block_cipher None a Analysis( [main.py], pathex[C:\\Users\\YourName\\Projects\\MyTool], binaries[], datas[(config.ini, .), (data/*.json, data)], hiddenimports[pkg_resources.py2_warn], hookspath[], runtime_hooks[], excludes[matplotlib, scipy], win_no_prefer_redirectsFalse, win_private_assembliesFalse, cipherblock_cipher, noarchiveFalse, ) pyz PYZ(a.pure, a.zipped_data, cipherblock_cipher) exe EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], nameMyTool, debugFalse, bootloader_ignore_signalsFalse, stripFalse, upxTrue, upx_exclude[], runtime_tmpdirNone, consoleFalse, iconapp.ico, )这里有几个关键配置需要说明datas参数用来包含非Python文件比如配置文件、数据文件、图片等。格式是(源路径, 目标路径)的列表。目标路径是相对于exe运行时的目录。比如(config.ini, .)表示把config.ini放到exe同目录下。hiddenimports用来指定那些PyInstaller分析不出来的隐式导入。比如有些库在运行时动态导入模块PyInstaller静态分析发现不了就需要在这里手动加上。excludes用来排除不需要的库。这个对减小打包体积非常关键。比如你的程序用了pandas但pandas会连带把matplotlib、scipy这些也拉进来如果你确定用不到就可以在excludes里排除掉。upx参数控制是否使用UPX压缩。UPX是一个可执行文件压缩工具可以显著减小exe体积。但有时候压缩会导致一些奇怪的问题如果打包出来的exe运行异常可以试试把upx关掉。3.3 打包体积的优化策略PyInstaller打包出来的exe体积大是出了名的。一个简单的脚本可能打出几十兆稍微复杂点的上百兆很正常。但通过合理的优化可以把体积降下来不少。第一个策略是排除不需要的库。在spec文件的excludes列表里加上那些你用不到的重量级库。常见的可以排除的有matplotlib、scipy、PyQt5、tkinter如果你不用GUI的话、IPython、jupyter等。第二个策略是使用UPX压缩。UPX可以把exe和DLL文件压缩运行时在内存中解压。一般能减少30%到50%的体积。但要注意有些杀毒软件会对UPX压缩过的文件误报如果你的用户环境有严格的杀毒软件可能需要关闭UPX。第三个策略是使用虚拟环境打包。前面说过干净的虚拟环境可以避免把无关的包打进去。这个对体积的影响很大因为全局环境里往往装了很多用不到的东西。第四个策略是考虑使用--onedir模式。虽然文件夹模式看起来文件多但总体积其实比单文件模式小因为单文件模式需要把解压后的内容也包含进去。而且文件夹模式启动快不需要每次解压。4. 实操过程与错误处理实录4.1 完整打包流程演示假设我们有一个名为data_cleaner.py的脚本功能是读取Excel文件做一些数据清洗然后输出结果。脚本依赖pandas和openpyxl。我们要把它打包成一个独立的exe。第一步创建虚拟环境并安装依赖cd C:\Projects\DataCleaner python -m venv venv venv\Scripts\activate pip install pandas openpyxl pyinstaller3.6第二步生成spec文件pyinstaller --nameDataCleaner --onefile data_cleaner.py这一步会生成DataCleaner.spec文件同时会尝试打包一次。第一次打包大概率会成功但打出来的exe可能有问题比如缺少某些隐式导入的模块。第三步编辑spec文件添加必要的配置a Analysis( [data_cleaner.py], pathex[], binaries[], datas[], hiddenimports[openpyxl, pandas._libs.tslibs.base], hookspath[], runtime_hooks[], excludes[matplotlib, scipy, PyQt5, IPython], ... )这里添加了openpyxl和pandas._libs.tslibs.base到hiddenimports因为pandas在运行时可能会动态导入这些模块。excludes里排除了几个用不到的大库。第四步用spec文件重新打包pyinstaller --clean DataCleaner.spec第五步测试打包结果。去dist目录下找到DataCleaner.exe双击运行测试各项功能是否正常。4.2 常见错误信息与解决方案打包过程中会遇到各种各样的错误这里整理几个最典型的。错误一ModuleNotFoundError: No module named xxx这是最常见的错误。原因是PyInstaller没有把某个模块打包进去。解决方法是在spec文件的hiddenimports里加上这个模块名。如果不知道具体缺哪个模块可以看错误信息里的模块名然后加上去。有时候缺的不是一个模块而是一个包里的子模块。比如pandas._libs.tslibs.base这种。这时候要把完整的路径写进去。错误二ImportError: DLL load failed这个错误通常发生在使用了C扩展的库上比如numpy、pandas、PyQt5等。原因是PyInstaller没有正确地把相关的DLL文件打包进去。解决方法有几个一是确保你的Python环境和打包目标机器架构一致都是64位或都是32位。二是在spec文件的binaries参数里手动指定DLL文件。三是尝试更新PyInstaller的hooks有时候是新版本的库需要新的hook支持。错误三Failed to execute script xxx这个错误信息很笼统具体原因要看详细的日志。通常是因为运行时缺少某个文件或者某个依赖没有正确加载。排查方法是先用--debug模式打包这样运行时会输出详细的日志。根据日志里的错误信息定位问题。错误四打包成功但exe运行闪退双击exe之后窗口一闪就没了看不到任何错误信息。这种情况通常是因为程序在启动阶段就崩溃了。解决方法是用命令行运行exe这样可以看到输出。打开cmdcd到exe所在目录然后运行DataCleaner.exe。这样错误信息就会打印在命令行里。错误五RecursionError: maximum recursion depth exceeded这个错误发生在PyInstaller分析依赖的时候某个模块的依赖关系太复杂导致递归深度超限。解决方法是在spec文件开头加上import sys sys.setrecursionlimit(5000)把递归深度限制调大。4.3 打包后的测试验证要点打包完成不代表万事大吉充分的测试是必须的。测试要在干净的环境里进行最好是一台没装过Python的电脑或者一个全新的虚拟机。测试要点包括程序能否正常启动所有功能是否正常文件读写是否正常网络请求是否正常如果有的话GUI显示是否正常如果是GUI程序退出时是否正常清理资源。特别要注意的是文件路径问题。在开发环境下程序的工作目录和打包后的工作目录可能不一样。如果你的代码里用了相对路径打包后可能会找不到文件。解决办法是使用绝对路径或者用sys._MEIPASS来获取打包后的临时目录。import sys import os if getattr(sys, frozen, False): # 打包后的环境 base_path sys._MEIPASS else: # 开发环境 base_path os.path.dirname(os.path.abspath(__file__)) config_path os.path.join(base_path, config.ini)这段代码可以确保无论是否打包都能正确找到资源文件。5. 进阶技巧与经验总结5.1 减小打包体积的实战技巧除了前面说的排除不需要的库和使用UPX还有几个技巧可以进一步减小体积。第一个是使用--exclude-module参数排除单个模块。比如--exclude-moduletkinter可以排除tkinter。这个在命令行里用比较方便不用改spec文件。第二个是分析打包日志看看哪些大文件被打进去了。PyInstaller在打包时会输出详细的日志里面会列出所有被打包的文件和它们的大小。根据这些信息可以有针对性地排除一些不需要的东西。第三个是考虑使用Nuitka作为替代方案。Nuitka是另一个Python打包工具它把Python代码编译成C代码再编译成机器码生成的exe体积更小运行速度更快。但Nuitka的兼容性不如PyInstaller对某些库的支持不够好。如果你的项目依赖比较简单可以试试Nuitka。5.2 多平台打包的注意事项PyInstaller不支持交叉编译也就是说你在Windows上只能打Windows的包在Linux上只能打Linux的包在Mac上只能打Mac的包。如果你需要多平台的exe必须在对应的平台上分别打包。Windows平台要注意的是32位和64位的Python打出来的包不通用。如果你的用户有32位系统你需要用32位的Python来打包。Linux平台要注意的是不同发行版的glibc版本可能不一样。在Ubuntu 20.04上打的包在CentOS 7上可能跑不起来。解决办法是在目标系统对应的环境里打包或者使用Docker容器来构建。Mac平台要注意的是打包出来的app需要签名才能在最新版本的macOS上运行。没有签名的app会被Gatekeeper拦截。签名需要苹果开发者账号这个门槛比较高。5.3 版本兼容性问题的处理PyInstaller 3.6虽然稳定但也不是万能的。有些新版本的库可能不兼容需要降级或者打补丁。比如numpy 1.20以上的版本在PyInstaller 3.6下打包可能会出问题。解决办法是降级到numpy 1.19或者升级PyInstaller到更新的版本。再比如Python 3.7.10以上的版本某些情况下会有ssl模块的问题。解决办法是降级到Python 3.7.9或者手动把ssl相关的DLL文件复制到打包目录。处理版本兼容性问题的通用思路是先确认问题出在哪个库上然后尝试降级或升级这个库或者找对应的补丁。社区里通常已经有人遇到过类似的问题搜索一下就能找到解决方案。5.4 我踩过的那些坑说几个我实际踩过的坑都是文档里不会写的。第一个坑是中文路径问题。PyInstaller对中文路径的支持不太好如果你的项目路径里有中文打包可能会失败。解决办法是把项目放到纯英文路径下。第二个坑是杀毒软件误报。打包出来的exe经常被Windows Defender或者360杀毒误报为病毒。这是因为PyInstaller的bootloader被某些杀毒软件标记了。解决办法是给exe做代码签名或者把exe加到杀毒软件的白名单里。如果用户环境不可控可以考虑用其他打包工具。第三个坑是临时目录权限问题。--onefile模式运行时会把文件解压到临时目录如果临时目录没有写权限程序就会崩溃。解决办法是用--onedir模式或者在代码里指定一个可写的临时目录。第四个坑是打包后的程序读取不到环境变量。开发环境下能读到的环境变量打包后可能读不到。这是因为打包后的程序运行环境变了。解决办法是在代码里显式设置需要的环境变量或者用配置文件来代替环境变量。第五个坑是打包时间过长。大型项目的打包可能需要十几分钟甚至更久。解决办法是使用--onedir模式增量打包只重新打包改动的部分。另外把杀毒软件的实时扫描关掉也能加快打包速度。5.5 常见问题速查表问题现象可能原因解决方案打包失败提示ModuleNotFoundError缺少隐式导入在hiddenimports中添加模块名exe运行闪退启动阶段崩溃用命令行运行查看错误信息提示DLL load failedC扩展库未正确打包检查架构一致性手动指定binaries打包体积过大包含了不需要的库在excludes中排除使用UPX压缩中文乱码编码问题在代码中指定UTF-8编码找不到配置文件路径问题使用sys._MEIPASS获取资源路径杀毒软件报毒bootloader被误报代码签名或加白名单打包速度慢项目太大或杀毒扫描使用onedir模式关闭实时扫描这张表可以收藏起来遇到问题的时候对照着排查能省不少时间。5.6 后续维护与更新策略程序打包发布之后后续的维护和更新也是要考虑的。如果你的程序需要频繁更新每次都重新打包分发就很麻烦。一个方案是做一个简单的更新检查机制。程序启动时检查服务器上有没有新版本有的话提示用户下载。这个实现起来不难用requests发个请求对比版本号就行。另一个方案是把核心逻辑做成可替换的模块打包的时候只打包框架业务逻辑放在外部文件里。这样更新的时候只需要替换业务逻辑文件不用重新打包整个exe。还有一个方案是使用安装包制作工具把exe和依赖文件打成一个安装包。用户安装一次后续更新通过安装包的升级功能来完成。常用的安装包工具有Inno Setup、NSIS等。不管用哪种方案关键是要在项目初期就考虑好更新策略不要等到发布之后才发现更新很麻烦。我在实际项目中的体会是如果程序是给内部同事用的做一个简单的版本检查就够了。如果是给外部用户用的最好从一开始就设计好更新机制不然后期改起来很痛苦。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →