python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦
今天给大家介绍一种python程序的打包方式,使用python嵌入式解释器来打包程序。
本打包姿势融合了较多本人的理解,使得其最终实现的效果优于常规的pyinstaller及nuitka。
写在前面,本文充分借鉴了韦易笑大佬的文章,非常感谢大佬的分享。
怎么样打包 pyqt 应用才是最佳方案?或者说 pyqt 怎样的发布方式最优?
首先需要介绍,什么是嵌入式打包?
嵌入式打包指的是使用python官方提供的python embeddable解释器,来打包一个独立的python环境。
注意:本打包方法仅适用于python3.5版本及以上。
示例程序
接下来介绍本文所用的示例程序,为了简单,本文仅使用numpy生成数据,pandas处理数据,matplotlib绘图,pyqt5显示结果,都是非常常见的第三方依赖。
import sys import numpy as np import pandas as pd from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class MplCanvas(FigureCanvas): """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" def __init__(self, parent=None, width=5, height=4, dpi=100): fig = Figure(figsize=(width, height), dpi=dpi) self.axes = fig.add_subplot() # 初始化父类 super(MplCanvas, self).__init__(fig) class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() # 创建MplCanvas实例作为窗口中的中央部件 self.canvas = MplCanvas(self, width=5, height=4, dpi=100) # 生成数据并绘制 x_values = np.linspace(0, 2 * np.pi, num=200) y_values = np.sin(x_values) df = pd.DataFrame({'x': x_values, 'sin(x)': y_values}) self.canvas.axes.plot(x_values, y_values, label='Sine Function') # 设置图表标题、坐标轴标签等 self.canvas.axes.set_xlabel('X axis (x)') self.canvas.axes.set_ylabel('Y axis(sin(x))') self.canvas.axes.set_title('y = sin(x)') self.canvas.axes.legend() # 布局管理 layout = QVBoxLayout() layout.addWidget(self.canvas) self.text=QLabel(str(df.info)) layout.addWidget(self.text) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) if __name__ == '__main__': app = QApplication(sys.argv) main_win = MainWindow() main_win.show() sys.exit(app.exec_())
程序运行结果如下,包含个曲线图以及一段pandas的dataframe信息。
操作步骤
接下来就不废话了,直接上操作步骤。
1,下载pystand项目中的pystand程序,这是一个壳,用来调用python解释器运行代码。
pystand项目
只需要看这两个就行,一个是py38-x64,一个是py38,分别对应64位解释器和32位解释器,同时也对应了64位的pystand及32位的pystand,请注意这俩一定要对应上。不能用32位的pystand去运行64位的python解释器。
2,本文以64位为例,下载PyStand-py38-x64.7z。下载到本地后,它应该长这样。
解释一下,runtime文件夹里面放python embeddable解释器,这里面自带的是python3.8.10。
site-packages里面放第三方库。
pystand.exe是一个c语言写的壳,双击即可运行pystand.int文件中的代码。
pystand.int中保存的是python代码,你可以理解为main.py文件,这里需要改为程序的入口,后面会详细介绍。
你可以选择将runtime文件夹里面的内容删掉,自己去官网下载其他版本的python embeddable解释器,我这里以python 3.10为例,去官网下载python-3.10.5-embed-amd64.zip,并替换runtime文件夹中的文件。ok,现在python解释器已经配置完毕。
3,将你的项目所依赖的虚拟环境下的sitepackage文件夹下所有内容copy到pystand的sitepackage文件夹下,请注意,如果你的项目所依赖的虚拟环境并不是特别的干净,比如还有项目根本没用到的scipy库等,我建议你重新新建虚拟环境,安装好依赖后拷贝。
4,在pystand目录下,新建script目录,将程序代码拷贝到里面去。并对代码入口做简要修改,注释掉 if __name__ == '__main__':,新建入口函数,名称随意,这里以start为例。
def start(): app = QApplication(sys.argv) main_win = MainWindow() main_win.show() sys.exit(app.exec_()) # if __name__ == '__main__': # app = QApplication(sys.argv) # main_win = MainWindow() # main_win.show() # sys.exit(app.exec_())
5,修改pystand.int中的代码,以文本编辑器打开该文件,直接清空内容,输入以下代码。这样就表示,从start函数启动程序。
但是第4步我们将main.py文件扔到了scripts文件夹下,程序是不知道main.py文件在哪儿的,所以会报错:
from main import start.
ModuleNotFoundError: no module named "main"
from main import start if __name__ == "__main__": #运行入口函数 start()
所以我们需要告诉一下python,你需要去scripts目录找main模块,这里利用.pth文件。
新建main.txt文本文件(名称随意),里面写上scripts,保存,修改后缀名为.pth,将该文件放在pystand.exe同级目录下。
tips:你要是觉得放在pystand.exe同级目录显得很乱,你可以将新建的main.txt放在其他地方,里面的路径也可以写其他相对路径,如./scripts,../scripts等,这一块就不赘述了,自己问chatai吧。
6,双击pystand.exe,运行程序,可得到如期结果。到这儿,打包算初步完成。
!如果不能如期运行,可以使用cmd命令运行pystand.exe,可以查看到报错信息。
!如果需要自定义程序名字,需要同时修改pystand.exe及pystand.int两个文件,保持同名即可。
!如果觉得保持同名很麻烦,想允许用户自己随便改名,那么请将pystand.int改名为_pystand_static.int即可。
!如果需要自定义程序图标,请使用Resource Hacker 更换图标,非常简单。
!程序源代码是以源码形式放在script目录中的,建议使用nuitka批量将py文件转为pyd文件,提升运行速度的同时隐藏了源码。
!如果有需要,也可以自己编译pystand,修改原代码后彻底去除int文件。
nuitka转pyd命令:
对文件:切换至scripts文件夹下,python -m nuitka --module main.py
对文件夹:假设代码都放在app文件夹下。切换至scripts文件夹下,python -m nuitka --module app --include-package=app
打包到这里就算完成了。
先简要总结下此打包方法的优缺点:
优点:
①熟练后打包速度非常快,只需要简单的复制粘贴即可。
②不怕缺包漏包,嵌入式解释器自带了所有的标准库(在那个zip文件里,通过python310._pth导入),同时咱们拷贝了sitepackge文件夹,不存在pyinstaller及nuitka自动分析依赖所导致的缺文件问题。
③exe外壳用的是pystand,该项目已存续两年,用户很多,因此打包后报毒风险较低。
④软件启动速度,运行速度优于pyinstaller,与nuitka打包的程序也可以简单比比(当然比不上nuitka全编译)。
缺点:
sitepackage文件夹体积过大。
下面的步骤在于解决sitepackage体积过大的问题。
接下来就是本文打包姿势所表演的时候了。
7,查看打包体积,足足有338M,这太大了。
优化第一步,删除不需要的固有第三方库,请删除sitepackage文件夹下的pip,pip-info,wheel,wheel-info,setuptools,setuptools-info,合计6个文件夹,这大概是15M。删除rruntime文件夹里的get-pip.py文件,这文件居然有2M,是用来安装pip的,不需要它。
优化第二步,使用神秘脚本删除程序运行不需要的第三方库文件。
请到下面的链接获取,如果觉得好用,请大力宣传哦!
打包瘦身脚本
请仔细阅读该脚本使用说明,本文仅展示瘦身效果。
瘦身前:
瘦身后:
从338M减小到168M,减小了170M,50%的瘦身效果,删除的文件以原本的目录结构保存在site-packages_new文件夹中,删除的文件清单保存在site-packages_文件移动清单.txt中,可随时查看。
运行程序,程序运行无误,瘦身完美!
如果运行失败,请使用cmd命令运行pystand.exe程序,根据错误提示信息从文件移动清单中寻找缺失到的文件,还原即可。本部分内容请详细阅读脚本使用说明。
优化第三步,使用upx对打包后的文件夹中的dll,pyd,exe文件进行压缩,进一步压缩空间。
tips:使用upx后,并不能减小最终7z压缩包的体积,同时会增大程序运行时占用内存(大概就是翻倍)及增加程序启动时间。
from pathlib import Path import os dir_path = Path("E:\\pystand\\site-packages") upx_path = Path('D:\\ProgramData\\anaconda3\\Scripts\\upx.exe') command = f"{upx_path} --compress-icons=0 --lzma --strip-loadconf " upack = f"{upx_path} -d " exclude_files = ["qwindows.dll","qwindowsvistastyle.dll", "arrow.dll"] extensions_to_find = ('.pyd', '.dll', '.exe') def check_file(file_path: Path) -> bool: for file in exclude_files: if file in str(file_path): return False return True # 遍历目录及其子目录,找到指定后缀名的文件 for ext in extensions_to_find: files = list(dir_path.glob("**/*{}".format(ext))) for file_path in files: if check_file(file_path): upx_command = command + str(file_path) os.system(upx_command) print("upx操作结束")
压缩结束后,查看打包体积,103M,相比一开始的338M,此时瘦身效果已达70%。
此时基本已经实现了较大的瘦身效果,下面的步骤收益不大了,感兴趣请继续往下看。
优化第四步,使用批处理命令命令依次实现以下3个目标操作,对sitepackage文件夹,①将.py文件编译为pyc文件,②删除py文件,③删除__pycache__文件夹。
SET "SOURCE_DIR=E:\pystand\site-packages" SET "PYTHON_PATH=E:\pystand\runtime\python.exe" setlocal enabledelayedexpansion "%PYTHON_PATH%" -m compileall -b "%SOURCE_DIR%" FOR /R "%SOURCE_DIR%" %%i IN (*.py) DO ( echo Deleting file: %%i del "%%i" ) echo All .py files have been deleted. for /r "%SOURCE_DIR%" %%d in (__pycache__) do ( echo Removing directory: %%d rmdir /s /q "%%d" ) echo All __pycache__ directories have been removed. pause
操作完成后,体积减小到84M,此时瘦身效果达75%。
优化第五步,这一步就不演示了,可以使用7z.sfx/BoxedApp Packer 2020将程序制作为单文件程序,可以使用in nosetup将程序制作为安装包,或直接7z压缩发给用户。使用7z压缩后,体积大概为36M左右。制作为单文件程序体积大概为36M,这个时候就可以跟pyinstaller的单文件模式对比一下了,绝对是秒杀。但是我绝对不推荐将程序打包发布为单文件程序,你看看你电脑上哪个大型软件是一个文件呢?
单文件制作工具获取
链接:https://pan.baidu.com/s/1o9jE9SJ9ecBzBT728-tQtA?pwd=vecq 提取码:vecq 复制这段内容后打开百度网盘手机App,操作更方便哦
群友反馈
加密源码
按照本文所推荐的打包方式,源码其实是放在script文件夹中,以python源码的形式存放的。现在我们来简单加密一下,不让别人随随便看见。原理就是把py文件编译为pyd文件,用于替代py文件。
注意:这个只能转换py文件,其余的资源文件或二进制文件保留原路径即可。资源文件路径我建议代码里通过工作路径+相对路径拼接
这里可能有几种情况:
①只有一个py文件,比如本文的main.py文件,该文件目前是放在scripts文件夹下,使用cd命令切换至script文件夹。然后使用nuitka命令
命令如下:
python -m nuitka --module main.py
完事后会生成main.pyd文件,将原来的main.py及__pycache__文件夹(这个文件夹如果有一定要删,里面是pyc文件,可以完美还原源码)删掉。
②只有一个代码文件夹,文件夹包含子文件夹或代码文件,名字假设为app。使用cd命令切换至script文件夹。然后使用nuitka命令
命令如下:
python -m nuitka --module app --include-package=app
完事后会生成app.pyd文件,将原来的app文件夹里面的源码及__pycache__文件夹(这个文件夹如果有一要删,里面是pyc文件,可以完美还原源码)删掉。
③多个同级目录代码文件及代码文件夹
那就只能重复使用上述①②情况下的命令编译,生成多个pyd(模块)文件。
pyd文件比pyc文件安全的多,它类似于dll文件,一般人很难破解,想逆向还原源码,那更是不可能。
但是pyd文件作为一个依赖模块,是可以直接通过dir函数查看其暴露的接口的,所以请将重要变量如key什么的匿名化(不能被import,dir出来)。
这里贴一下nuitka维护者关于pyd文件安全性的回复
当然pyd文件在专业的人手里,那还是有破解的可能,类似于dll文件逆向。如果还想继续加密,可以参考本文开头所引用的韦易笑大佬的文章中的代码加密办法。本人不擅长此道。
常见问题
大部分问题都可以在pystand项目issues页找到答案,下面简要贴出几个常见问题及解决方案。
1,multiprocessing相关代码打包后运行,重复开启多个窗口。
解决方案:
if not hasattr(sys, 'frozen'): sys.frozen = True multiprocessing.freeze_support()
2,目录含中文,报错no QT platform plugin
解决方案:pyside同理
import sys, os import os.path os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = r'.\site-packages\PyQt5\Qt5\plugins' #### 这一行是新增的。用的是相对路径。 from PyQt5.QtCore import * from PyQt5.QtWidgets import * app = QApplication([])
3,打包含tkinter的程序
官方提供的嵌入式解释器并不包含pip工具,以及tkinter模块,下面的步骤是怎么补上这个tkinter模块。
注:本方法目的在于后续可以复用,所以并不按照官方的目录结构来。
①复制tkinter模块。从已经安装了tkinter模块的相同 python解释器环境中复制,该模块通常位于解释器的Lib文件夹下,将tkinter文件夹复制到pystand/runtime/lib文件夹下,注意,这里需要新建一个lib文件夹。
②复制tcl资源文件。复制tcl文件夹里面所有的文件,到pystand/runtime/lib文件夹下,tcl文件夹通常位于解释器的同级目录。
③复制二进制模块。复制_tkinter.pyd,tcl86t.dll,tk86t.dll三个文件到pystand/runtime/lib文件夹下,这三个文件通常位于解释器的DLLs文件夹下。
④修改路径。修改runtime文件夹里面的python310._pth文件,增加一行./lib。注意,这里有个点,代表同级目录的lib文件夹。
现在可以运行程序了。如果后续还需要打包tkinter模块。直接复制这里弄好的lib文件夹跟._pth文件到runtime文件夹下即可实现复用。
视频讲解
python嵌入式打包_哔哩哔哩_bilibili
总结
本文实际上是对嵌入式打包这种打包方式的进一步完善,原来流行的嵌入式打包方法,其打包后sitepackage文件夹体积过于庞大,本文结合了瘦身工具,使得打包后的体积与pyinstaller/nuitka相比也毫不逊色,同时拥有了更强的性能与更快的启动速度。熟练后,打包速度也非常快。
下面简单总结了几种打包方式的优缺点,欢迎讨论。
总算写完了,下一篇文章打算讲讲嵌入式打包相对于pyinstaller打包,nuitka打包的优点,这里先卖个关子。只能用遥遥领先来形容。
附录
有打包相关问题咨询的可以加群,群里面大佬甚多
注意:
新群友进群后,按下面格式做自我介绍
比如:
版面:Python与模具(您的知乎号,B站,或者公众号,可以为无,方便交流)
行业:营销,模具,广告
模块:Sanic,PyQt,爬虫,后端,全栈