PyInstaller,如何包含 pip 安装的外部包中的数据文件?

Posted

技术标签:

【中文标题】PyInstaller,如何包含 pip 安装的外部包中的数据文件?【英文标题】:PyInstaller, how to include data files from an external package that was installed by pip? 【发布时间】:2018-03-10 12:26:47 【问题描述】:

问题

我正在尝试使用 PyInstaller 创建一个供我公司内部使用的应用程序。该脚本在工作 python 环境中运行良好,但在转换为包时会丢失一些东西。

我知道如何在我的包中包含和引用我自己需要的数据文件,但是我在包含或引用导入时应该包含的文件时遇到了问题。

我正在使用一个名为 tk-tools 的 pip 可安装包,其中包括一些用于类似面板的显示器(看起来像 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 是空白的。 Spec 文件,使用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 包。此外,tk_tools 包的某些功能在找到数据文件之前可以完美运行,所以我知道它正在某个地方导入,我只是不知道在哪里。当我搜索tk_tools 时,我在文件结构中找不到对它的引用。

我也尝试了--hidden-imports 选项,结果相同。

部分解决方案

如果我使用datas = [('C:\\_virtualenv\\aspen\\Lib\\site-packages\\tk_tools\\img\\', 'tk_tools\\img\\')]datas=datasAnalysis 中“手动”添加到规范文件的路径,那么一切都会按预期工作。这将起作用,但我宁愿 PyInstaller 找到包数据,因为它已明确安装。我会继续寻找解决方案,但是 - 目前 - 我可能会使用这种不理想的解决方法。

如果您可以控制包...

然后您可以在子包上使用stringify,但这仅适用于您自己的包。

【问题讨论】:

如果我理解正确,除了***.com/a/31966932/4724196 这样的解决方案之外,您还想要tk_tools 依赖静态文件的自动发现方法,对吗? @HassenPy 是的,但我怀疑自动发现已经是 PyInstaller 包的一部分,我只是没有找到正确的语法。您链接的解决方案仍然具有硬编码到 .spec 文件中的静态文件的绝对路径。我的印象是 pip installed 包应该毫不费力地被拉入 PyInstaller,但显然不是这样。感谢您的关注! 【参考方案1】:

以下代码会将您目录中的所有 PNG 文件放入捆绑应用的顶层文件夹imgs

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

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

【讨论】:

路径C:\_code\tools\python\aspen_comm\dist\aspen_comm 存在,但其中没有名为tk_tools 的目录,因此该解决方案目前不可行。感谢您的观看! 我一定是误会了。那么你的图像在哪里? 图片是在tk_tools用pip安装时安装的,所以它们位于python虚拟环境中。我希望自动发现功能会发现文件位于 tk_tools 中,然后将它们拉入。 不,Pyinstaller 不会那样做。如果您可以发布图像所在的文件路径,我可以编辑答案以使其工作,假设您有图像。 我已经这样做并相应地编辑了我的帖子。我知道如何创建安装程序并明确包含路径,我只是希望自动发现会使用包清单并找到静态文件。【参考方案2】:

编辑添加

为了更持久地解决这个问题,我创建了一个名为 stringify 的 pip 可安装包,它将获取文件或目录并将其转换为 python 字符串,以便 pyinstaller 等包将它们识别为原生 python 文件。

查看project page,欢迎反馈!


原答案

答案有点绕,处理的是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

【讨论】:

【参考方案3】:

我利用规范文件是可以执行的 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 的参数。

【讨论】:

【参考方案4】:

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 答案。

【讨论】:

【参考方案5】:

聚会有点晚了,但写了一篇关于我如何做到这一点的帮助文章:

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')

【讨论】:

【参考方案6】:

这是一个使用与提到的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))

【讨论】:

有一个 .txt 文件的依赖项,这很有效,谢谢

以上是关于PyInstaller,如何包含 pip 安装的外部包中的数据文件?的主要内容,如果未能解决你的问题,请参考以下文章

新人小白,安装pip install pyinstaller遇到问题,求大神帮帮忙!

Python PyInstaller安装和使用教程(详解版)

pip安装pyinstaller失败的解决方法

pyinstaller 3.6版本通过pip安装失败的解决办法

Python使用pip报错和安装pyinstaller报错

使用Pyinstaller打包成EXE文件