简体   繁体   English

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

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

Problem问题

I am attempting to use PyInstaller to create an application for internal use within my company.我正在尝试使用 PyInstaller 创建一个供我公司内部使用的应用程序。 The script works great from a working python environment, but loses something when translated to a package.该脚本在工作 python 环境中运行良好,但在转换为 package 时会丢失一些东西。

I know how to include and reference data files that I myself need within my package, but I am having trouble including or referencing files that should come in when imported.我知道如何在我的 package 中包含和引用我自己需要的数据文件,但是我在包含或引用导入时应该进入的文件时遇到了麻烦。

I am using a pip-installable package called tk-tools , which includes some nice images for panel-like displays (looks like LEDs).我正在使用一个名为tk-tools的 pip 可安装 package ,其中包括一些用于类似面板的显示器的漂亮图像(看起来像 LED)。 The problem is that when I create a pyinstaller script, any time that one of those images is referenced, I get an error:问题是,当我创建 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

I looked within that directory in the last line - which is where my distribution is located - and found that there is no tk_tools directory present.我查看了最后一行的那个目录——这是我的发行版所在的位置——发现不存在tk_tools目录。

Question问题

How to I get pyinstaller to collect the data files of imported packages?如何让 pyinstaller 收集导入包的数据文件?

Spec File规格文件

Currently, my datas is blank.目前,我的datas是空白的。 Spec file, created with pyinstaller -n aspen_comm aspen_comm/__main__.py :使用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')

When I look within /build/aspen_comm/out00-Analysis.toc and /build/aspen_comm/out00-PYZ.toc , I find an entry that looks like it found the tk_tools package.当我查看/build/aspen_comm/out00-Analysis.toc/build/aspen_comm/out00-PYZ.toc时,我发现一个条目看起来像是找到了tk_tools package。 Additionally, there are features of the tk_tools package that work perfectly before getting to the point of finding data files, so I know that it is getting imported somewhere, I just don't know where.此外, tk_tools package 的一些功能在找到数据文件之前可以完美运行,所以我知道它正在某个地方导入,我只是不知道在哪里。 When I do searches for tk_tools , I can find no reference to it within the file structure.当我搜索tk_tools时,我在文件结构中找不到对它的引用。

I have also tried the --hidden-imports option with the same results.我也尝试了--hidden-imports选项,结果相同。

Partial Solution部分解决方案

If I 'manually' add the path to the spec file using datas = [('C:\\_virtualenv\\aspen\\Lib\\site-packages\\tk_tools\\img\\', 'tk_tools\\img\\')] and datas=datas in the Analysis , then all works as expected.如果我“手动”使用datas = [('C:\\_virtualenv\\aspen\\Lib\\site-packages\\tk_tools\\img\\', 'tk_tools\\img\\')]datas=datas中的Analysis ,然后一切都按预期工作。 This will work, but I would rather PyInstaller find the package data since it is clearly installed.这将起作用,但我宁愿 PyInstaller 找到 package 数据,因为它已明确安装。 I will keep looking for a solution, but - for the moment - I will probably use this non-ideal workaround.我将继续寻找解决方案,但是 - 目前 - 我可能会使用这种非理想的解决方法。

If you have control of the package...如果您可以控制 package...

Then you can use stringify on the subpackage, but this only works if it is your own package.然后您可以在子包上使用stringify ,但这仅适用于您自己的 package。

I solved this by taking advantage of the fact that the spec file is Python code that gets executed.我通过利用规范文件是执行的 Python 代码这一事实解决了这个问题。 You can get the root of the package dynamically during the PyInstaller build phase and use that value in the datas list.您可以在 PyInstaller 构建阶段动态获取包的根目录,并在datas列表中使用该值。 In my case I have something like this in my .spec file:就我而言,我的.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)

And use the the resulting datas list as a parameters to Analysis .并将结果datas列表用作Analysis的参数。

Here's a one-liner using the same idea as Turn mentioned.这是一个使用与Turn提到的相同想法的单线 In my case I needed a package (zbarcam) that was inside of kivy_garden.就我而言,我需要一个位于 kivy_garden 中的包 (zbarcam)。 But I tried to generalize the process here.但我试图在这里概括这个过程。

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

The following code will put all the PNG files in your directory into a folder in the top level of the bundled app called imgs :以下代码会将您目录中的所有 PNG 文件放入名为imgs的捆绑应用程序顶层的文件夹中:

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

You can then reference them with os.path.join("imgs", "your_image.png") in your code.然后,您可以在代码中使用os.path.join("imgs", "your_image.png")引用它们。

Edited to Add编辑添加

To solve this problem more permanently, I created a pip-installable package called stringify which will take a file or directory and convert it into a python string so that packages such as pyinstaller will recognize them as native python files.为了更永久地解决这个问题,我创建了一个名为stringify的 pip 可安装包,它将获取一个文件或目录并将其转换为 python 字符串,以便 pyinstaller 等包将它们识别为本地 python 文件。

Check out the project page , feedback is welcome!查看项目页面,欢迎反馈!


Original Answer原答案

The answer is a bit roundabout and deals with the way that tk_tools is packaged rather than pyinstaller.答案有点迂回,涉及tk_tools的打包方式,而不是 pyinstaller。

Someone recently made me aware of a technique in which binary data - such as image data - could be stored as a base64 string:最近有人让我知道了一种技术,其中可以将二进制数据(例如图像数据)存储为base64字符串:

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

The encoded string actually stores the data.编码后的字符串实际上存储了数据。 If the originating package simply stores the package files as strings instead of as image files and creates a python file with that data accessible as a string variable, then it is possible to simply include the binary data within the package in a form that pyinstaller will find and detect without intervention.如果原始包只是将包文件存储为字符串而不是图像文件,并使用可作为字符串变量访问的数据创建一个 python 文件,那么可以简单地以pyinstaller将找到的形式在包中包含二进制数据并且无需干预即可检测。

Consider the below functions:考虑以下函数:

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)

If archive_image_files() is placed within the setup file, then the <package_name>/images.py is automatically created anytime the setup script is run (during wheel creation and installation).如果archive_image_files()放置在安装文件中,则<package_name>/images.py会在安装脚本运行时(在创建和安装轮子期间)自动创建。

I may improve on this technique in the near future.我可能会在不久的将来改进这项技术。 Thank you all for your assistance,谢谢大家的帮助,

j j

Mac OS X 10.7.5 Pyinstaller; Mac OS X 10.7.5 Pyinstaller; adding images using 1 line of code, instead of individual lines, for each image in the app.spec file.使用 1 行代码而不是单独的行为 app.spec 文件中的每个图像添加图像。 This is all the code I used to get my images to compile with my script.这是我用来让我的图像与我的脚本一起编译的所有代码。 Add this function to the top of: yourappname.py:将此函数添加到: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: # 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)`  

Also, in the appname.py script, add this 'resource_path' to get your images from your resource, like this:此外,在 appname.py 脚本中,添加此“resource_path”以从资源中获取图像,如下所示:

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

In your appname.spec file replace 'datas=[], with your pathway to the images, you want to use.在您的 appname.spec 文件中,用您想要使用的图像路径替换 'datas=[]。 I used only '*.png' image files and this worked for me:我只使用了 '*.png' 图像文件,这对我有用:

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

Make sure you replace /Users/rodman/PycharmProjects/tkinter/ with your path, to the folder where your images are located.确保将 /Users/rodman/PycharmProjects/tkinter/ 替换为您的图像所在文件夹的路径。 Forgive the sloppy code formmating, I am not used to these code tags, and thanks Steampunkery, for stearring me in the right direction to figure this Mac os x answer.原谅草率的代码格式,我不习惯这些代码标签,感谢 Steampunkery,让我朝着正确的方向前进,以找出这个 Mac os x 答案。

A bit late to the party, but wrote a help article on how I did this:参加聚会有点晚,但写了一篇关于我如何做到这一点的帮助文章:

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

Snippet:片段:

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

Possibly this option was added after this question was asked, but PyInstaller provides a few command line options that helped:可能在询问此问题后添加了此选项,但 PyInstaller 提供了一些有助于的命令行选项

--collect-submodules MODULENAME --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-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-binaries MODULENAME

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

--collect-all MODULENAME --collect-all MODULENAME

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

I simply used the name of the external package as MODULENAME我只是简单的使用了外部package的名字作为MODULENAME

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM