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目录,其内部结构为:

相关推荐
ada7_17 小时前
LeetCode(python)——543.二叉树的直径
数据结构·python·算法·leetcode·职场和发展
小白学大数据17 小时前
Python 多线程爬取社交媒体品牌反馈数据
开发语言·python·媒体
HAPPY酷17 小时前
压缩文件格式实战速查表 (纯文本版)
python
祝余Eleanor17 小时前
Day 31 类的定义和方法
开发语言·人工智能·python·机器学习
背心2块钱包邮17 小时前
第6节——微积分基本定理(Fundamental Theorem of Calculus,FTC)
人工智能·python·机器学习·matplotlib
larance17 小时前
修改jupyterlab 默认路径
python
别叫我->学废了->lol在线等18 小时前
python单例模式下线程安全优化
python·安全·单例模式
西江6497618 小时前
【个人博客系统—测试报告】
python·功能测试·jmeter·pycharm·postman
CHANG_THE_WORLD18 小时前
C++ vs Python 参数传递方式对比
java·c++·python
梁正雄20 小时前
10、Python面向对象编程-2
开发语言·python