小记

您所在的位置:网站首页 pycharm怎么打包项目发给别人 小记

小记

2024-07-08 20:41| 来源: 网络整理| 查看: 265

PyInstaller 可以将 Python 项目打包成一个可执行文件,或是一个文件夹,包含可执行文件以及依赖包。方便我们将 Python 项目交付给用户,方便用户使用的同时也可以一定程度的保护项目源代码。本文将介绍如何简单使用 PyInstaller 打包。

安装

使用 pip 安装即可:

pip install pyinstaller 简单使用

让我们新建一个项目命名为 miniapp,文件结构如下:

miniapp app __init__.py app.py

其中项目核心文件为 app.py,内容如下:

def run(): print('欢迎使用 miniapp') if __name__ == '__main__': run()

现在,进入到项目根目录 miniapp,运行如下命令:

pyinstaller -D app/app.py -n miniapp --clean

初次运行可能会出现下面的错误:

OSError: Python library not found: libpython3.7.so.1.0, libpython3.7mu.so.1.0, libpython3.7m.so, libpython3.7.so, libpython3.7m.so.1.0 This means your Python installation does not come with proper shared library files. This usually happens due to missing development package, or unsuitable build parameters of the Python installation. * On Debian/Ubuntu, you need to install Python development packages: * apt-get install python3-dev * apt-get install python-dev * If you are building Python by yourself, rebuild with `--enable-shared` (or, `--enable-framework` on macOS).

这是因为 PyInstaller 需要 python-dev 环境,如果是使用 pyenv 可以用以下命令重新安装 Python:

env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.5.0

或者,可以安装 python-devel 版本,比如在 CentOS 上可以通过下面的命令查看可用版本:

# yum search python | grep devel ... python3-devel.i686 : Libraries and header files needed for Python development ...

安装 python3-devel.i686 即可。

成功运行后目录下会多出来一些文件,现在的结构如下:

miniapp app __init__.py app.py build miniapp ... dist miniapp _internal ... miniapp miniapp.spec

其中 dist/miniapp/miniapp 就是打包出来的可执行文件了,我们现在可以将 dist/miniapp 整个文件夹压缩并打包交付给用户。用户拿到后解压并运行 miniapp 文件即可。以下是运行后的结果

# ./dist/miniapp/miniapp 欢迎使用 miniapp

也可以通过指定 -F 参数将依赖包与项目文件打成一个文件:

pyinstaller -F app/app.py -n miniapp --clean

此时可执行文件路径为:

# ./dist/miniapp 欢迎使用 miniapp

相对与单个文件的形式,单个文件夹在启动时会更快,而且在后期更新时,在没有依赖更新的情况下,仅需要交给客户项目文件即可,可以一定程度减少文件传输。

打包模块

一般来说,我们的项目很少会只有一个文件。现在,让我们为 miniapp 创建一个新的文件 core.py,作为核心逻辑的存放位置。现在的目录结构如下:

miniapp app __init__.py app.py core.py ...

core.py 文件内容为:

def hello(): print('这里是核心文件')

通常情况下,如果我们在 app.py 中有导入 app.core 的话,PyInstaller 会在打包时将 core.py 也一同编译进去,但若是涉及到动态导入的话,则 core.py 文件会缺失导致导入失败,比如现在将 app.py 逻辑更新如下:

import importlib def run(): print('欢迎使用 miniapp') core = importlib.import_module('app.core') core.hello() if __name__ == '__main__': run()

再次打包并运行,会报错找不到模块 app:

# pyinstaller -D app/app.py -n miniapp --clean # ./dist/miniapp/miniapp Traceback (most recent call last): File "app/app.py", line 39, in run() File "app/app.py", line 34, in run core = importlib.import_module('app.core') File "importlib/__init__.py", line 127, in import_module File "", line 1030, in _gcd_import File "", line 1007, in _find_and_load File "", line 972, in _find_and_load_unlocked File "", line 228, in _call_with_frames_removed File "", line 1030, in _gcd_import File "", line 1007, in _find_and_load File "", line 984, in _find_and_load_unlocked ModuleNotFoundError: No module named 'app' [15561] Failed to execute script 'app' due to unhandled exception!

可以通过指定 --hiddenimport 参数告诉 PyInstaller app 模块需要被一块打包,通过这样的方式再次打包后就可以正常运行了:

pyinstaller -D app/app.py -y -n miniapp --clean --hiddenimport app.core 使用 hooks

如果只有一个 app.core 是动态导入,通过传入 --hiddenimport 即可,如果项目中存在多个模块或依赖库存在动态导入,那么命令行的参数只会越来越长变得难以阅读,为此 PyInstaller 提供了 hook 模式可以便于我们通过 Python 文件维护这些动态导入的模块。

下面效仿 django 为项目添加一个 backends 模块,分别提供了对 MySQL 和 Oracle 数据库的支持,现在项目结构如下:

miniapp app __init__.py app.py core.py backends __init__.py mysql __init__.py oracle __init__.py ...

app.py 中增加对支持的数据库的加载并打印到屏幕:

import importlib import os from pkgutil import iter_modules support_backends = () def load_support_backends(): global support_backends backends = importlib.import_module('app.backends') modules = [] modpath = os.path.dirname(backends.__file__) for _, subpath, ispkg in iter_modules([modpath]): if not ispkg: continue module = importlib.import_module('app.backends.' + subpath) modules.append((subpath, module)) support_backends = tuple(modules) def run(): print('欢迎使用 miniapp') core = importlib.import_module('app.core') core.hello() print('正在加载可用的数据库') load_support_backends() print('可用的数据库有: %s' % ', '.join(path for path, _ in support_backends)) if __name__ == '__main__': run()

现在在项目中新增一个文件夹用于管理 hooks,命名为 pyinstallerhooks,项目结构如下:

miniapp app ... pyinstallerhooks ...

在 pyinstallerhooks 下新建一个名为 hook-app.py 的文件,内容如下:

from PyInstaller.utils.hooks import collect_submodules hiddenimports = collect_submodules('app')

然后使用命令重新打包,指定 --additional-hooks-dir 为 pyinstallerhooks 文件夹路径,同时将 --hiddenimport 改为 app:

pyinstaller -D app/app.py -y -n miniapp --clean --hiddenimport app --additional-hooks-dir pyinstallerhooks

再次运行,可以看到不管是 app.backends 还是 app.core 模块都有被正常加载:

# ./dist/miniapp/miniapp 欢迎使用 miniapp 这里是核心文件 正在加载可用的数据库 可用的数据库有: oracle, mysql

如果我们想要提供的包只支持 oracle,那么可以在 pyinstallerhooks 中对 app.backends 模块做更精细的控制。首先修改 hook-app.py 为:

from PyInstaller.utils.hooks import collect_submodules def filter_backends(name): """排除 app.backends 下面的非 oracle 子模块""" if not name.startswith('app.backends'): return True return name.startswith(('app.backends.oracle')) hiddenimports = collect_submodules('app', filter=filter_backends)

如果想对 app.backends.oracle 下面的子模块提供更进一步的控制,可以在 pyinstallerhooks 下面新建一个 hook-app.backends.oracle.py 文件,并写上具体逻辑,PyInstaller 会在打包时自动找到该文件并应用,同理其他模块也是如此。

再次打包并运行,结果如下:

# ./dist/miniapp/miniapp 欢迎使用 miniapp 这里是核心文件 正在加载可用的数据库 可用的数据库有: oracle 添加静态文件

项目增加了一些模板文件需要提供给用户,放在 templates 目录下:

miniapp app __init__.py app.py core.py backends ... templates config.txt ...

那么如何将 templates 下面的文件能被一起打包并被程序识别到呢,可以通过 --add-data 参数将文件放入指定的相对路径,现在将打包命令更改为:

pyinstaller -D app/app.py -y -n miniapp --clean --hiddenimport app --additional-hooks-dir pyinstallerhooks \ --add-data "./app/templates/:./templates"

其中 : 前的 ./app/templates/ 是我们打包时 templates 所在的相对路径,: 后的 ./templates 是期望打包后文件所在的路径。后者的 . 代表了打包后项目运行时的根目录。

将 app.py 文件修改为如下以获取可用的模板文件:

import importlib import os from pkgutil import iter_modules support_backends = () # 获取项目的根路径 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def load_support_backends(): global support_backends backends = importlib.import_module('app.backends') modules = [] modpath = os.path.dirname(backends.__file__) for _, subpath, ispkg in iter_modules([modpath]): if not ispkg: continue module = importlib.import_module('app.backends.' + subpath) modules.append((subpath, module)) support_backends = tuple(modules) def run(): print('欢迎使用 miniapp') core = importlib.import_module('app.core') core.hello() print('正在加载可用的数据库') load_support_backends() print('可用的数据库有: %s' % ', '.join(path for path, _ in support_backends)) templates_path = os.path.join(BASE_DIR, 'templates') print('可用的模板文件有: %s' % ', '.join(os.listdir(templates_path))) if __name__ == '__main__': run()

重新打包并运行,可以看到模板文件已经被正常加载:

# ./dist/miniapp/miniapp 欢迎使用 miniapp 这里是核心文件 正在加载可用的数据库 可用的数据库有: oracle 可用的模板文件有: config.txt 自省

如何判断当前代码是在打包后的环境运行,还是非打包环境运行呢,可以通过如下方式:

import sys if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): print('正在 PyInstaller 打包环境中运行') else: print('正在非打包环境运行')

PyInstaller 会在运行时将打包文件放在 sys._MEIPASS 所指向的路径下,对于以文件夹方式打包的项目,该路径实际上就是 ./dist/miniapp/_internal,而对于单个文件的方式,这个路径实际上指向的是某个临时文件夹路径(比如 Linux 下的 /tmp/..),如果项目需要生成一些需要保留的文件,可以通过参数 --runtime-tmpdir 重新指定该路径。

参考:

更多用法可参考官方文档:PyInstaller Manual — PyInstaller 6.3.0 documentation



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3