用 PyInstaller 绑定数据文件(—— one file)

我正在尝试用 PyInstaller 构建一个包含一个图像和一个图标的文件 EXE。我无论如何也不能让它和 --onefile一起工作。

如果我做 --onedir它的工作一切都很好。 当我使用 --onefile时,它无法找到引用的附加文件(在运行编译后的 EXE 时)。它可以找到 DLL 和其他所有内容,只是不能找到这两个图像。

我查看了运行 EXE (例如 \Temp\_MEI95642\)时生成的 temp-dir,文件确实在其中。当我把 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')]

我应该补充一下,我也试过不把它们放在子文件夹中,但没有起到什么作用。

编辑: < em > 由于 PyInstaller 更新,将较新的答案标记为正确。

217729 次浏览

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 不再设置 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)

对已接受的答案稍作修改。

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)

我没有按照建议重写所有的路径代码,而是改变了工作目录:

if getattr(sys, 'frozen', False):
os.chdir(sys._MEIPASS)

只要在代码的开头添加这两行代码,其余的代码就可以保持不变。

我发现现有的答案令人困惑,花了很长时间才找出问题所在。这是我找到的所有东西的汇编。

当我运行我的应用程序时,我得到一个错误 Failed to execute script foo(如果 foo.py是主文件)。要解决这个问题,不要使用 --noconsole运行 PyInstaller (或者编辑 main.spec以更改 console=False = > console=True)。通过这种方式,从命令行运行可执行文件,您将看到失败。

首先要检查的是它是否正确地打包了额外的文件。如果希望包含文件夹 x,则应该添加类似 ('x', 'x')的元组。

崩溃后,如果你在 Windows 上,你可以使用 搜索一切。寻找您的文件之一(例如。sword.png).您应该找到它解压缩文件的临时路径(例如。C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png).您可以浏览这个目录,并确保它包含了所有内容。如果您无法通过这种方式找到它,那么可以寻找类似于 main.exe.manifest(Windows)或 python35.dll(如果您使用的是 Python 3.5)的工具。

如果安装程序包含了所有内容,那么下一个可能的问题就是文件 I/O: Python 代码正在可执行文件的目录中查找文件,而不是临时目录。

为了解决这个问题,这个问题的任何答案都是有效的。就我个人而言,我发现它们都可以工作: 在主入口点文件中,有条件地更改目录是第一件事,其他所有事情都可以按原样工作:

如果 hasattr (sys,’_ MEIPASS’) : (sys _ MEIPASS)

在应用程序未被 PyInstled (即未设置 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)

也许我错过了一个步骤或者做了一些错误的事情,但是上面的方法并没有将数据文件和 PyInstaller 绑定到一个 exe 文件中。让我分享我所做的步骤。

  1. Step: 使用导入 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)
    
  2. step: Write, pyi-makespec file.py, to the console, to create a file.spec file.

  3. step: Open, file.spec with Notepad++ to add the data files like below:

    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')
    
  4. step: I followed the above steps, then saved the spec file. At last opened the console and write, pyinstaller file.spec (in my case, file=Converter-GUI).

Conclusion: There's still more than one file in the dist folder.

Note: I'm using Python 3.5.

EDIT: Finally it works with Jonathan Reinhart's method.

  1. step: Add the below codes to your python file with importing sys and 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)
    
  2. step: Call the above function with adding the path of your file:

    image_path = resource_path("Converter-GUI.ico")
    
  3. step: Write the above variable that call the function to where your codes need the path. In my case it's:

        self.window.iconbitmap(image_path)
    
  4. step: Open the console in the same directory of your python file, write the codes like below:

        pyinstaller --onefile your_file.py
    
  5. step: Open the .spec file of the python file and append the a.datas array and add the icon to the exe class, which was given above before the edit in 3'rd step.
  6. step: Save and exit the path file. Go to your folder which include the spec and py file. Open again the console window and type the below command:

        pyinstaller your_file.spec
    

After the 6. step your one file is ready to use.

我见过的 wrt PyInstaller 最常见的抱怨/问题是“我的代码找不到我肯定包含在包中的数据文件,它在哪里?”,并且不容易看到代码正在搜索的内容/位置,因为提取的代码位于临时位置,并且在退出时被删除。使用@Jonathon Reinhart 的 resource_path()将这段代码添加到 你看中,您的文件中包含了什么内容以及它在哪里

for root, dirs, files in os.walk(resource_path("")):
print(root)
for file in files:
print( "  ",file)

如果您仍然试图将文件相对于您的可执行文件而不是放在临时目录中,那么您需要自己复制它。我就是这样完成的。

Https://stackoverflow.com/a/59415662/999943

在 spec 文件中添加一个步骤,将文件系统复制到 DISTPATH 变量。

希望能帮上忙。

我已经处理这个问题很长时间了(好吧,非常很长时间)。我几乎找遍了所有的资料,但是事情还没有在我的脑海中形成一个模式。

最后,我想我已经想出了具体的步骤来遵循,我想分享。

请注意,我的答案使用了其他人关于这个问题的答案的信息。

如何创建 Python 项目的独立可执行文件。

假设我们有一个 project _ file,文件树如下所示:

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_dirimg_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上进行的操作的详细信息,请参阅 给你。

运行 pyinstaller --onefile main.spec

仅此而已,您可以在 project_folder/dist中从任何地方运行 main,而不需要在其文件夹中放置任何其他内容。您只能分发该 main文件。它现在是一个真正的 独立的

使用来自 麦克斯这个帖子的关于添加额外数据文件(如图像或声音)的优秀答案和我自己的研究/测试,我已经找到了我认为添加这些文件最简单的方法。

如果您希望看到一个实时示例,我的存储库是 GitHub 上的 给你

注意: 这是用 pyinstaller 使用 --onefile-F命令进行编译的。

我的环境如下。


分两步解决问题

为了解决这个问题,我们需要明确地告诉 Pyinstaller,我们有额外的文件需要与应用程序“绑定”。

我们还需要是 使用“相对”路径,这样当应用程序作为 Python 脚本或冻结 EXE 运行时就可以正常运行。

既然这样,我们需要一个函数,允许我们有相对路径。利用 Max Posted Max 发布函数可以方便地求解相对路径。

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 目录中创建这些文件。

我们可以通过两种方式解决这个问题,如 文件所示,但我个人更喜欢自己管理。规格文件,所以我们要这么做。

首先,您必须已经有一个。规格文件。在我的例子中,我能够通过运行带有额外参数的 pyinstaller来创建我需要的内容,您可以找到额外的参数 给你。因此,我的规格文件可能看起来有点不同,但我张贴后,我解释了重要部分的参考所有它。

Add _ files 本质上是一个包含 Tuple 的 List,在我的例子中,我只想添加一个 SINGLE 图像,但是你可以使用 ('app/img/*.ico', 'app/img')添加多个 ico、 png 或 jpg 图像。你也可以创建另一个类似 soadded_files = [ (), (), ()]的元组来具有多个导入

元组的第一部分定义要添加的文件或 文件的的类型以及在哪里找到它们。可以把它想象成 CTRL + C

元组的第二部分告诉 Pyinstaller 创建路径‘ app/img/’,并将该目录中的文件放置到运行。前任。可以把它想象成 CTRL + V

a = Analysis([main... 下,我已经设置了 datas=added_files,最初它是 datas=[],但是我们希望导入的列表是导入的,所以我们传递自定义导入。

您不需要这样做,除非您需要 EXE 的特定图标,在 spec 文件的底部,我告诉 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 文件包含我们所有的编译设置和参数(又名选项) ,我们只需要给它。到 Pyinstaller 的 spec 文件。

pyinstaller.exe "Process Killer.spec"

另一种解决方案是创建一个运行时挂钩,它将把数据(文件/文件夹)复制(或移动)到存储可执行文件的目录。钩子是一个简单的 python 文件,几乎可以做任何事情,就在应用程序执行之前。为了设置它,您应该使用 pyinstaller 的 --runtime-hook=my_hook.py选项。因此,如果您的数据是 影像文件夹,您应该运行以下命令:

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)

我使用这个基于最大解决方案

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

对于那些仍在寻找更近期答案的人来说,这就是答案:

文件里,有一个 关于访问添加数据文件的部分
简而言之。


您需要找到 import pkgutil并定位您添加数据文件到的文件夹; 例如,元组中添加到 spec 文件的第二个字符串:

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!

参考文献:

基于 卢卡的解决方案

我最终能够执行包含在—— onefile pyinstaller 目录中的文件。

只要使用他发布的方法,你甚至可以输入一个完整的目录名称的文件,它仍然可以工作。

你需要做的第二件事就是在 pyinstaller 中添加你想要的目录/文件:

--add-data "yourDir/And/OrFile;yourDir"