146

我正在尝试使用 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 更新,将较新的答案标记为正确。

4

14 回答 14

205

较新版本的 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)
于 2012-12-09T19:08:32.747 回答
58

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"
于 2011-10-06T13:36:18.447 回答
46

在应用程序未安装 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)
于 2017-06-04T10:07:27.420 回答
22

也许我错过了一步或做错了什么,但上面的方法没有将数据文件与 PyInstaller 捆绑到一个 exe 文件中。让我分享我所做的步骤。

  1. 步骤:将上述方法之一写入您的 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)
    
  2. 步骤:将pyi-makespec file.py写入控制台,以创建 file.spec 文件。

  3. 步骤:使用 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')
    
  4. 步骤:我按照上述步骤,然后保存了规范文件。最后打开控制台并写入,pyinstaller file.spec(在我的例子中,file=Converter-GUI)。

结论: dist 文件夹中还有不止一个文件。

注意:我使用的是 Python 3.5。

编辑:最后它适用于乔纳森莱因哈特的方法。

  1. 步骤:通过导入 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)
    
  2. 步骤:通过添加文件的路径调用上述函数:

    image_path = resource_path("Converter-GUI.ico")
    
  3. 步骤:将调用函数的上述变量写入代码需要路径的位置。就我而言,它是:

        self.window.iconbitmap(image_path)
    
  4. 步骤:在您的python文件的同一目录中打开控制台,编写如下代码:

        pyinstaller --onefile your_file.py
    
  5. 步骤:打开python文件的.spec文件并附加a.datas数组并将图标添加到exe类,这是上面在第三步编辑之前给出的。
  6. 步骤:保存并退出路径文件。转到包含规范和 py 文件的文件夹。再次打开控制台窗口并输入以下命令:

        pyinstaller your_file.spec
    

在 6. 步骤之后,您的一个文件就可以使用了。

于 2017-06-08T14:14:25.313 回答
10

我一直在处理这个问题很长时间(嗯,长时间)。我搜索了几乎所有来源,但事情并没有在我脑海中形成一种模式。

最后,我想我已经想出了要遵循的确切步骤,我想分享一下。

请注意,我的答案使用了其他人对此问题的答案的信息。

如何创建 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_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,则激活虚拟环境,

如果您还没有安装 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

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

于 2020-03-31T15:04:46.737 回答
8

为了按照建议重写我的所有路径代码,我更改了工作目录:

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

只需在代码开头添加这两行,其余部分保持不变。

于 2016-04-06T16:09:18.147 回答
4

使用Max这篇关于添加额外数据文件(如图像或声音)以及我自己的研究/测试的帖子的出色答案,我已经找到了我认为添加此类文件的最简单方法。

如果您想查看一个实时示例,我的存储库位于GitHub 上。

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

我的环境如下。


分两步解决问题

为了解决这个问题,我们需要特别告诉 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找到额外的参数。因此,我的规范文件可能看起来与您的略有不同,但在我解释了重要的部分后,我将其全部发布以供参考。

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 的特定图标,否则您不需要这样做,在规范文件的底部,我告诉 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"
于 2020-05-03T04:26:59.773 回答
3

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

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)
于 2015-08-17T10:31:08.330 回答
1

我在 PyInstaller 中看到的最常见的抱怨/问题是“我的代码找不到我明确包含在捆绑包中的数据文件,它在哪里?”,而且很难看到你的代码是什么/在哪里正在搜索,因为提取的代码位于临时位置并在退出时被删除。使用@Jonathon Reinhart's添加这段代码以查看您的 onefile 中包含的内容及其位置resource_path()

for root, dirs, files in os.walk(resource_path("")):
    print(root)
    for file in files:
        print( "  ",file)
于 2019-02-26T09:12:21.483 回答
1

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

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)
于 2020-07-25T05:00:44.610 回答
0

我发现现有的答案令人困惑,并且花了很长时间才弄清楚问题出在哪里。这是我找到的所有内容的汇编。

当我运行我的应用程序时,我收到一个错误Failed to execute script foo(如果foo.py是主文件)。要解决此问题,请不要使用--noconsole(或编辑main.spec更改console=False=> console=True)运行 PyInstaller。有了这个,从命令行运行可执行文件,你会看到失败。

首先要检查的是它是否正确打包了您的额外文件。您应该添加元组,例如('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)

于 2016-12-18T19:56:58.633 回答
0

如果您仍在尝试将文件相对于您的可执行文件而不是放在临时目录中,则需要自己复制它。这就是我最终完成它的方式。

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

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

希望有帮助。

于 2019-12-20T15:40:29.377 回答
0

我根据最大解决方案使用它

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
于 2020-09-21T17:35:38.980 回答
0

对于那些仍在寻找更新答案的人,请点击此处:

文档中,有一节介绍了访问添加的数据文件
这是它的简短和甜蜜。


您需要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!

参考:

于 2020-12-11T10:14:22.957 回答