pyinstaller 介绍

文章目录

介绍

参考文档

pyinstaller 是一个将python应用及其依赖库打包为可执行软件的三方库,在没有python开发环境的系统中仍可以直接运行。

  • 支持Windows(win8+), macOS, and Linux,在windows下打包的可执行程序只能在windows下运行,是面向平台的打包;
  • 安装 pip install -U pyinstaller , 或者指定某个版本pyinstaller==6.15.0
  • 打包原理
    • 分析依赖,从py应用的入口程序开始 递归地分析 所有的导入依赖包;
    • 收集依赖,将python解释器、依赖包、py应用项目代码等拷贝到一个目录中,生成一个压缩文件(如.exe);
    • 创建引导程序,创建可执行程序的引导程序;
    • 解压执行,在执行打包后的可执行程序时,默认将压缩文件解压到Temp目录下,并从入口开始执行
  • 简单的打包
    • 命令行进入到项目目录下,pyinstaller main.py
  • 两种打包模式
    • 单个文件,通过参数--onefile, -F指定,生成一个可执行文件,如windows下的exe,所有的项目脚本及依赖包都会打包到一个可执行文件中,启动执行会比单目录模式稍慢;执行该单文件exe时,会默认在Temp目录下解压出所有的依赖包、python解释器等(_MEIxxx唯一目录),并从引导程序开始执行;
    • 单个目录,通过参数--onedir, -D指定,生成一个目录(包含所有的依赖)和一个可执行文件(包含项目文件);
      • 适合debug调试问题
      • 如果仅更改了项目脚本文件,只需重新发布exe程序即可,不用更新_internal目录;但是如果依赖也更新了,那么必须重新发布整个包(exe+_internal目录);
      • exe中的启动引导程序bootloader创建一个临时的python环境,使用python解释器开始执行所有的python脚本,从_internal目录中找到所有的依赖包;
  • 隐式导入问题,在python脚本中的隐式导入,pyinstaller无法感知,必须通过其他的参数来处理;
    • import()
    • importlib.import_module()
    • 运行时操作sys.path
  • 不会打包项目的py源码,而是打包编译后的pyc字节码,pyc原则上是可以反编译的,为了代码逻辑的安全性,最好使用Cython将py源码转为C代码,进而编译为机器码,更加安全;

命令行参数

pyinstaller main.py 打包时,可以指定一系列的命令行参数;

  • -h 帮助信息

    • -distpath DIR,打包产物存储目录
  • --workpath 工作目录

  • --clean,构建之前清理缓存和临时文件;

  • --log-level LEVEL 构建时是日志级别;

  • -D, --onedir,单目录打包模式;

  • -F, --onefile 单文件打包模式;

    --specpath spec文件存储的目录,默认在当前目录

    -n, --name 打包应用的名称

    --contents-directory 单目录模式的目录名;

  • --add-data source:dest, 将项目中的数据文件打包到根目录下的dest目录;例如.../resources:resources,将上级目录下的resources目录打包到根目录下的resources目录;./db.sqlite:. 将当前目录下的db文件打包到_internal或者_MEIxx目录下(根目录)

  • --add-binary source:dest, 添加二进制文件;

  • -p, --paths DIR,依赖包的搜索目录,等价于spec文件中的pathex

  • --hidden-import, --hiddenimport MODULENAME

  • --collect-submodules MODULENAME

    Collect all submodules from the specified package or module. This option can be used multiple times.

  • --collect-data module_name, 从模块中搜索数据文件;

  • --collect-binaries module_name; 从模块中搜索二进制文件;

  • --collect-all module_name,从模块中搜索数据文件、二进制文件、子模块;

  • --copy-metadata packagename,复制包的元数据;

  • --recursive-copy-metadatapackagename

  • --additional-hooks-dir hookspath,搜索钩子的目录;

  • --runtime-hook hook ,运行时的钩子

  • --exclude-module module_name, 排除的模组

  • --splash IMAGE_FILE,启动画面的图片

  • -w 没有控制台窗口;-c 有控制台窗口;

案例:

bash 复制代码
pyinstaller --noconfirm --log-level=WARN 
    -F -w
    --add-data="README:." 
    --add-data="image1.png:img" 
    --add-binary="libfoo.so:lib"
    --hidden-import=secret1
    --hidden-import=secret2
    --icon=..\MLNMFLCN.ICO
    myscript.py

# 也可以在python脚本中运行
import PyInstaller.__main__

PyInstaller.__main__.run([
    'my_script.py',
    '--onefile',
    '--windowed'
])

使用spec文件打包

第一次执行pyinstaller main.py时会在当前目录下生成一个.spec文件,编辑该文件后,可以直接使用.spec文件进行打包。

bash 复制代码
from PyInstaller.utils.hooks import collect_all, collect_data_files

datas = [
  ('src/README.txt', '.')
]
binaries = =[ ( '/usr/lib/libiodbc.2.dylib', '.' ) ]
hiddenimports = ["pydantic.deprecated.decorator"]
sklearn_data = collect_all("sklearn")
datas += sklearn_data[0]
binaries += sklearn.data[1]
hiddenimports += sklearn.data[2]

a = Analysis(['main.py'],
         pathex=['/xxdir'],  # 模块的搜索目录
         binaries=binaries, # 包含二进制文件
         datas=datas ,  #包含数据文件 
         hiddenimports=hiddenimports, # 隐式导入
         hookspath=None,
         runtime_hooks=None,
         excludes=None) # 排除的模块,不打包
pyz = PYZ(a.pure)
exe = EXE(pyz,... )
coll = COLLECT(...)

运行时信息

  • 区分程序运行时是否为打包,sys.frozen
python 复制代码
import sys

# 打包
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
	# _MEIPASS 就是_internal根目录或者单文件的解压目录_MEIXXX 
	# sys.executable 可执行文件的目录
    print('running in a PyInstaller bundle')
else:
    print('running in a normal Python process')

# 模块的路径
# __file__, sys._MEIPASS + 'mypackage/mymodule.py'
# main.py脚本被打包在sys._MEIPASS根目录下,内部的__file__为sys._MEIPASS + main.py
# 所以打包后读取main.py同级目录下的数据库文件,如下
# db_path = os.path.join(sys._MEIPASS, "db.sqlite")
# 也可以在main.py脚本中通过__file__获取该模块的绝对路径,然后获取根目录,再拼接数据库文件

# --add-data="./db.sqlite:." 打包到sys._MEIPASS根目录中
# --add-data="../path/to/file.dat:."  打包到sys._MEIPASS根目录中
  • sys.executable, 可执行文件路径
    • 打包前是python解释器的路径;
    • 打包后是exe可执行程序的路径;

项目打包案例

软件版本:

python 3.9

pyinstaller 6.16.0

pydantic 2.12.4

目录结构:

命令行切到main.py的同级目录下,执行pyinstaller -D main.py 会在同级目录下生成一个main.spec,编辑该main.spec:

bash 复制代码
# -*- mode: python ; coding: utf-8 -*-
import sys
from PyInstaller.utils.hooks import collect_all, collect_data_files

sys.setrecursionlimit(5000)

block_cipher = None

# pyinstaller==6.16.0
# pydantic== 2.12.4   防止两者不兼容
pydantic_data = collect_all("pydantic")

datas = [("./users.db", "."), ("../resource", "./resource"), ("./static", "./static")]
binaries = []
hidden_imports = []

datas += pydantic_data[0]
binaries += pydantic_data[1]
hidden_imports += pydantic_data[2]

a = Analysis(
    ['main.py'],
    pathex=["../"],
    binaries=binaries,
    datas=datas,
    hiddenimports=hidden_imports,
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=["PyQt5"],  # 如果python环境中既安装了PySide2,又安装了PyQt5,必须排除一个
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=False,
    name='lauf_app',  # 打包应用的名称
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True, # 启动时有控制台窗口
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='lauf_app',
)

常见问题:

  • 打包pydantic兼容性报错问题,确保pyinstaller 的版本兼容pydantic的版本
    • 如pyinstaller 6.16.0, pydantic 2.12.4
  • python环境中同时安装PySide2, PyQt5的问题,必须排除一个,在spec文件中excludes=["PyQt5"]
  • 在main.py目录下执行入口脚本,程序可以直接连接当前目录下的users.db;但是打包后的users.db在_internal目录中,exe就无法连接自己当前目录下的users.db,所以数据库连接的路径需要分情况设定:
python 复制代码
# 固定的db文件
if hasattr(sys, "frozen") and hasattr(sys, "_MEIPASS"):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{sys._MEIPASS}/users.db"
else:
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{BASE_DIR}/app/users.db"

engine = create_engine(SQLALCHEMY_DATABASE_URI, connect_args={
    "check_same_thread": False
})

打包完成后,在dist目录下会生成一个main目录,其内部结构为:

相关推荐
谅望者2 小时前
数据分析笔记09:Python条件语循环
笔记·python·数据分析
Auspemak-Derafru2 小时前
从U盘损坏中恢复视频文件并修复修改日期的完整解决方案
python
techzhi3 小时前
Intellij idea 注释模版
java·python·intellij-idea
李昊哲小课3 小时前
wsl ubuntu24.04 cuda13 cudnn9 pytorch 显卡加速
人工智能·pytorch·python·cuda·cudnn
温暖名字3 小时前
调用qwen3-omni的api对本地文件生成视频文本描述(批量生成)
python·音视频·qwen·qa问答
一眼万里*e4 小时前
搭建个人知识库
python
程序员小远5 小时前
软件测试之bug分析定位技巧
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·bug
江上清风山间明月5 小时前
Android 系统中进程和线程的区别
android·python·线程·进程
mit6.8245 小时前
[LivePortrait] docs | Gradio用户界面
python