PyInstaller,如何从 pip 安装的外部包中包含数据文件?
PyInstaller, how to include data files from an external package that was installed by pip?
问题
我正在尝试使用 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
是空白的。规范文件,使用 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
选项,结果相同。
部分解决方案
如果我 'manually' 在 Analysis
中使用 datas = [('C:\_virtualenv\aspen\Lib\site-packages\tk_tools\img\', 'tk_tools\img\')]
和 datas=datas
添加规范文件的路径,那么一切都会按预期工作。这会起作用,但我宁愿 PyInstaller 找到包数据,因为它已明确安装。我会继续寻找解决方案,但目前 - 我可能会使用这种不理想的解决方法。
如果您可以控制包...
然后你可以在子包上使用stringify,但是这只适用于你自己的包。
以下代码会将您目录中的所有 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 文件。
查看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
会在安装脚本为 运行 时自动创建(在 wheel 创建和安装期间)。
我可能会在不久的将来改进这项技术。谢谢大家的帮助,
j
我利用规范文件是 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
的参数。
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=[], with your pathway to the images, you want to use. I used only '*.png' 图像文件,这对我有用:
datas=[("/Users/rodman/PycharmProjects/tkinter/*.png", ".")],
确保将 /Users/rodman/PycharmProjects/tkinter/ 替换为您的图像所在文件夹的路径。请原谅草率的代码格式,我不习惯这些代码标签,感谢 Steampunkery,让我朝着正确的方向前进来计算这个 Mac os x 答案。
聚会有点晚了,但写了一篇关于我如何做到这一点的帮助文章:
片段:
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')
这是一个使用与 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))
问题
我正在尝试使用 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
是空白的。规范文件,使用 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
选项,结果相同。
部分解决方案
如果我 'manually' 在 Analysis
中使用 datas = [('C:\_virtualenv\aspen\Lib\site-packages\tk_tools\img\', 'tk_tools\img\')]
和 datas=datas
添加规范文件的路径,那么一切都会按预期工作。这会起作用,但我宁愿 PyInstaller 找到包数据,因为它已明确安装。我会继续寻找解决方案,但目前 - 我可能会使用这种不理想的解决方法。
如果您可以控制包...
然后你可以在子包上使用stringify,但是这只适用于你自己的包。
以下代码会将您目录中的所有 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 文件。
查看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
会在安装脚本为 运行 时自动创建(在 wheel 创建和安装期间)。
我可能会在不久的将来改进这项技术。谢谢大家的帮助,
j
我利用规范文件是 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
的参数。
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=[], with your pathway to the images, you want to use. I used only '*.png' 图像文件,这对我有用:
datas=[("/Users/rodman/PycharmProjects/tkinter/*.png", ".")],
确保将 /Users/rodman/PycharmProjects/tkinter/ 替换为您的图像所在文件夹的路径。请原谅草率的代码格式,我不习惯这些代码标签,感谢 Steampunkery,让我朝着正确的方向前进来计算这个 Mac os x 答案。
聚会有点晚了,但写了一篇关于我如何做到这一点的帮助文章:
片段:
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')
这是一个使用与 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))