簡體   English   中英

使用 PyInstaller (--onefile) 捆綁數據文件

[英]Bundling data files with PyInstaller (--onefile)

我正在嘗試使用 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 更新,將較新的答案標記為正確。

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

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"

在應用程序不是 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)

也許我錯過了一個步驟或做錯了什么,但是上面的方法沒有將 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. 步驟:使用 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. 步驟:我按照上面的步驟,然后保存了spec文件。 最后打開控制台並寫入, 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類中,這是在第3步編輯之前給出的。
  6. 步驟:保存並退出路徑文件。 轉到包含規范和 py 文件的文件夾。 再次打開控制台窗口並輸入以下命令:

     pyinstaller your_file.spec

6. 步驟之后,您的一個文件就可以使用了。

我沒有按照建議重寫所有路徑代碼,而是更改了工作目錄:

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

只需在代碼的開頭添加這兩行,其余部分就可以了。

我一直在處理這個問題長一段時間(好吧,長時間)。 我幾乎搜索了所有來源,但事情並沒有在我腦海中形成模式。

最后,我想我已經找到了要遵循的確切步驟,我想分享一下。

請注意,我的回答使用了其他人對此問題的回答的信息。

如何創建 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=added_files, datas=[],行更改為datas=added_files,

有關在main.spec完成的操作的詳細信息,請參見此處。

運行pyinstaller --onefile main.spec

就是這樣,您可以從任何地方在project_folder/dist運行main ,而在其文件夾中沒有任何其他內容。 您只能分發該main文件。 現在,一個真正的獨立.

對接受的答案稍作修改。

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)

使用Max這篇關於添加額外數據文件(如圖像或聲音)和我自己的研究/測試的優秀答案,我找到了我認為添加此類文件的最簡單方法。

如果你想看到一個活生生的例子,我的倉庫是這里GitHub上。

注意:這是為了在--onefile使用--onefile-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)

我們會像這樣使用上面的函數,這樣當應用程序作為腳本或 Frozen EXE 運行時,應用程序圖標就會顯示出來。

icon_path = img_resource_path("app/img/app_icon.ico")
root.wm_iconbitmap(icon_path)

下一步是我們需要指示 Pyinstaller 在編譯時在哪里找到額外的文件,以便當應用程序運行時,它們會在臨時目錄中創建。

我們可以通過兩種方式解決這個問題,如文檔中所示,但我個人更喜歡管理我自己的 .spec 文件,這就是我們要做的。

首先,您必須已經有一個 .spec 文件。 就我而言,我能夠通過運行帶有額外參數的pyinstaller來創建我需要的pyinstaller ,您可以在此處找到額外的參數。 因此,我的規范文件可能看起來與您的略有不同,但在我解釋了重要部分后,我將所有文件都發布以供參考。

added_files本質上是一個包含元組的列表,在我的情況下,我只想添加單個圖像,但您可以使用('app/img/*.ico', 'app/img')添加多個 ico、png 或 jpg您也可以像這樣創建另一個元組added_files = [ (), (), ()]以進行多個導入

元組定義你想什么文件或者文件的類型如加在哪里可以找到他們的第一部分。 把它想象成 CTRL+C

元組的第二部分告訴 Pyinstaller,將路徑設為“app/img/”,並將該目錄中的文件與運行 .exe 時創建的任何臨時目錄相對應。 把它想象成 CTRL+V

a = Analysis([main... , 我已經設置了datas=added_files ,最初它曾經是datas=added_files datas=[]但我們希望導入列表被導入,所以我們傳入我們的自定義導入。

你不需要這樣做,除非你想要一個特定的 EXE 圖標,在規范文件的底部,我告訴 Pyinstaller 使用選項icon='app\\\\img\\\\app_icon.ico'為 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"

我在 PyInstaller 上看到的最常見的抱怨/問題是“我的代碼找不到我肯定包含在包中的數據文件,它在哪里?”,並且不容易看到代碼的內容/位置正在搜索,因為提取的代碼位於臨時位置並在退出時被刪除。 使用@Jonathon Reinhart 的resource_path()添加這段代碼以查看您的 onefile 中包含的內容及其位置

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

另一種解決方案是制作一個運行時掛鈎,它將復制(或移動)您的數據(文件/文件夾)到存儲可執行文件的目錄。 hook 是一個簡單的 python 文件,幾乎可以做任何事情,就在你的應用程序執行之前。 為了設置它,您應該使用--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)

我發現現有的答案令人困惑,並花了很長時間才找出問題所在。 這是我找到的所有內容的匯編。

當我運行我的應用程序時,出現錯誤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)

如果您仍然試圖將文件相對於您的可執行文件而不是放在臨時目錄中,您需要自己復制它。 這就是我最終完成它的方式。

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

您在規范文件中添加一個步驟,將文件系統復制到 DISTPATH 變量。

希望有幫助。

我使用這個基於最大解決方案

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並找到將數據文件添加到的文件夾; 即添加到規范文件的元組中的第二個字符串:

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!

參考:

Чтобыскомпилироватьприпомощи自動PY到EXE:Указываемрасположениескрипта,выбираем--onefile(однимфайлом,--windowed(скрытьконсоль),указываемпутькзначку(иконке)img.ico(увасимяфайлавашейиконки, добавляемдополнительныйфайлimg.icon(ввашемслучаефайлвашейиконки),вводимвполеназванияфайлатоимякаквашфайлбудетназыватьсяпослетогокакскомпилируется。УменявпримереонFILE_NAME。

В самом скрипте прописываем путь "dist\\file_name\\img.ico", где file_name это имя вашего будущего exe файла, img.ico имя файла вашей иконки。

icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap("dist\\file_name\\img.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off) icon.addPixmap(QtGui.QPixmap("dist\\file_name \\img.ico"), QtGui.QIcon.Normal, QtGui.QIcon.On)

Итог - получаем файл exe с отображение иконок не зависимо где файл открывается...

最后 ! 基於Luca的解決方案

我最終能夠在 --onefile pyinstaller 的包含目錄中執行文件。

只需使用他發布的方法,您甚至可以將整個目錄名稱輸入文件,它仍然有效。

您需要做的第二件事實際上是在 pyinstaller 中添加您想要的目錄/文件。 只需使用如下參數:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM