使用PyInstaller捆绑数据文件(--onefile)


107

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


11
非常感谢!此处的行(a.datas += ...)确实对我有所帮助。有关使用pyinstaller文档会谈COLLECT但未能将文件转换成二进制使用时--onefile
伊戈尔Serebryany

@IgorSerebryany:借调了!我只是有完全相同的问题。
Hubro 2012年

如果我使用菜单栏,则我的.exe崩溃
iacopo 2013年

1
考虑到temp文件夹在执行完成时会消失,因此要检查其中的内容,请在程序
hithwen

有没有一种方法可以使用我似乎在周围看到的Tree()语法?
fatuhoku 2013年

Answers:


152

较新版本的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)

10
我在python 2.7中使用pyinstaller 2 _MEIPASS2,但在envs中却没有,但sys._MEIPASS效果很好,所以+1。我建议:path = getattr(sys, '_MEIPASS', os.getcwd())
kbec

2
+1。感谢那。现在尝试使用_MEIPASS2环境变量已有一段时间,显然我在仍为环境变量的时候编写了代码,因为我记得它可以工作。突然,当我最近重新编译时,它停止了。
Favil Orbedios

8
我建议赶上AttributeError而不是更一般Exception。我遇到了缺少sys导入且路径默认为的问题os.path.abspath
亚伦

您也许可以帮助解决我的问题
菲利普(Phillip)2013年

1
在未设置_MEIPASS的情况下使用当前工作目录是错误的。看我的回答
Jonathon Reinhart

51

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"

10
一个人将如何,何时何地使用它?
gseattle

4
您应该在尝试使用PyInstaller编译的.py文件中使用此脚本。请勿将此代码段放在.spec文件中,否则将不起作用。通过使用通常用键入的路径来访问文件resource_path("file_to_be_accessed.mp3")。请注意,对于当前版本的PyInstaller,应使用max'答案。
Exeleration-G

1
这个答案是特定于使用--one-file选项的吗?
fatuhoku 2013年

您也许可以帮助解决我的问题
菲利普(Phillip)2013年

在未设置_MEIPASS的情况下使用当前工作目录是错误的。看我的回答
Jonathon Reinhart

30

在未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)

谢谢乔恩,您的职能今天帮助我解决了一个问题。我有一个问题。为什么有些人写_MEIPASS2而不是_MEIPASS?当我尝试添加2之前,它不起作用。
艾哈迈德·阿卜杜勒·哈勒克

我认为os.env['_MEIPASS2'](使用OS环境)是获取目录的旧方法。AFAIK sys._MEIPASS是目前唯一正确的方法。
乔纳森·莱因哈特

您好,当resource_path()位于主脚本中时,您的解决方案可以正常工作。当它写在模块中时,有没有办法使它起作用?到目前为止,它将尝试在模块所在的文件夹(而不是主文件夹)中获取资源。
Guimoute

1
@Guimoute听起来像此答案中的代码按设计那样工作:因为它使用,所以将资源本地化到提供它的模块__file__。如果您希望模块能够访问其自身范围之外的资源,则必须实现导入代码为模块提供使用它们的路径,例如,通过模块提供的“ set_resource_location()”调用,导入程序调用-不要认为这与不使用pyinstaller时的工作方式没有什么不同。
Barny

@barny感谢您的提示!我将尝试一些类似的方法。
Guimoute

14

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

  1. 步骤:通过导入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. 步骤:将pyi-makespec file.py写入控制台,以创建file.spec文件。

  3. 步骤:使用记事本++打开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。

编辑:最后,它与乔纳森·莱因哈特(Jonathan Reinhart)的方法一起使用。

  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类,该类在第3步编辑之前已在上面给出。
  6. 步骤:保存并退出路径文件。转到包含spec和py文件的文件夹。再次打开控制台窗口,然后键入以下命令:

        pyinstaller your_file.spec

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


谢谢@dilde!并不是说a.datas第5步中的数组采用了字符串元组,请参阅pythonhosted.org/PyInstaller/spec-files.html#adding-data-files作为参考。
伊恩·坎贝尔

抱歉,由于我的英语不佳,我也无法理解您的部分信息。这部分是“不就是a.datas ......”,你想要写“请注意,a.datas ......”是不是有什么错什么,我写了一篇关于添加到字符串数组a.datas?
dildeolupbiten

糟糕!应该注意的是...是的,对错字感到抱歉。您的步骤对我非常有用:),我无法在他们的文档或其他任何地方找到此信息。
伊恩·坎贝尔

8

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

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

只需在代码的开头添加这两行,其余部分就可以保留。


2
否。好的应用程序应该很少更改工作目录。有更好的方法。
Jonathon Reinhart

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)

在未设置_MEIPASS的情况下使用当前工作目录是错误的。看我的回答
Jonathon Reinhart

1

我在PyInstaller上看到的最常见的投诉/问题是“我的代码找不到我确实包含在捆绑软件中的数据文件,它在哪里?”,而且很难看到您的代码在哪里。正在搜索,因为提取的代码位于临时位置,退出时将其删除。使用@Jonathon Reinhart的代码,添加以下代码以查看 onefile中包含的内容及其所在位置。resource_path()

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

0

我发现现有的答案令人困惑,并且花了很长时间解决问题所在。这是我发现的所有内容的汇总。

运行我的应用程序时,出现错误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)


1
抱歉。但是更改目录永远不是正确的答案。如果用户提供了应用程序的路径,则他们希望该路径是相对于当前目录的。如果您更改它,那将是错误的。
Jonathon Reinhart

0

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

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

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

希望能有所帮助。


0

我已经(长时间)处理这个问题了。我搜索了几乎所有的资源,但事情并没有在我的脑海中浮现。

最后,我想我想出了我想分享的确切步骤。

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

如何创建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_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创建用于编译和构建过程的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

就是这样,您可以mainproject_folder/dist任何位置运行,而无需在其文件夹中添加任何其他内容。您只能分发该main文件。现在是真正的独立


@JonathonReinhart(如果您还在那里,我想告诉您我在回答中使用了您的功能)。
muyustan

0

使用Max本文中有关添加额外的数据文件(如图像或声音)以及我自己的研究/测试的出色答案,我弄清楚了我认为添加此类文件的最简单方法。

如果你想看到一个活生生的例子,我的仓库是这里 GitHub上。

注意:这是用于在pyinstaller中使用--onefileor -F命令进行编译。

我的环境如下。


分两步解决问题

为了解决该问题,我们需要专门告诉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

在之下a = Analysis([main...,我设置了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"

0

另一个解决方案是制作一个运行时挂钩,该挂钩将把您的数据(文件/文件夹)复制(或移动)到存储可执行文件的目录中。挂钩是一个简单的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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.