使用 PyInstaller (--onefile) 捆绑数据文件
- 2024-11-22 08:48:00
- admin 原创
- 190
问题描述:
我正在尝试使用 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 更新,将较新的答案标记为正确。
解决方案 1:
较新版本的 PyInstaller 不再设置env
变量,因此 Shish 的出色答案将不起作用。现在路径设置为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)
解决方案 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"
解决方案 3:
如果应用程序未安装 PyInstalled(即未设置),则所有其他答案都使用当前工作目录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)
解决方案 4:
我已经处理这个问题很长时间了(好吧,非常长时间)。我搜索了几乎所有的资料,但事情并没有在我的脑海中形成规律。
最后,我想我已经找到要遵循的具体步骤,我想分享一下。
请注意,我的回答使用了其他人对这个问题的回答的信息。
如何创建 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,则激活虚拟环境,
如果还没有安装 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 #<<< NOTICE this new file!
打开(带有编辑器)main.spec
:
在其顶部插入:
added_files = [
("sound", "sound"),
("img", "img")
]
然后,将行更改datas=[],
为datas=added_files,
有关所执行操作的详细信息,main.spec
请参阅PyInstaller 文档中的使用 Spec 文件部分
跑步pyinstaller --onefile main.spec
就是这样,您可以从任何地方运行main
,project_folder/dist
而不必在文件夹中放置任何其他内容。您只能分发该main
文件。现在,它是一个真正的独立版本。
解决方案 5:
也许我错过了某个步骤或做错了什么,但上述方法并没有将数据文件与 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')
步骤:我按照上述步骤操作,然后保存 spec 文件。最后打开控制台并写入pyinstaller file.spec(在我的情况下,file=Converter-GUI)。
结论:dist文件夹中仍然有多个文件。
注意:我使用的是 Python 3.5。
编辑:最后它与Jonathan Reinhart 的方法兼容。
步骤:将以下代码添加到您的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类,这是在第3步编辑之前给出的。
步骤:保存并退出路径文件。转到包含 spec 和 py 文件的文件夹。再次打开控制台窗口并输入以下命令:
pyinstaller your_file.spec
完成第 6 步后,您的一个文件即可使用。
解决方案 6:
使用Max的优秀答案和这篇关于添加额外数据文件(如图像或声音)的帖子以及我自己的研究/测试,我已经找到了我认为添加此类文件的最简单方法。
如果您想看一个实例,我的存储库就在GitHub 上。
注意:这是使用pyinstaller 的--onefile
或-F
命令进行编译。
我的环境如下。
Python 3.3.7
Tkinter 8.6 (检查版本)
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 在编译时在哪里找到额外的文件,以便在应用程序运行时在临时目录中创建它们。
我们可以通过文档中所示的两种方式解决这个问题,但我个人更喜欢管理自己的 .spec 文件,所以我们将这样做。
首先,您必须已经有一个 .spec 文件。就我而言,我能够通过使用pyinstaller
额外的参数来创建我需要的内容,您可以在此处找到额外的参数。因此,我的 spec 文件可能看起来与您的略有不同,但我在解释重要部分后会发布所有内容以供参考。
Added_files本质上是一个包含元组的列表,在我的例子中,我只想添加单个图像,但你可以添加多个 ico、png 或 jpg,('app/img/*.ico', 'app/img')
你也可以创建另一个元组,以便added_files = [ (), (), ()]
进行多个导入
元组的第一部分定义了您想要添加什么文件或什么类型的文件以及在哪里找到它们。可以将其视为 CTRL+C
元组的第二部分告诉 Pyinstaller,创建路径“app/img/”,并将文件放置在与运行 .exe 时创建的临时目录相关的目录中。可以将其视为 CTRL+V
在下a = Analysis([main...
,我已经设置了datas=added_files
,最初它曾经是datas=[]
,但我们想要导入列表,所以我们传入我们的自定义导入。
除非您想要为 EXE 设置特定的图标,否则您不需要这样做,在 spec 文件的底部,我告诉 Pyinstaller 使用选项为 exe 设置我的应用程序图标icon='app\img\app_icon.ico'
。
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"
解决方案 7:
没有按照建议重写所有路径代码,而是更改了工作目录:
if getattr(sys, 'frozen', False):
os.chdir(sys._MEIPASS)
只需在代码开头添加这两行,其余部分保持原样。
解决方案 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)
解决方案 9:
另一个解决方案是制作一个运行时钩子,它将复制(或移动)您的数据(文件/文件夹)到存储可执行文件的目录中。钩子是一个简单的 Python 文件,几乎可以在执行应用程序之前执行任何操作。为了设置它,您应该使用--runtime-hook=my_hook.py
pyinstaller 选项。因此,如果您的数据是图像文件夹,您应该运行以下命令:
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.")
每次执行之前,图像文件夹都会移动到当前目录(从 _MEIPASS 文件夹),因此可执行文件将始终可以访问它。这样就无需修改项目代码。
第二种解决方案
您可以利用运行时挂钩机制并更改当前目录,根据一些开发人员的说法,这不是一个好的做法,但它可以正常工作。
钩子代码如下:
import sys
import os
path = getattr(sys, '_MEIPASS', os.getcwd())
os.chdir(path)
解决方案 10:
我见过的关于 PyInstaller 最常见的抱怨/问题是“我的代码找不到我肯定包含在包中的数据文件,它在哪里?”,而且很难看到你的代码在搜索什么/在哪里,因为提取的代码位于临时位置,退出时会被删除。使用@Jonathon Reinhart 的resource_path()
for root, dirs, files in os.walk(resource_path("")):
print(root)
for file in files:
print( " ",file)
解决方案 11:
如果您仍想将文件放置在可执行文件的相关位置而不是临时目录中,则需要自行复制。这就是我最终完成的方法。
https://stackoverflow.com/a/59415662/999943
您在 spec 文件中添加一个步骤,将文件系统复制到 DISTPATH 变量。
希望有所帮助。
解决方案 12:
我根据最大解决方案使用它
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
解决方案 13:
对于那些仍在寻找更新答案的人,请看这里:
在文档中,有一节介绍如何访问添加的数据文件。
下面是它的简要介绍。
您需要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
数据文件.txt
Hello world!
主程序
import pkgutil
file = pkgutil.get_data("mypackage", "data_file.txt")
contents = file.decode("utf-8")
print(contents) # Hello world!
参考:
pkgutil
- 内置库__init__.py
和包裹
解决方案 14:
最后!基于Luca的解决方案
我最终能够执行--onefile pyinstaller中包含的目录内的文件。
只需使用他发布的方法,您甚至可以将整个目录名输入到文件中,它仍然有效。
您需要做的第二件事实际上是在 pyinstaller 中添加所需的目录/文件。只需使用如下参数:
--add-data "yourDir/And/OrFile;yourDir"
解决方案 15:
斩断戈耳迪之结 re: os.getcwd()
vs.__file__
@JonathonReinhart强烈反对os.getcwd()
在脚本中使用,尤其是反对更改它。这是有充分理由的。但是,正如@Guimoute指出的那样,如果您想将该resource_path()
函数放入模块之外的某个地方main.py
,您可能会遇到问题。这是解决此问题的一种方法。
解决方案
我的解决方案遵循@DisappointedByUnaccountableMod的建议,但没有在 main 中定义新函数。以下是实现的关键:
./main.py
:
我在主脚本文件中添加了以下变量定义。
import os
# Import the resource_path helper function from wherever it is
from src.helpers import resource_path
# Store the location of the directory where the script is running
# i.e. NOT where it is being run from
__main_script_dir__ = os.path.dirname(__file__)
./src/helpers.py
:
import os
import sys
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
# Load dynamically to avoid circular import
from main import __main_script_dir__
base_path = getattr(sys, '_MEIPASS', os.path.abspath(__main_script_dir__))
# print(f"BASE_PATH: {base_path}")
return os.path.abspath(os.path.join(base_path, relative_path))
然后,无论您在何处使用想要捆绑到.exe
文件中的资源,只需调用resource_path(relative_path_to_your_resource)
,它将始终返回程序所需的路径,具体取决于它是作为可执行文件运行还是仅作为脚本运行。
解决方案 16:
我发现现有的答案令人困惑,花了很长时间才找出问题所在。以下是我找到的所有内容的汇编。
当我运行我的应用程序时,我收到错误Failed to execute script foo
(如果foo.py
是主文件)。要解决此问题,请不要使用--noconsole
(或编辑main.spec
以更改console=False
=> console=True
)运行 PyInstaller。使用此方法,从命令行运行可执行文件,您将看到失败。
首先要检查的是它是否正确打包了您的额外文件。('x', 'x')
如果您希望x
包含该文件夹,则应添加元组。
崩溃后,不要单击“确定”。如果您使用的是 Windows,则可以使用“搜索所有内容”。查找您的某个文件(例如sword.png
)。您应该找到解压文件的临时路径(例如)。您可以浏览此目录并确保它包含所有内容。如果您无法通过这种方式找到它,请查找类似(Windows)或(如果您使用的是 Python 3.5)的C:Usersashes999AppDataLocalTemp_MEI157682imagessword.png
内容。main.exe.manifest
`python35.dll`
如果安装程序包含所有内容,下一个可能的问题就是文件 I/O:你的 Python 代码正在可执行文件的目录中查找文件,而不是在临时目录中查找文件。
要解决这个问题,这个问题上的任何答案都可以。就我个人而言,我发现将它们混合使用都可以:首先在主入口点文件中有条件地更改目录,其他一切都按原样工作:
`if hasattr(sys, '_MEIPASS'):
os.chdir(sys._MEIPASS)`
解决方案 17:
以上方法都不适用于我。它不允许我使用 spec 文件和一个文件开关运行 pyinstaller。我在 Windows 上编译。我的应用程序组织如下:
主程序
包含 .png 文件的子文件夹图像
包含 csv 文件的子文件夹数据
这是对我有用的命令:pyinstaller -F -–add-data images\.png;images --add-data data\.csv;data main.py
可执行文件位于 dist 文件夹中。
我希望它能对别人有所帮助!
解决方案 18:
这将为您提供当前目录路径。
import sys, os
def get_current_dir_path():
if getattr(sys, 'frozen', False):
# If the application is run as a bundle (e.g., PyInstaller, cx_Freeze)
current_dir_path = os.path.dirname(sys.executable)
else:
# If the application is run as a script
current_dir_path = os.path.dirname(os.path.abspath(__file__))
return current_dir_path
current_dir_path = get_current_dir_path()
# Now you can use 'current_dir_path'
# Whether you run your script or executable, this will provide you with the current directory path.
print(current_dir_path)