使用 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_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上的操作详情见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 无法启动

有没有办法创建从 pyinstaller --onefile 生成的单个可执行 exe 文件的 MSI 包?