Skip to content

Python 打包心得

复制本地路径 | 在线编辑

Nuikta

  1. 优先 Nuikta:python -m nuitka --mingw64 --standalone --output-dir=out --show-progress --enable-plugin=pyqt5 --nofollow-import-to=numpy,cv2,PIL,six,pytweening,pyscreeze,pyrect,pyperclip --windows-disable-console .\main1.py

  2. 某些包使用 Nuikta 会有问题(如Plotly),这个时候有解决方法,但没必要,时间和收益不成正比。老老实实使用 pyinstaller 就行。

Pyinstaller 打包过大

多半是因为在 conda 环境下,并且程序里面有调用 numpy 包(或者其他间接调用了 numpy 的包,如 cv2 等)。进行了一些对比测试后,建议如下:

  1. 不要在 conda 下安装 pyinstaller,即使是 conda install pyinstaller -c conda-forge 也无法解决这个问题。应该在系统原始的 python3 上安装 pyinstaller。貌似是因为 pyinstaller 会根据它自己所在环境寻找 lib 路径,判断哪些包(.so)涵盖进去,所以 conda 环境下某些 lib 会被包进去。如果经常打包,建议直接用一个虚拟机,虽然很笨但是有奇效。
  2. ~~conda 安装 numpy 建议使用 conda install numpy -c conda-forge,主要是因为 conda 中的 numpy 包有使用 mkl 这个矩阵计算库,打包时很有可能会把 mkl 包上。~~ 本条作废:这个问题已经解决了,现在 numpy 的安装默认行为已经不是这样了。

Pyintsaller 优先生成目录

及其不推荐 Pyinstaller 只生成一个文件,这样有更新会相当相当麻烦。而是生成目录,这样可以备份生成的目录(即 _internal),后续有更新操作时,如果包不变的话,那么 _internal 里面内容也不会变,后续直接替换 exe 即可。

这种操作方式下还有如下的几个技巧:

删除目录中没必要的文件

打开 exe 文件,然后对 _internal 目录中的文件进行无脑删除,此时会弹出某些文件被占用的窗口,这些文件跳过处理即可。这样那些没有用的文件就会被删除掉了。

PS:这种可能有风险,有些文件可能是后续操作才会用到(但我很少遇到过),所以先做好备份。

一些常见的无用文件

以下是我遇到的 _internal 中没用到的文件,即可以删除的文件:

  1. tcltcl8tk 里面的文件都可以删除,但这三个文件夹要保留。
  2. PyQt 中的 translation 不用,bin 目录下有些文件不用。

后续包有更新

在更新的时候,还遇到一个问题,就是遇到用了新的包。其实最简单的做法就是只把 _internal 新加的文件放到更新的文件夹中,即:

_internal
   new_file1
   new_file2
new.exe

合成一个压缩包,然后用户解压把里面的文件直接拖进去即可,系统会自动把新增的文件放入到了 _internal 中了。

第一次发布时目录很大

第一次发布的时候,_internal 会有很多文件,而且子文件比较多,这样复制起来很麻烦。我觉得最直接的就是压缩目录,然后写一个 readme.txt 就行了。

如果非要钻牛角尖,想直接无脑被用户使用(不推荐)。可以在程序里面刚开始运行的时候进行检测是否有压缩包,如果有那么就在程序里先解压。此时目录结构为:

bootstrap.py (bootstrap 程序,刚开始会先解压)
main.py (项目文件入口)
...

打包的时候对 bootstrap.py 进行打包,但问题是要启动 bootstrap 也需要必须的文件呀。但是好处是这样 _internal 里面的文件只用放基础的几个就行,如下所示:

_internal (bootstrap 必须要用的文件)
   _tcl_data (空)
   _tk_data (空)
   _socket.pyd
   base_library.zip
   python3.dll
   python312.dll
   select.pyd
_internal.zip (整个项目用到的文件)
my.exe (对 bootstrap 打包生成的 exe)

下面是一个 bootstrap.py 示例,他是会检查同目录下是否有 .notok 文件,如果有则解压,解压成功后删除压缩包。

# bootstrap.py
import os
import sys
import zipfile
import subprocess
from pathlib import Path

if getattr(sys, 'frozen', False): 
    BASE_DIR = Path(sys.executable).parent.absolute()
else:
    BASE_DIR = Path(__file__).parent.absolute() 

INTERNAL_DIR = os.path.join(BASE_DIR, "_internal")
INTERNAL_ZIP = os.path.join(BASE_DIR, "_internal.zip")
NOTOK_MARK = os.path.join(BASE_DIR, ".notok")

def extract_internal():
    print("[bootstrap] extracting _internal.zip ...")
    with zipfile.ZipFile(INTERNAL_ZIP, "r") as zf:
        # 只解压不存在的文件,跳过已有文件
        for member in zf.namelist():
            # 如果是目录,确保目录存在(即使文件被跳过,目录结构也要保持)
            if member.endswith('/'):
                target_dir = os.path.join(BASE_DIR, member)
                os.makedirs(target_dir, exist_ok=True)
                continue

            target_path = os.path.join(BASE_DIR, member)
            # 如果文件已存在,跳过
            if os.path.exists(target_path):
                continue

            # 确保目标目录存在
            target_dir = os.path.dirname(target_path)
            if target_dir:
                os.makedirs(target_dir, exist_ok=True)

            # 解压单个文件
            zf.extract(member, BASE_DIR)

    # 解压成功后删除 NOTOK 标记
    if os.path.exists(NOTOK_MARK):
        os.remove(NOTOK_MARK)
        print("[bootstrap] removed .notok mark")

def remove_internal():
    if os.path.exists(INTERNAL_ZIP):
        os.remove(INTERNAL_ZIP)

def internal_ready():
    # 如果 NOTOK 标记不存在,说明已经解压完成
    return not os.path.exists(NOTOK_MARK)

def relaunch():
    subprocess.Popen([sys.executable])
    sys.exit(0)

def main():
    # Windows DLL 搜索路径(非常重要)
    if hasattr(os, "add_dll_directory") and os.path.isdir(INTERNAL_DIR):
        os.add_dll_directory(INTERNAL_DIR)

    # 如果存在 NOTOK 标记或 _internal.zip,则需要解压
    if os.path.exists(NOTOK_MARK) or os.path.exists(INTERNAL_ZIP):
        if os.path.exists(INTERNAL_ZIP):
            extract_internal()
            remove_internal()
            relaunch()
        else:
            # 如果只有 NOTOK 但没有 zip 文件,删除 NOTOK(可能是误操作)
            os.remove(NOTOK_MARK)
            print("[bootstrap] .notok exists but no _internal.zip, removed .notok")

    # ===== 到这里环境一定 OK =====
    import main      # ← 你的原 main.py
    if hasattr(main, "main"):
        main.main(BASE_DIR)

if __name__ == "__main__":
    main()

Comments