python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

07-21 1256阅读

今天给大家介绍一种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信息。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

操作步骤

接下来就不废话了,直接上操作步骤。

1,下载pystand项目中的pystand程序,这是一个壳,用来调用python解释器运行代码。

pystand项目

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

只需要看这两个就行,一个是py38-x64,一个是py38,分别对应64位解释器和32位解释器,同时也对应了64位的pystand及32位的pystand,请注意这俩一定要对应上。不能用32位的pystand去运行64位的python解释器。

2,本文以64位为例,下载PyStand-py38-x64.7z。下载到本地后,它应该长这样。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

解释一下,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同级目录下。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

 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体积过大的问题。

接下来就是本文打包姿势所表演的时候了。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

7,查看打包体积,足足有338M,这太大了。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

优化第一步,删除不需要的固有第三方库,请删除sitepackage文件夹下的pip,pip-info,wheel,wheel-info,setuptools,setuptools-info,合计6个文件夹,这大概是15M。删除rruntime文件夹里的get-pip.py文件,这文件居然有2M,是用来安装pip的,不需要它。

优化第二步,使用神秘脚本删除程序运行不需要的第三方库文件。

请到下面的链接获取,如果觉得好用,请大力宣传哦!

打包瘦身脚本

请仔细阅读该脚本使用说明,本文仅展示瘦身效果。

瘦身前:

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

瘦身后:

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

从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%。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

此时基本已经实现了较大的瘦身效果,下面的步骤收益不大了,感兴趣请继续往下看。

优化第四步,使用批处理命令命令依次实现以下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%。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

优化第五步,这一步就不演示了,可以使用7z.sfx/BoxedApp Packer 2020将程序制作为单文件程序,可以使用in nosetup将程序制作为安装包,或直接7z压缩发给用户。使用7z压缩后,体积大概为36M左右。制作为单文件程序体积大概为36M,这个时候就可以跟pyinstaller的单文件模式对比一下了,绝对是秒杀。但是我绝对不推荐将程序打包发布为单文件程序,你看看你电脑上哪个大型软件是一个文件呢?

单文件制作工具获取

链接:https://pan.baidu.com/s/1o9jE9SJ9ecBzBT728-tQtA?pwd=vecq 提取码:vecq 复制这段内容后打开百度网盘手机App,操作更方便哦

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

群友反馈

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

加密源码

按照本文所推荐的打包方式,源码其实是放在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文件安全性的回复

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

当然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嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

视频讲解

python嵌入式打包_哔哩哔哩_bilibili

总结

本文实际上是对嵌入式打包这种打包方式的进一步完善,原来流行的嵌入式打包方法,其打包后sitepackage文件夹体积过于庞大,本文结合了瘦身工具,使得打包后的体积与pyinstaller/nuitka相比也毫不逊色,同时拥有了更强的性能与更快的启动速度。熟练后,打包速度也非常快。

下面简单总结了几种打包方式的优缺点,欢迎讨论。

python嵌入式打包,打包新姿势!打包速度比pyinstaller还快哦

总算写完了,下一篇文章打算讲讲嵌入式打包相对于pyinstaller打包,nuitka打包的优点,这里先卖个关子。只能用遥遥领先来形容。

附录

有打包相关问题咨询的可以加群,群里面大佬甚多

注意:

新群友进群后,按下面格式做自我介绍

比如:

版面:Python与模具(您的知乎号,B站,或者公众号,可以为无,方便交流)

行业:营销,模具,广告

模块:Sanic,PyQt,爬虫,后端,全栈

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]