簡體   English   中英

PyInstaller,如何包含來自 pip 安裝的外部 package 的數據文件?

[英]PyInstaller, how to include data files from an external package that was installed by pip?

問題

我正在嘗試使用 PyInstaller 創建一個供我公司內部使用的應用程序。 該腳本在工作 python 環境中運行良好,但在轉換為 package 時會丟失一些東西。

我知道如何在我的 package 中包含和引用我自己需要的數據文件,但是我在包含或引用導入時應該進入的文件時遇到了麻煩。

我正在使用一個名為tk-tools的 pip 可安裝 package ,其中包括一些用於類似面板的顯示器的漂亮圖像(看起來像 LED)。 問題是,當我創建 pyinstaller 腳本時,只要引用其中一個圖像,就會出現錯誤:

DEBUG:aspen_comm.display:COM23 19200
INFO:aspen_comm.display:adding pump 1 to the pump list: [1]
DEBUG:aspen_comm.display:updating interrogation list: [1]
Exception in Tkinter callback
Traceback (most recent call last):
  File "tkinter\__init__.py", line 1550, in __call__
  File "aspen_comm\display.py", line 206, in add
  File "aspen_comm\display.py", line 121, in add
  File "aspen_comm\display.py", line 271, in __init__
  File "aspen_comm\display.py", line 311, in __init__
  File "lib\site-packages\tk_tools\visual.py", line 277, in __init__
  File "lib\site-packages\tk_tools\visual.py", line 289, in to_grey
  File "lib\site-packages\tk_tools\visual.py", line 284, in _load_new
  File "tkinter\__init__.py", line 3394, in __init__
  File "tkinter\__init__.py", line 3350, in __init__
_tkinter.TclError: couldn't open "C:\_code\tools\python\aspen_comm\dist\aspen_comm\tk_tools\img/led-grey.png": no such file or directory

我查看了最后一行的那個目錄——這是我的發行版所在的位置——發現不存在tk_tools目錄。

問題

如何讓 pyinstaller 收集導入包的數據文件?

規格文件

目前,我的datas是空白的。 使用pyinstaller -n aspen_comm aspen_comm/__main__.py創建的規范文件:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['aspen_comm\\__main__.py'],
             pathex=['C:\\_code\\tools\\python\\aspen_comm'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)

pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='aspen_comm',
          debug=False,
          strip=False,
          upx=True,
          console=True )

coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name='aspen_comm')

當我查看/build/aspen_comm/out00-Analysis.toc/build/aspen_comm/out00-PYZ.toc時,我發現一個條目看起來像是找到了tk_tools package。 此外, tk_tools package 的一些功能在找到數據文件之前可以完美運行,所以我知道它正在某個地方導入,我只是不知道在哪里。 當我搜索tk_tools時,我在文件結構中找不到對它的引用。

我也嘗試了--hidden-imports選項,結果相同。

部分解決方案

如果我“手動”使用datas = [('C:\\_virtualenv\\aspen\\Lib\\site-packages\\tk_tools\\img\\', 'tk_tools\\img\\')]datas=datas中的Analysis ,然后一切都按預期工作。 這將起作用,但我寧願 PyInstaller 找到 package 數據,因為它已明確安裝。 我將繼續尋找解決方案,但是 - 目前 - 我可能會使用這種非理想的解決方法。

如果您可以控制 package...

然后您可以在子包上使用stringify ,但這僅適用於您自己的 package。

我通過利用規范文件是執行的 Python 代碼這一事實解決了這個問題。 您可以在 PyInstaller 構建階段動態獲取包的根目錄,並在datas列表中使用該值。 就我而言,我的.spec文件中有這樣的內容:

import os
import importlib

package_imports = [['package_name', ['file0', 'file1']]

datas = []
for package, files in package_imports:
    proot = os.path.dirname(importlib.import_module(package).__file__)
    datas.extend((os.path.join(proot, f), package) for f in files)

並將結果datas列表用作Analysis的參數。

這是一個使用與Turn提到的相同想法的單線 就我而言,我需要一個位於 kivy_garden 中的包 (zbarcam)。 但我試圖在這里概括這個過程。

from os.path import join, dirname, abspath, split
from os import sep
import glob
import <package>

pkg_dir = split(<package>.__file__)[0]
pkg_data = []
pkg_data.extend((file, dirname(file).split("site-packages")[1]) for file in glob.iglob(join(pkg_dir,"**{}*".format(sep)), recursive=True))

以下代碼會將您目錄中的所有 PNG 文件放入名為imgs的捆綁應用程序頂層的文件夾中:

datas=[("C:\\_code\\tools\\python\\aspen_comm\\dist\\aspen_comm\\tk_tools\\img\\*.png", "imgs")],

然后,您可以在代碼中使用os.path.join("imgs", "your_image.png")引用它們。

編輯添加

為了更永久地解決這個問題,我創建了一個名為stringify的 pip 可安裝包,它將獲取一個文件或目錄並將其轉換為 python 字符串,以便 pyinstaller 等包將它們識別為本地 python 文件。

查看項目頁面,歡迎反饋!


原答案

答案有點迂回,涉及tk_tools的打包方式,而不是 pyinstaller。

最近有人讓我知道了一種技術,其中可以將二進制數據(例如圖像數據)存儲為base64字符串:

with open(img_path, 'rb') as f:
    encoded_string = base64.encode(f.read())

編碼后的字符串實際上存儲了數據。 如果原始包只是將包文件存儲為字符串而不是圖像文件,並使用可作為字符串變量訪問的數據創建一個 python 文件,那么可以簡單地以pyinstaller將找到的形式在包中包含二進制數據並且無需干預即可檢測。

考慮以下函數:

def create_image_string(img_path):
    """
    creates the base64 encoded string from the image path 
    and returns the (filename, data) as a tuple
    """

    with open(img_path, 'rb') as f:
        encoded_string = base64.b64encode(f.read())

    file_name = os.path.basename(img_path).split('.')[0]
    file_name = file_name.replace('-', '_')

    return file_name, encoded_string


def archive_image_files():
    """
    Reads all files in 'images' directory and saves them as
    encoded strings accessible as python variables.  The image
    at images/my_image.png can now be found in tk_tools/images.py
    with a variable name of my_image
    """

    destination_path = "tk_tools"
    py_file = ''

    for root, dirs, files in os.walk("images"):
        for name in files:
            img_path = os.path.join(root, name)
            file_name, file_string = create_image_string(img_path)

            py_file += '{} = {}\n'.format(file_name, file_string)

    py_file += '\n'

    with open(os.path.join(destination_path, 'images.py'), 'w') as f:
        f.write(py_file)

如果archive_image_files()放置在安裝文件中,則<package_name>/images.py會在安裝腳本運行時(在創建和安裝輪子期間)自動創建。

我可能會在不久的將來改進這項技術。 謝謝大家的幫助,

j

Mac OS X 10.7.5 Pyinstaller; 使用 1 行代碼而不是單獨的行為 app.spec 文件中的每個圖像添加圖像。 這是我用來讓我的圖像與我的腳本一起編譯的所有代碼。 將此函數添加到:yourappname.py 的頂部:

# Path to the resources, (pictures and files) needed within this program 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)`  

此外,在 appname.py 腳本中,添加此“resource_path”以從資源中獲取圖像,如下所示:

yourimage = PhotoImage(file=resource_path("yourimage.png"))

在您的 appname.spec 文件中,用您想要使用的圖像路徑替換 'datas=[]。 我只使用了 '*.png' 圖像文件,這對我有用:

datas=[("/Users/rodman/PycharmProjects/tkinter/*.png", ".")],

確保將 /Users/rodman/PycharmProjects/tkinter/ 替換為您的圖像所在文件夾的路徑。 原諒草率的代碼格式,我不習慣這些代碼標簽,感謝 Steampunkery,讓我朝着正確的方向前進,以找出這個 Mac os x 答案。

參加聚會有點晚,但寫了一篇關於我如何做到這一點的幫助文章:

https://www.linkedin.com/pulse/loading-external-module-data-during-pyinstaller-bundling-deguzis/?published=t

片段:

import os
import pkgutil
import PyInstaller.__main__
import platform
import shutil
import sys


# Get mypkg data not imported automagically
# Pre-create location where data is expected
if not os.path.exists('ext_module'):
    os.mkdir('ext_module')

with open ('ext_module' + os.sep + 'some-env.ini', 'w+') as f:
    data = pkgutil.get_data( 'ext_module', 'some-env.ini' ).decode('utf-8', 'ascii')
    f.write(data)

# Set terminator (PyInstaller does not provide an easy method for this)
# ':' for OS X / Linux
# ';' for Windows
if platform.system() == 'Windows':
    term = ';'
else:
    term = ':'


PyInstaller.__main__.run([
        '--name=%s' % 'mypkg',
        '--onefile',
        '--add-data=%s%smypkg' % (os.path.join('mypkg' + os.sep + 'some-env.ini'),term),
        os.path.join('cli.py'),
    ])


# Cleanup
shutil.rmtree('mypkg')

可能在詢問此問題后添加了此選項,但 PyInstaller 提供了一些有助於的命令行選項

--collect-submodules MODULENAME

 Collect all submodules from the specified package or module. This option can be used multiple times.

--collect-data MODULENAME,--collect-datas MODULENAME

 Collect all data from the specified package or module. This option can be used multiple times.

--collect-binaries MODULENAME

 Collect all binaries from the specified package or module. This option can be used multiple times.

--collect-all MODULENAME

 Collect all submodules, data files, and binaries from the specified package or module. This option can be used multiple times.

我只是簡單的使用了外部package的名字作為MODULENAME

暫無
暫無

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

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