问题:使用PyInstaller捆绑数据文件(–onefile)
我正在尝试使用PyInstaller构建一个包含文件和图标的单文件EXE。我一生无法忍受--onefile
。
如果我这样做了,--onedir
那么一切都很好。当我使用时--onefile
,它找不到引用的其他文件(在运行编译的EXE时)。它找到DLL和其他所有东西,只是找不到两个映像。
我查看了运行EXE时生成的temp-dir(\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更新,将较新的答案标记为正确。
回答 0
较新版本的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)
回答 1
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"
回答 2
在未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)
回答 3
也许我错过了一个步骤或做错了什么,但是上面的方法并没有将PyInstaller的数据文件捆绑到一个exe文件中。让我分享我所做的步骤。
步骤:通过导入sys和os模块,将上述方法之一写入py文件。我都试过了。最后一个是:
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文件。
步骤:使用记事本++打开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)的方法一起使用。
步骤:通过导入sys和os,将以下代码添加到您的python文件中。
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.步骤之后,您的一个文件就可以使用了。
回答 4
为了更改建议的所有路径代码,我更改了工作目录:
if getattr(sys, 'frozen', False):
os.chdir(sys._MEIPASS)
只需在代码的开头添加这两行,其余部分就可以保留。
回答 5
稍微修改接受的答案。
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)
回答 6
我在PyInstaller上看到的最常见的投诉/问题是“我的代码找不到我确实包含在捆绑软件中的数据文件,它在哪里?”,而且很难看到您的代码在哪里。正在搜索,因为提取的代码位于临时位置,退出时将其删除。使用@Jonathon Reinhart的代码,添加以下代码以查看 onefile中包含的内容及其所在位置。resource_path()
for root, dirs, files in os.walk(resource_path("")):
print(root)
for file in files:
print( " ",file)
回答 7
我发现现有的答案令人困惑,并且花了很长时间解决问题所在。这是我发现的所有内容的汇总。
运行我的应用程序时,出现错误Failed to execute script foo
(如果foo.py
是主文件)。要解决此问题,请不要使用来运行PyInstaller --noconsole
(或进行编辑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代码正在可执行文件的目录中查找文件,而不是在temp目录中查找。
要解决此问题,此问题的任何答案都可以解决。就个人而言,我发现它们混合在一起起作用:在主入口点文件中有条件地更改目录的第一件事,其他所有内容都保持原样:
if hasattr(sys, '_MEIPASS'):
os.chdir(sys._MEIPASS)
回答 8
如果仍在尝试将文件相对于可执行文件而不是放置在temp目录中,则需要自己复制文件。这就是我最终完成它的方式。
https://stackoverflow.com/a/59415662/999943
您在规范文件中添加了一个将文件系统复制到DISTPATH变量的步骤。
希望能有所帮助。
回答 9
我已经(很长很长时间)处理这个问题了。我搜索了几乎所有的资源,但事情并没有在我的脑海中浮现。
最后,我想我想出了我想分享的确切步骤。
请注意,我的答案使用有关其他人对此问题的答案的信息。
如何创建python项目的独立可执行文件。
假设我们有一个project_folder,文件树如下:
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
首先,假设您已将变量的路径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
创建用于编译和构建过程的spec文件。
这会将文件层次结构更改为:
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
打开(带有edior)main.spec
:
在其顶部插入:
added_files = [
("sound", "sound"),
("img", "img")
]
然后,将的行更改datas=[],
为datas=added_files,
有关操作的详细信息,main.spec
请参见此处。
跑 pyinstaller --onefile main.spec
就是这样,您可以main
在project_folder/dist
任何位置运行,而无需在其文件夹中添加任何其他内容。您只能分发该main
文件。现在是真正的独立。
回答 10
使用Max和本文中有关添加额外的数据文件(如图像或声音)以及我自己的研究/测试的出色答案,我弄清楚了我认为添加此类文件的最简单方法。
如果你想看到一个活生生的例子,我的仓库是这里 GitHub上。
注意:这是用于在pyinstaller中使用--onefile
or -F
命令进行编译。
我的环境如下。
- Python 3.3.7
- Tkinter 8.6 (检查版)
- Pyinstaller 3.6
分两步解决问题
为了解决该问题,我们需要专门告诉Pyinstaller,我们还有一些其他文件需要与应用程序“捆绑”在一起。
我们还需要使用“相对”路径,因此当应用程序作为Python脚本或Frozen EXE运行时,它可以正常运行。
话虽如此,我们需要一个允许我们拥有相对路径的功能。使用“ 最大发布 ”功能,我们可以轻松解决相对路径。
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在编译时在哪里可以找到多余的文件,以便在运行应用程序时在temp目录中创建它们。
我们可以按照文档中所示的两种方式解决此问题,但是我个人更喜欢管理自己的.spec文件,因此我们将采用这种方式。
首先,您必须已经有一个.spec文件。就我而言,我可以通过运行pyinstaller
额外的args 创建所需的内容,您可以在此处找到额外的args 。因此,我的规格文件可能看起来与您的有些不同,但是在解释了重要内容之后,我将其全部发布以供参考。
add_files本质上是一个包含元组的列表,在我的情况下,我只想添加一个图像,但是您可以使用来添加多个ico,png或jpg,('app/img/*.ico', 'app/img')
也可以创建另一个元组,例如added_files = [ (), (), ()]
具有多个导入
元组的第一部分定义您要添加的文件或文件的类型以及在何处找到它们。认为这是CTRL + C
元组的第二部分告诉Pyinstaller,使其路径为“ app / img /”,并将文件放置在该目录中,与您运行.exe时创建的任何临时目录相对。认为这是CTRL + V
datas=added_files
,最初它曾经是,datas=[]
但是我们想要导入的列表是导入的,所以我们传入自定义导入。
除非您需要EXE的特定图标,否则您无需执行此操作,在spec文件的底部,我告诉Pyinstaller使用option为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文件,只需单击即可。您不必这样做,此代码将在没有它的情况下在命令提示符外壳中运行。
由于.spec文件包含我们所有的编译设置和args(aka选项),我们只需要将该.spec文件提供给Pyinstaller。
pyinstaller.exe "Process Killer.spec"
回答 11
另一个解决方案是制作一个运行时挂钩,该挂钩将把您的数据(文件/文件夹)复制(或移动)到存储可执行文件的目录中。挂钩是一个简单的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.")
在每次执行之前,将images文件夹移至当前目录(从_MEIPASS文件夹),因此可执行文件将始终可以对其进行访问。这样就无需修改项目代码。
第二解决方案
您可以利用运行时挂钩机制并更改当前目录,对于某些开发人员而言,这不是一个好习惯,但可以正常工作。
挂钩代码可以在下面找到:
import sys
import os
path = getattr(sys, '_MEIPASS', os.getcwd())
os.chdir(path)