使用 PyInstaller (--onefile) 捆绑数据文件
Posted
技术标签:
【中文标题】使用 PyInstaller (--onefile) 捆绑数据文件【英文标题】:Bundling data files with PyInstaller (--onefile) 【发布时间】:2011-12-02 06:03:57 【问题描述】:我正在尝试使用 PyInstaller 构建一个包含图像和图标的单文件 EXE。我一辈子都无法让它与--onefile
一起工作。
如果我这样做--onedir
,它会运行得很好。
当我使用--onefile
时,它找不到引用的附加文件(运行编译的EXE 时)。它发现 DLL 和其他一切都很好,只是不是这两个图像。
我查看了运行 EXE 时生成的临时目录(例如\Temp\_MEI95642\
),文件确实在那里。当我将 EXE 放在该临时目录中时,它会找到它们。很费解。
这是我添加到 .spec
文件中的内容
a.datas += [('images/icon.ico', 'D:\\[workspace]\\App\\src\\images\\icon.ico', 'DATA'),
('images/loaderani.gif','D:\\[workspace]\\App\\src\\images\\loaderani.gif','DATA')]
我应该补充一点,我也尝试过不将它们放在子文件夹中,但没有任何区别。
编辑: 由于 PyInstaller 更新,将较新的答案标记为正确。
【问题讨论】:
非常感谢!这里的线路 (a.datas += ...
) 刚才对我很有帮助。 pyinstaller 文档讨论了使用COLLECT
,但是在使用--onefile
时无法将文件放入二进制文件中
@IgorSerebryany:借调!我刚刚遇到了完全相同的问题。
如果我使用了单击菜单栏,我的 .exe 会崩溃
考虑到 temp 文件夹在执行完成后会消失,因此要检查其中的内容,请在您的程序 main 中放置一个 sys._MEIPASS 的 listdir
还有一种方法可以使用我似乎在这个地方看到的 Tree() 语法吗?
【参考方案1】:
较新版本的 PyInstaller 不再设置 env
变量,因此 Shish 出色的 answer 将不起作用。现在路径被设置为sys._MEIPASS
:
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
【讨论】:
我正在使用带有 python 2.7 的 pyinstaller 2,我在 envs 中没有_MEIPASS2
,但 sys._MEIPASS
效果很好,所以 +1。我建议:path = getattr(sys, '_MEIPASS', os.getcwd())
+1。感谢那。现在尝试使用 _MEIPASS2 环境变量一段时间,显然我在它仍然是环境变量时编写了我的代码,因为我记得它工作。最近重新编译时突然停止了。
我建议使用AttributeError
而不是更通用的Exception
。我遇到了缺少 sys 导入并且路径默认默认为 os.path.abspath
的问题。
你或许能帮我解决我的problem。
在没有设置_MEIPASS的情况下使用当前工作目录是错误的。见my answer。【参考方案2】:
pyinstaller 将您的数据解压缩到一个临时文件夹中,并将此目录路径存储在_MEIPASS2
环境变量中。为了在打包模式下获取_MEIPASS2
目录并在解包(开发)模式下使用本地目录,我使用了这个:
def resource_path(relative):
return os.path.join(
os.environ.get(
"_MEIPASS2",
os.path.abspath(".")
),
relative
)
输出:
# in development
>>> resource_path("app_icon.ico")
"/home/shish/src/my_app/app_icon.ico"
# in production
>>> resource_path("app_icon.ico")
"/tmp/_MEI34121/app_icon.ico"
【讨论】:
如何、何时、何地使用它? 您应该在您尝试使用 PyInstaller 编译的 .py 文件中使用此脚本。不要将此代码 sn-p 放在 .spec 文件中,那将不起作用。通过将您通常键入的路径替换为resource_path("file_to_be_accessed.mp3")
来访问您的文件。请注意,您应该对当前版本的 PyInstaller 使用 max' 答案。
这个答案是否特定于使用--one-file
选项?
你或许能帮我解决我的problem。
在没有设置_MEIPASS的情况下使用当前工作目录是错误的。见my answer。【参考方案3】:
如果应用程序未安装 PyInstall(即未设置 sys._MEIPASS
),则所有其他答案都使用 当前工作目录。这是错误的,因为它会阻止您从脚本所在目录以外的目录运行应用程序。
更好的解决方案:
import sys
import os
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)
【讨论】:
感谢 Jon,您的功能帮助我今天解决了一个问题。我只是有一个问题。为什么有些写 _MEIPASS2 而不是 _MEIPASS?当我之前尝试添加 2 时,它不起作用。 我认为os.env['_MEIPASS2']
(使用操作系统环境)是获取目录的旧方法。 AFAIK sys._MEIPASS
是现在唯一正确的方法。
您好,当resource_path()
在主脚本中时,您的解决方案可以正常工作。当它被写在一个模块中时,有没有办法让它工作?截至目前,它会尝试在模块所在的文件夹中获取资源,而不是在主文件夹中。
@Guimute 听起来这个答案中的代码按设计工作:它将资源本地化到提供它的模块,因为它使用__file__
。如果您希望模块能够访问其自身范围之外的资源,那么您将必须实现导入代码为您的模块提供它们的路径以供您使用,例如通过模块提供导入器调用的“set_resource_location()”调用 - 不要认为这与不使用 pyinstaller 时的工作方式有什么不同。
@barny 感谢您的提示!我会按照这些思路尝试一下。【参考方案4】:
也许我错过了一个步骤或做错了什么,但上面的方法没有将数据文件与 PyInstaller 捆绑到一个 exe 文件中。让我分享我所做的步骤。
步骤:将上述方法之一写入您的 py 文件,并导入 sys 和 os 模块。我尝试了他们两个。最后一个是:
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)
步骤:将pyi-makespec file.py写入控制台,创建file.spec文件。
步骤:使用Notepad++打开file.spec,添加如下数据文件:
a = Analysis(['C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.py'],
pathex=['C:\\Users\\TCK\\Desktop\\Projeler'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
#Add the file like the below example
a.datas += [('Converter-GUI.ico', 'C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico', 'DATA')]
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='Converter-GUI',
debug=False,
strip=False,
upx=True,
#Turn the console option False if you don't want to see the console while executing the program.
console=False,
#Add an icon to the program.
icon='C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico')
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='Converter-GUI')
步骤:我按照上述步骤,然后保存了规范文件。最后打开控制台并写入,pyinstaller file.spec(在我的例子中,file=Converter-GUI)。
结论:dist文件夹里还有不止一个文件。
注意:我使用的是 Python 3.5。
编辑:最后它适用于Jonathan Reinhart's method.
步骤:将以下代码添加到您的 python 文件中,并导入 sys 和 os。
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)
步骤:通过添加文件路径调用上述函数:
image_path = resource_path("Converter-GUI.ico")
步骤:将调用函数的上述变量写入代码需要路径的位置。在我的情况下是:
self.window.iconbitmap(image_path)
步骤:在你的python文件同目录下打开控制台,编写如下代码:
pyinstaller --onefile your_file.py
步骤:打开python文件的.spec文件,追加a.datas数组,将图标添加到exe类,上面在第三步编辑前给出。
步骤:保存并退出路径文件。转到包含规范和 py 文件的文件夹。再次打开控制台窗口并输入以下命令:
pyinstaller your_file.spec
在第 6 步之后,您的一个文件就可以使用了。
【讨论】:
谢谢@dilde!并不是说第 5 步中的a.datas
数组采用字符串元组,请参阅 pythonhosted.org/PyInstaller/spec-files.html#adding-data-files 以供参考。
对不起,由于我的英语很弱,我无法理解您的部分信息。那部分是"不是那个a.datas....",你要不要写'注意那个a.datas..."有什么问题吗与我写的关于将字符串添加到 a.datas 数组的内容?
糟糕!应该是注意... 是的,抱歉打错了。您的步骤对我很有用:),我无法在他们的文档或其他任何地方找到此信息。【参考方案5】:
我已经处理这个问题很长时间了(嗯,非常 很长时间)。我几乎搜索了所有来源,但事情并没有在我的脑海中形成模式。
最后,我想我已经想出了要遵循的确切步骤,我想分享一下。
请注意,我的回答使用了其他人对此问题的回答的信息。
如何创建 Python 项目的独立可执行文件。
假设我们有一个project_folder,文件树如下:
project_folder/ main.py xxx.py # another module yyy.py # another module sound/ # directory containing the sound files img/ # directory containing the image files venv/ # if using a venv
首先,假设您已将sound/
和img/
文件夹的路径定义为变量sound_dir
和img_dir
,如下所示:
img_dir = os.path.join(os.path.dirname(__file__), "img")
sound_dir = os.path.join(os.path.dirname(__file__), "sound")
你必须改变它们,如下:
img_dir = resource_path("img")
sound_dir = resource_path("sound")
其中,resource_path()
在脚本顶部定义为:
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, relative_path)
如果使用 venv,则激活虚拟 env,
如果您还没有安装 pyinstaller,请通过:pip3 install pyinstaller
。
运行:pyi-makespec --onefile main.py
为编译和构建过程创建规范文件。
这会将文件层次结构更改为:
project_folder/ main.py xxx.py # modules xxx.py # modules sound/ # directory containing the sound files img/ # directory containing the image files venv/ # if using a venv main.spec
打开(带编辑)main.spec
:
在上面插入:
added_files = [
("sound", "sound"),
("img", "img")
]
然后,将datas=[],
的行改为datas=added_files,
main.spec
上的操作详情见here.
运行pyinstaller --onefile main.spec
仅此而已,您可以从任何地方在project_folder/dist
中运行main
,而其文件夹中无需其他任何内容。您只能分发该main
文件。它现在是真正的独立。
【讨论】:
@JonathonReinhart 如果你还在,我想通知你,我在回答中使用了你的函数。 这就像一个魅力!你为我节省了很多时间。 这意味着读取文本文件的简单 python 代码不会像 exe 一样工作,除非我们按照上述步骤操作? 如果我想在我的脚本中使用图片,我应该放什么?【参考方案6】:为了按照建议重写所有路径代码,我更改了工作目录:
if getattr(sys, 'frozen', False):
os.chdir(sys._MEIPASS)
只需在代码开头添加这两行,其余部分保持不变。
【讨论】:
没有。好的应用程序应该很少更改工作目录。还有更好的方法。【参考方案7】:使用来自 Max 和 This post 的关于添加额外数据文件(如图像或声音)以及我自己的研究/测试的出色回答,我发现了我认为添加此类文件的最简单方法。
如果您想查看一个实时示例,我的存储库是 GitHub 上的 here。
注意:这是为了使用 --onefile
或 -F
命令与 pyinstaller 进行编译。
我的环境如下。
Python 3.3.7 Tkinter 8.6 (check version) Pyinstaller 3.6分两步解决问题
为了解决这个问题,我们需要专门告诉 Pyinstaller 我们有额外的文件需要与应用程序“捆绑”。
我们还需要使用“相对”路径,以便应用程序在作为 Python 脚本或 Frozen EXE 运行时能够正常运行。
话虽如此,我们需要一个允许我们拥有相对路径的函数。使用Max Posted的函数我们可以轻松解决相对路径问题。
def img_resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
我们将像这样使用上述函数,以便在应用程序作为脚本或冻结 EXE 运行时显示应用程序图标。
icon_path = img_resource_path("app/img/app_icon.ico")
root.wm_iconbitmap(icon_path)
下一步是我们需要告诉 Pyinstaller 在编译时在哪里可以找到额外的文件,以便在运行应用程序时在临时目录中创建它们。
如documentation 所示,我们可以通过两种方式解决此问题,但我个人更喜欢管理我自己的 .spec 文件,因此我们将这样做。
首先,您必须已经有一个 .spec 文件。就我而言,我可以通过运行带有额外参数的pyinstaller
来创建我需要的东西,你可以找到额外的参数here。因此,我的规范文件可能看起来与您的略有不同,但在我解释了重要部分后,我将其全部发布以供参考。
added_files 本质上是一个包含元组的列表,在我的情况下,我只想添加单个图像,但您可以使用添加多个 ico、png 或 jpg ('app/img/*.ico', 'app/img')
你也可以创建另一个像 soadded_files = [ (), (), ()]
这样的元组来进行多个导入
元组的第一部分定义了您要添加什么文件或什么类型的文件,以及在哪里可以找到它们。将其视为 CTRL+C
元组的第二部分告诉 Pyinstaller,将路径设置为“app/img/”,并将该目录中的文件放置在与运行 .exe 时创建的任何临时目录相关的目录中。将其视为 CTRL+V
在a = Analysis([main...
下,我设置了datas=added_files
,原来是datas=[]
,但我们希望导出的导入列表是,好吧, 导入,所以我们传入我们的自定义导入。
除非您想要 EXE 的特定图标,否则您不需要这样做,在规范文件的底部,我告诉 Pyinstaller 使用选项 icon='app\\img\\app_icon.ico'
为 exe 设置我的应用程序图标。
added_files = [
('app/img/app_icon.ico','app/img/')
]
a = Analysis(['main.py'],
pathex=['D:\\Github Repos\\Processes-Killer\\Process Killer'],
binaries=[],
datas=added_files,
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Process Killer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True , uac_admin=True, icon='app\\img\\app_icon.ico')
编译成EXE
我很懒;我不喜欢打字超过我必须的东西。我创建了一个可以单击的 .bat 文件。您不必这样做,没有它,此代码将在命令提示符 shell 中运行。
由于 .spec 文件包含我们所有的编译设置和参数(又名选项),我们只需将该 .spec 文件提供给 Pyinstaller。
pyinstaller.exe "Process Killer.spec"
【讨论】:
【参考方案8】:对已接受的答案稍作修改。
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
【讨论】:
在没有设置_MEIPASS的情况下使用当前工作目录是错误的。见my answer。【参考方案9】:我在 PyInstaller 中看到的最常见的抱怨/问题是“我的代码找不到我明确包含在捆绑包中的数据文件,它在哪里?”,而且很难看到 /您的代码正在搜索的位置,因为提取的代码位于临时位置并在退出时被删除。使用@Jonathon Reinhart 的resource_path()
将这段代码添加到查看您的onefile 中包含的内容及其所在位置
for root, dirs, files in os.walk(resource_path("")):
print(root)
for file in files:
print( " ",file)
【讨论】:
【参考方案10】:另一个解决方案是制作一个运行时挂钩,它将您的数据(文件/文件夹)复制(或移动)到存储可执行文件的目录中。钩子是一个简单的 python 文件,它几乎可以做任何事情,就在你的应用程序执行之前。为了设置它,你应该使用 pyinstaller 的 --runtime-hook=my_hook.py
选项。因此,如果您的数据是 images 文件夹,您应该运行以下命令:
pyinstaller.py --onefile -F --add-data=images;images --runtime-hook=cp_images_hook.py main.py
cp_images_hook.py 可能是这样的:
import sys
import os
import shutil
path = getattr(sys, '_MEIPASS', os.getcwd())
full_path = path+"\\images"
try:
shutil.move(full_path, ".\\images")
except:
print("Cannot create 'images' folder. Already exists.")
在每次执行之前,images 文件夹被移动到当前目录(从 _MEIPASS 文件夹),因此可执行文件将始终可以访问它。 这样就不需要修改项目的代码了。
第二种解决方案
您可以利用运行时挂钩机制并更改当前目录,根据一些开发人员的说法,这不是一个好习惯,但它工作正常。
钩子代码如下:
import sys
import os
path = getattr(sys, '_MEIPASS', os.getcwd())
os.chdir(path)
【讨论】:
【参考方案11】:我发现现有的答案令人困惑,并且花了很长时间才弄清楚问题出在哪里。这是我找到的所有内容的汇编。
当我运行我的应用程序时,我收到错误 Failed to execute script foo
(如果 foo.py
是主文件)。要解决此问题,请不要使用 --noconsole
运行 PyInstaller(或编辑 main.spec
以更改 console=False
=> console=True
)。有了这个,从命令行运行可执行文件,你会看到失败。
首先要检查的是它是否正确打包了您的额外文件。如果要包含文件夹 x
,则应添加像 ('x', 'x')
这样的元组。
崩溃后,不要点击确定。如果您使用的是 Windows,则可以使用 Search Everything。查找您的文件之一(例如sword.png
)。您应该找到解压文件的临时路径(例如C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png
)。您可以浏览此目录并确保它包含所有内容。如果您无法通过这种方式找到它,请查找 main.exe.manifest
(Windows) 或 python35.dll
(如果您使用的是 Python 3.5)。
如果安装程序包含所有内容,则下一个可能的问题是文件 I/O:您的 Python 代码正在可执行文件的目录而不是临时目录中查找文件。
为了解决这个问题,这个问题的任何答案都有效。就个人而言,我发现它们都可以工作:有条件地首先在主入口点文件中更改目录,其他一切都按原样工作:
if hasattr(sys, '_MEIPASS'):
os.chdir(sys._MEIPASS)
【讨论】:
对不起。但是更改目录永远不是正确的答案。如果用户提供应用程序的路径,他们希望它是相对于当前目录的。如果你改变它,那就错了。【参考方案12】:如果您仍在尝试将文件相对于可执行文件而不是放在临时目录中,则需要自己复制它。这就是我最终完成它的方式。
https://***.com/a/59415662/999943
您在规范文件中添加一个步骤,将文件系统复制到 DISTPATH 变量。
希望对您有所帮助。
【讨论】:
【参考方案13】:我使用这个基于最大解决方案
def getPath(filename):
import os
import sys
from os import chdir
from os.path import join
from os.path import dirname
from os import environ
if hasattr(sys, '_MEIPASS'):
# PyInstaller >= 1.6
chdir(sys._MEIPASS)
filename = join(sys._MEIPASS, filename)
elif '_MEIPASS2' in environ:
# PyInstaller < 1.6 (tested on 1.5 only)
chdir(environ['_MEIPASS2'])
filename = join(environ['_MEIPASS2'], filename)
else:
chdir(dirname(sys.argv[0]))
filename = join(dirname(sys.argv[0]), filename)
return filename
【讨论】:
【参考方案14】:对于那些仍在寻找更新答案的人,请看:
在documentation 中有一个section on accessing added data files。 这是它的简短和甜蜜。
您需要import pkgutil
并找到您将数据文件添加到的文件夹;即添加到规范文件的元组中的第二个字符串:
datas = [("path/to/mypackage/data_file.txt", "path/to/mypackage")]
知道您在哪里添加了数据文件,然后可以将其作为二进制数据读取,并根据需要对其进行解码。举个例子:
文件结构:
mypackage
__init__.py # This is a MUST in order for the package to be registered
data_file.txt # The data file you've added
data_file.txt
Hello world!
main.py
>import pkgutil
file = pkgutil.get_data("mypackage", "data_file.txt")
contents = file.decode("utf-8")
print(contents) # Hello world!
参考资料:
pkgutil
- 内置库
__init__.py
and packages
【讨论】:
以上是关于使用 PyInstaller (--onefile) 捆绑数据文件的主要内容,如果未能解决你的问题,请参考以下文章
使用 PyInstaller 在 --onefile 中使用 QML 构建 PyQt5
在使用 PyInstaller --onefile 打包 kivy 时包含 .kv/.json 文件?
如何将 PortAudio 包含到 pyinstaller onefile 构建中
Pyinstaller --onefile 模式,如何在解包前向控制台写入消息
使用 --onefile 和 --noconsole 使用 PyInstaller 编译的 PyQt5 应用程序,但 exe 无法启动