概要:
很多人用pyinstaller
来打包python程序,但是都头疼图片等非py文件如何打包,或者打包后用原来的相对路径却加载不到。本文提供一种完美解决此类问题的方案,亲测可用且稳定。
pyinstaller
可以帮我们的python代码生成单个的可执行文件,方便分享,而不是发几个 py 文件让别人去执行。但是默认情况下,pyinstaller
只打包 *.py 文件,对于其它文件,如图片、文本文件等,需要添加额外的参数手动指定加入打包行列,或者更改特定的 spec 文件。
首先,你需要用 pip 安装 pyinstaller
[Can:]$ python3 -m pip install --user pyinstaller
正常的打包命令(-F
参数会只生成一个单独的文件,方便发给别人):
[Can:]$ pyinstaller -F alien_invasion.py
每次用类似命令打包的开始,pyinstaller
都会在当前目录生成一个 xxx.spec 文件。这是一个配置文件,本质上跟用命令行没什么差别。如果你用的是 pyinstaller xxx.py
的命令,这个 xxx.spec 文件就会根据当前命令的参数重新生成。如果你先修改这个 xxx.spec 文件,再用 pyinstaller xxx.spec
的命令去执行,就会采用配置文件里的内容。由于 xxx.spec 文件很容易被自动重新生成,所以除非你需要频繁重复打包,那可能会节省一些你输入命令的时间,否则我个人的感觉是直接用命令更方便一些。
那如何才能指定让 图片文件 也可以打包进去呢?我的当前目录下,有两个图片文件:
images/alien.bmp
images/ship.bmp
pyinstaller的官方提供了两种方式用来加入额外的文件:
其中 --add-data
用于普通文件, --add-binary
用于二进制文件。我们的bmp文件要用前面这个哦,后面这个主要用于dll之类的第三方库。官方甚至给了几个例子:
用法的注意事项有几个:
-
--add-data
后面可以加=,也可以直接空格,效果一样。 -
--add-data
后面的参数值里,有两部分,用:
或者;
隔开,前面是指打包前文件所在的位置,后面是指打包后你希望文件所在的位置。比如样例里的:--add-data="image1.png:img"
的意思是把当前目录里一个叫 “image1.png” 的文件打包进去,但是放在打包后的 “img” 目录下,也就是变成 img/image1.png,文件名不变。 -
--add-data
可以用好多次,也就是可以一个文件一个文件地加。但也可以整个文件夹一起加,这个官方文档没有说,我自己试过后可以这么用:--add-data 'images:images'
也就是把当前目录下 images 文件夹里的文件都打包进去,打包后的目录也是 images 一样的文件夹下。
最终的命令看起来是这个样子:
[Can:]$ pyinstaller --add-data 'images:images' -wF alien_invasion.py
其中这里的w是把运行的时候的命令行窗口隐藏,这个官方文档里有说,就不多解释了。
然而这就完事了吗?并没有!
(如果你这就高高兴兴地带着生成的 *.app 或者 *.exe 去运行的话,大概率还是会出现图片文件没有找到的情况)
原因是当app运行的时候,会先把资源解压到一个系统的临时目录,包括打包进去的 images文件等,在MAC系统上,会是类似于这样的目录:
/var/folders/gq/scyg5_zj2zz7vmmdr5v6tj2w0000gn/T/_MEI0ORo7m/
里面的内容大概是这样子的:
pygame/
numpy/
images/
include/
lib/
build/
dist/
alien_invasion.spec
base_binary.zip
无数个 *.so 文件
好多个 *.dylib 文件
基本上都是依赖库或者半编译的中间文件,这些我们都不关心,我们关心的是 images 文件夹的确在这里,里面的文件也都在。但是坑爹的是,此时的代码并不能直接通过 “images/alien.bmp” 这样的相对路径来得到文件(应该是因为本身程序就不在这里,所以自然引用不到)。
于是,遭受挫折的你,不得不回去改代码,这事已经不是通过命令加参数能够解决的了,至少 pyinstaller 不行。
这时需要用到 sys 的一个半私有的属性 sys._MEIPASS
,根据官方的说明,它是 pyinstaller 运行时创建的临时目录的绝对路径。需要在程序运行时判断当前是不是有这个属性,如果有的话,生成临时目录下图片的绝对路径,如果没有的话,那就直接返回当前程序的绝对路径:
def get_resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
然后在真正加载图片的代码里:
self.image = pygame.image.load(get_resource_path('images/ship.bmp'))
然后重新用pyinstaller打包一次就可以了:
[Can:]$ pyinstaller --add-data 'images:images' -wF alien_invasion.py
就可以了,这样不管是在什么环境下,都能够正常加载到图片资源,管理起来的时候也可以仍然按照相对路径去管理。
唯一让强近症者抓狂的可能就是,由于是访问了半私有的属性,pycharm会有个warning。这个目前也没有更好的方法。(照道理来说,用 __file__
也可以解决问题,但本质上没什么不同,我没试,看官方文档应该也是可以的,好处是 warning 可能可以消掉)
如果有更漂亮的解决方案,可以留言,谢谢!
参考资料:
https://pyinstaller.readthedocs.io/en/stable/usage.html https://pyinstaller.readthedocs.io/en/stable/runtime-information.html