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

sli*_*led 17 python pyinstaller python-3.5

问题

我正在尝试使用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
Run Code Online (Sandbox Code Playgroud)

我查看了最后一行中的那个目录 - 这是我的发行版所在的位置 - 并且发现没有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')
Run Code Online (Sandbox Code Playgroud)

当我看到中/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=datas在中添加spec文件的路径Analysis,那么所有工作都按预期工作.这将工作,但我宁愿PyInstaller找到包数据,因为它已明确安装.我会继续寻找解决方案,但是 - 目前 - 我可能会使用这种非理想的解决方法.

如果你有控制包...

然后你可以在子包上使用stringify,但这只适用于你自己的包.

Tur*_*urn 7

我通过利用规范文件是执行的 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)
Run Code Online (Sandbox Code Playgroud)

并将结果datas列表用作Analysis.


小智 6

可能这个选项是在提出这个问题后添加的,但是 PyInstaller 提供了一些有用的命令行选项:

--collect-submodules 模块名称

Collect all submodules from the specified package or module. This option can be used multiple times.
Run Code Online (Sandbox Code Playgroud)

--collect-data 模块名称, --collect-datas 模块名称

Collect all data from the specified package or module. This option can be used multiple times.
Run Code Online (Sandbox Code Playgroud)

--collect-binaries 模块名称

Collect all binaries from the specified package or module. This option can be used multiple times.
Run Code Online (Sandbox Code Playgroud)

--collect-all 模块名称

Collect all submodules, data files, and binaries from the specified package or module. This option can be used multiple times.
Run Code Online (Sandbox Code Playgroud)

我只是使用外部包的名称作为MODULENAME


sli*_*led 1

编辑添加

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

查看项目页面,欢迎反馈!


原答案

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

最近有人让我意识到一种可以将二进制数据(例如图像数据)存储为字符串的技术base64

with open(img_path, 'rb') as f:
    encoded_string = base64.encode(f.read())
Run Code Online (Sandbox Code Playgroud)

编码后的字符串实际上存储了数据。如果原始包只是将包文件存储为字符串而不是图像文件,并创建一个 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)
Run Code Online (Sandbox Code Playgroud)

如果archive_image_files()放置在安装文件中,则<package_name>/images.py只要运行安装脚本(在车轮创建和安装期间),就会自动创建 。

我可能会在不久的将来改进这项技术。谢谢大家的帮助,

j