利用pyinstaller打包python项目
由于本文讲述还算比较详细, 如果对pyinstaller有一定的了解,只需快速上手,可以直接跳到第四章结合实例进行操作。1简介及安装pyinstaller
1.1简介
PyInstaller是一个第三方库,它能够在Windows、Linux、 Mac OS X 等操作系统下将 Python 源文件打包,通过对源文件打包, Python 程序可以在没有安装 Python 的环境中运行,也可以作为一个独立文件方便传递和管理。 PyInstaller支持Python 2.7和Python 3.3+。可以在Windows、Mac OS X和Linux上使用,但是生成的可执行文件并不是跨平台的,也就是说如果要是希望打包成.exe文件,需要在Windows系统上运行PyInstaller进行打包工作;打包成mac app,需要在Mac OS上使用;而linux系统下打包的可执行文件则没有后缀,比如对xxx.py进行打包,打包后的文件名为xxx。 后续章节将主要以在windows系统下打包成exe可执行文件为例进行讲解。1.2 安装pyinstaller
PyInstaller 库会自动将 PyInstaller 命令安装到 Python 解释器 目录中,与 pip 或 pip3 命令路径相同,因此可以直接使用。 无论是linux、mac或者windows都可以直接使用pip命令进行安装,pip安装命令如下:pip install pyinstaller
2 pyinstaller的基本使用
2.1基本命令及常用参数
Pyinstaller可以通过简单的命令进行python代码的打包工作,其基本的命令为:pyinstaller -option xxx.py
其中xxx.py就是需要打包的python脚本,option为pyinstaller中的参数选项,其中 option默认为“-D” 。 完成后生成三个文件夹“__pycache__”(缓存)、“build”(临时文件)、“dist”(生成文件)和 一个文件“xxx.spec”(配置文件)。生成的应用程序就在“dist”文件夹中。 如在Windows下打包主文件为main.py的python项目脚本,只需在main.py所在路径下的cmd中执行下面的代码即可:
pyinstaller main.py
图2-1 打包前项目文件 图2-2 打包后文件 PyInstaller既可以在Windows平台上使用,也可以在 Mac OS X 平台上运行。在不同的平台上使用 PyInstaller 工具的方法是一样的,它们支持的选项也是一样的。其常用选项如下: 表2-1 pyinstaller常用参数
可选参数 | 格式举例 | 功能说明 |
-F | pyinstaller -F demo.py | 只在dist中生产一个demo.exe文件。 |
-D | pyinstaller -D demo.py | 默认选项,除了demo.exe外,还会在在dist中生成很多依赖文件,推荐使用。 |
-c | pyinstaller -c demo.py | 默认选项,只对windows有效,使用控制台,就像编译运行C程序后的黑色弹窗。 |
-w | pyinstaller -w demo.py | 只对windows有效,不使用控制台。 |
-p | pyinstaller -p E:\python\Lib\site-packages demo.py | 设置导入路径,一般用不到。 |
-i | pyinstaller -i D:\file.icon demo.py | 将file.icon设置为exe文件的图标,推荐一个icon网站: icon |
2.2 打包成:单个文件or多个文件
打包成单个文件和打包成多个文件(文件夹)的命令方式很简单,只是参数选项不同而已,分别如下:pyinstaller -F xxx.py #打包成单个文件
pyinstaller -D xxx.py #打包成多个文件
打包成单个文件和多个文件后二者在文件的数量上、以及执行exe程序的时间上都存在差异。 打包成单个文件,生成的dist文件夹中只有一个exe程序,其更容易管理,但是这个程序的运行却比较慢。因为,在打包的过程中,其将项目脚本的所有依赖都压缩到了这一个文件当中。当运行程序时,PyInstaller的引导程序会新建一个临时文件夹。然后解压程序的第三方依赖文件到临时文件夹中。这也是为什么一个可执行文件比文件夹中执行的时间要长的原因。 而打包成多个文件,生成的dist文件夹中包含所有依赖项,以及可执行文件。其管理比打包成单个文件要麻烦些,但是执行exe程序的速度却相对快些,且更新的时候只需更新可执行文件即可。PyInstaller的引导程序是一个二进制可执行程序。当用户启动你的程序的时候,PyInstaller的引导程序开始运行,首先创建一个临时的Python环境,然后通过Python解释器导入程序的依赖,当然他们都在同一个文件夹下。 图2-3 dist文件夹情况——打包成单个文件 图2-4 dist文件夹——打包成多个文件
3 spec配置文件
3.1 spec文件简介及生成命令
3.1.1 spec基介绍及作用
当执行打包命令时,PyInstaller首先建一个sepc(specification)文件:script.spec。这个文件的存放地址可以使用参数–specpath= 来定义,默认放在当前文件夹下。 spec文件的作用是什么呢?它会告诉PyInstaller如何处理你的py文件,它会将你的py文件名字和输入的大部分参数进行编码。PyInstaller通过执行spec文件中的内容来生成exe,有点像makefile。3.1.2 需要修改spec文件的一般场景
正常使用中我们是不需要管spec文件的,但是下面几种情况需要修改spec文件:- 需要打包资源文件;
- 需要include一些PyInstaller不知道的库;
- 为可执行文件添加run-time 选项;
- 多程序打包。
3.1.3 spec文件生成命令
sepc文件在执行pyinstaller打包命令(如:pyinstaller -F main.py)的时候就会自动生成,但有些场景我们需要修改spec文件,因此需要在打包前生成spec文件并修改。生成spec文件的命令如下所示,默认其生成名为xxx.spec配置文件。pyi-makespec -options xxx.py
图3-1 spec文件生成成功提示 出现图片上的提示,即为生成spec文件成功。 生成并修改spec文件后,可以根据spec文件进行打包,具体命令如下:
pyinstaller xxx.spec
3.2 spec文件解析与配置
3.2.1 spec文件解析
spec文件是一个python脚本,文件中主要包含4个class: Analysis, PYZ, EXE和COLLECT。- Analysis以py文件为输入,它会分析py文件的依赖模块,并生成相应的信;
- PYZ是一个.pyz的压缩包,包含程序运行需要的所有依赖;
- EXE根据上面两项生成;
- COLLECT生成其他部分的输出文件夹,生成单个文件没有COLLECT,生成多个文件有COLLECT。
3.2.2 spec文件配置
在对python项目进行打包修改sepc文件时,主要是以对analysis进行修改和配置为主。如图3.2所示,以针对多脚本、多目录的python项目为例,一般而言对spec文件的配置都包含如下一些操作: a)py文件打包配置 analysis的第一个参数(如”[main.py]”)为需要解析的py脚本。针对多目录多脚本的python项目,打包时需要将所有相关的py脚本文件都添加到Analysis类里,即第一个参数中。 Analysis类中的pathex定义了打包的主目录(生成spec文件的时候会自动填充好主目录路径),对于在此目录下的py文件可以只写文件名不写路径。 如图3.2所示,除了主文件main.py外,其他相关脚本model.py、create_ feature.py、get_data.py、define_resource_path.py等脚本也添加到了Analysis类中。值得注意的时主目录为“C:\\Users\\RYW\\Desktop\\test”,mian.py在主目录下,所以在添加py文件时main.py可以直接添加,而其他不在主目录下的py文件则需要带上绝对路径。 b)资源文件打包配置 资源文件包括打包的python项目使用的相关文件,如模型配置文件、模型数据文件或者图标文件、文本文件等。对于此类资源文件的打包需要设置Analysis的datas,如例子所示datas接收元组:元组的基本格式为:(”原项目中资源文件路径”,”打包后的文件路径”)。如图3.2所示,('data','data')表示将“C:\\Users\\RYW\\Desktop\\test\\data” 下的所有资源文件打包后放入打包结果路径下的“data”目录中。 c)Hidden import配置 pyinstaller在进行打包时,会解析打包的python文件,自动寻找py源文件的依赖模块。但是pyinstaller解析模块时可能会遗漏某些模块(not visible to the analysis phase),造成打包后执行程序时出现类似No Module named xxx。这时我们就需要在Analysis下hiddenimports中加入遗漏的模块,如例子中所示。一般而言都是在生成可执行程序(exe)的过程中抛出No Module named xxx错误才会发现这些遗漏的模块,碰到这些异常,只需在spec文件中添加对应的遗漏模块即可。 d)递归深度设置 在打包导入某些模块时,常会出现"RecursionError: maximum recursion depth exceeded"的错误,这可能是打包时出现了大量的递归超出了python预设的递归深度。因此需要在spec文件上添加递归深度的设置,设置一个足够大的值来保证打包的进行,即:import sys
sys.setrecursionlimit(5000)
如果没有设置,则基本上都会抛出"RecursionError: maximum recursion depth exceeded"错误。 图3-3 spec文件Analysis示例
4 pyinstaller打包流程示例及常见错误
本章节主要对单个py文件、多个py文件以及带有资源数据文件的这三种python项目的打包方法做个梳理,并以实际示例来介绍基本打包流程,以及在实际打包过程中以及运行打包后的可执行文件时会遇到的问题和对应的解决方案。4.1 单脚本python项目打包示例
如需要对test目录下的hello.py进行打包(单个文件或者多个文件)。 图4.1 示例原始文件 打包成单个文件或者多个文件:pyinstaller -F hello.py #打包成单个文件
pyinstaller -D hello.py #打包成单个文件
直接在test路径下的cmd中执行上面的代码即可。执行后,打开test目录会发现,多了dist、build这两个文件夹和一个hello.spec文件。且程序运行过程中一般会报如下错误: 图4-2 RecursionError错误 正如第3章在spec文件解析与配置章节所述,这是递归深度错误,只需在spec文件前几行添加如下代码即可:
import sys
sys.setrecursionlimit(5000)
修改spec配置文件后,再在cmd中根据spec文件进行打包:
pyinstaller -F hello.spec
当出现如下图所示的提示时,则表明打包成可执行文件成功。 图4-3 打包成功提示 在dist文件夹中可以看到有个hello.exe文件,双击执行该exe文件即可运行。 图4-4 单脚本示例打包后项目目录中的文件 例如本文hello.py文件代码为:
import pandas as pd
def print_string():
print("-" * 10)
print("this is a test")
print("hello python")
print("exit this exe")
if __name__ == '__main__':
print_string()
运行程序后,会弹出个窗口,具体如下图所示: 图4-5 成功运行程序窗口显示图
4.2 多脚本或含有资源数据文件打包示例
一般而言,针对多脚本的python项目可以通过修改spec文件的方法进行打包,也可以通过核心命令: pyinstaller -F main.py -p py_dir ,进行打包。但是在实际打包过程中或者即便打包成功后但是在执行可执行程序时,经常会遇到错误使得我们还是得去修改spec文件重新打包执行程序。因此,本文直接使用修改spec文件再根据spec文件的方法对这两种类型的python项目打包示例进行讲解,对于使用核心命令对多个程序进行打包的案例,可以参考 这篇博客 ,本文不再讲述。一般而言,大部分项目都会涉及多个脚本并包含数据资源文件。 示例场景如下所述,整个python项目文件包括conf、data、model、output、utils共5个文件夹以及一个main.py文件,其中conf文件夹中包含项目配置文件datasource.conf文件;data中包含项目数据train.csv,model中包含ensemble_model.py(在主程序main.py被import);output中包含model_result文件夹,该文件夹中又包含已经与训练好的模型classifier_result.p;utils中包含create_feature.py和get_data.py,这两个脚本都在主程序main.py中被import。项目目录如下图所示。 图4-6 多脚本项目原始文件 针对上述python项目打包过程如下: 1)打开cmd进入该项目路径,并执行下述命令创建spec配置文件。pyi-makespec -F main.py # 将项目打包成单个文件
2)修改spec配置文件。 修改后的spec文件如下: # -*- mode: python ; coding: utf-8 -*- import sys sys.setrecursionlimit(5000) block_cipher = None a = Analysis(['main.py', 'D:\\work\\test_pyinstaller\\model\\ensemble_model.py', 'D:\\work\\test_pyinstaller\\utils\\create_feature.py', 'D:\\work\\test_pyinstaller\\utils\\get_data.py'], pathex=['D:\\work\\test_pyinstaller'], binaries=[], datas=[('data','data'),('output\\model_result','output\\model_result'),('conf','conf')], hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], 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, a.binaries, a.zipfiles, a.datas, [], name='main', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=True ) 具体修改过程如下: 首先,针对"RecursionError: maximum recursion depth exceeded"错误进行处理,添加了 sys.setrecursionlimit(5000) ; 其次,将项目的主程序及相关脚本的路径都进行了添加,除主程序外,其他相关脚本都是绝对路径,如“D:\\work\\test_pyinstaller\\utils\\get_data.py”,详细讲解请见3.2.2spec文件配置; 最后,将项目conf文件中的所有配置文件、data文件夹中的所有数据文件、output文件夹中的所有模型输出文件都添加到datas参数中,如('data','data')表示将项目中“data”文件夹中的所有文件放到打包结果路径下的“data”目录, 注意不论这些文件夹中的文件是一个还是多个都是如此添加 。 3)根据配置文件进行打包。
pyinstaller -F main.spec
4)根据打包过程中的错误提示修改spec文件,修改后删除build、dist文件夹并执行第3)步重新打包,直至dist文件夹中成功出现main.exe文件。 由于示例针对"RecursionError: maximum recursion depth exceeded"做了处理,所以打包过程很顺利,直接生成了exe文件。否则如果未做处理,则不会生成exe文件,而是报RecursionError错误。除此之外,还发现了一个“Unable to find "nltk_data" when adding binary and data files”错误,详细情况请见下一节。 5)双击执行exe程序,对弹出的窗口信息进行截屏,如果为错误信息,则根据错误提示继续修改spec文件,并删除build、dist文件夹执行第3)步重新打包,直至执行exe成功运行项目。 本案例执行exe的错误提示及对应的解决方案见4.3常见错误小节。
4.3 常见错误
本小节主要是介绍4.2小节示例在第4)步和第5)步中遇到的一些错误以及对应的解决方案,将这些错误单独放在一个小节以便于以后快速查找。由于介绍的只是自己在打包过程中遇到的错误,因此所罗列的错误可能并不全面,仅供参考。 本小节是4.2节中示例第5)步的介绍。 (1)Unable to find "nltk_data" when adding binary and data files 这种错误发生在生成exe可执行程序时,会导致生成exe失败。这是PyInstaller的hook文件夹在处理nltk时出现这样的错误,开发者的问题,跟我们没关系。 解决方案如下:首先,下载nltk_data,下载后解压,并将解压后的文件夹放到错误提示中的任一路径下。然后,找到hook-nltk.py文件,通常此文件的目录为“D:\anaconda\Lib\site-packages\PyInstaller\hooks\hook-nltk.py”。最后,修改此文件,具体如下:import nltk
from PyInstaller.utils.hooks import collect_data_files
# add datas for nltk
datas = collect_data_files('nltk', False)
# loop through the data directories and add them
# for p in nltk.data.path:
# datas.append((p, "nltk_data"))
datas.append(("<path_to_nltk_data>", "nltk_data"))
# nltk.chunk.named_entity should be included
hiddenimports = ["nltk.chunk.named_entity"]
记住一定要把<path_to_nltk_data>替换成自己电脑上nltk_data文件夹所在的路径!例如,我的nltk_data在“D:\anaconda\share\nltk_data”,所以修改为:datas.append(("D:\\anaconda\\share\\nltk_data ", "nltk_data")) 。 (2)ModuleNotFoundError:No module named ‘xxx’ 运行exe后,错误信息如下图所示,此时是pyinstaller没有解析出一些隐藏的模块,因此需要将这些模块添加到spec文件中的“ hiddenimports ”参数中。在打包过程中除了如图中显示的python库外,还有别的依赖库未能成功打包,因此全部添加后,spec文件中的hiddenimports参数变为:
hiddenimports=['sklearn.neighbors._typedefs','sklearn.utils._cython_blas','sklearn.neighbors._quad_tree','sklearn.tree._utils','xgboost','lightgbm']
图4-7 ModuleNotFoundError (3)XGBoostLibraryNotFound:Connot find XGBoost Library in … 针对这种错误可以在spec文件中增加如下代码:
from PyInstaller.utils.hooks import collect_data_files
data_xgb = collect_data_files('xgboost')
然后,将data_xgb赋给Analysis类中的binaries参数,即: binaries=data_xgb 。 也可以在pyinstaller安装目录中“\PyInstaller\hooks”添加一个hook-xgboost.py文件(例如我的路径为“D:\anaconda\Lib\site-packages\PyInstaller\hooks”),文件中内容如下:
from PyInstaller.utils.hooks import collect_data_files
data_xgb = collect_data_files('xgboost')
在示例中,不光是不能发现xgboost.dll二进制文件,lightgbm.dll二进制文件也未发现,因此通过添加hook-lightgbm.py的方式解决此问题。所以, 只要是类似找不到“.dll”二进制文件的情况都可以通过上述方法解决。注意:使用将xgboost.dll等相关二进制文件复制到下面红框中对应路径的方法实测无效。 图4-8 无法找到二进制文件错误 (4)FileNotFoundError:No such file or directory:…(数据资源未能成功打包) 这种错误信息如下所示,从绿色框种可以发现,项目可以运行,但是从红色框种发现,程序无法发现数据文件,且明明output/model_result目录中有已经训练好的模型,但是程序确显示没有本地模型,这些情况都表明,打包过程中未能将模型文件、数据文件打包到程序中。 图4-9 FileNotFoundError 解决方案如下:将项目涉及的py文件中所有文件路径改为资源访问路径,在python主文件中加入如下函数及类似路径: #生成资源文件目录访问路径
def resourcePath(relative_path):
if getattr(sys, 'frozen', False): #是否Bundle Resource
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
local_model_path = resourcePath("output\\model_result\\classifier_result.p")
conf_path = resourcePath("conf\\datasource.conf")
data_path = resourcePath("data\\train.csv")
这样执行exe文件时就无需附带数据文件,使用后会在“C:\Users\你的用户名\AppData\Local\Temp”生成临时文件。 将这些错误解决后,spec文件最终如下:
# -*- mode: python ; coding: utf-8 -*-
import sys
sys.setrecursionlimit(5000)
block_cipher = None
from PyInstaller.utils.hooks import collect_data_files
data_xgb = collect_data_files('xgboost')
a = Analysis(['main.py',
'D:\\work\\test_pyinstaller\\model\\ensemble_model.py',
'D:\\work\\test_pyinstaller\\utils\\create_feature.py',
'D:\\work\\test_pyinstaller\\utils\\get_data.py'],
pathex=['D:\\work\\test_pyinstaller'],
binaries=data_xgb,
datas=[('data','data'),('output\\model_result','output\\model_result'),('conf','conf')],
hiddenimports=['sklearn.neighbors._typedefs','sklearn.utils._cython_blas','sklearn.neighbors._quad_tree','sklearn.tree._utils','xgboost','lightgbm'],
hookspath=[],
runtime_hooks=[],
excludes=[],
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,
a.binaries,
a.zipfiles,
a.datas,
[],
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True )
4.4 打包后的exe文件太大(pipenv)
如果本机安装了很多模块,使用pyinstaller打包的时候就会把已安装的模块都打包进去,从而导致打包生成的exe文件特别大。因此可以结合pipenv打包,具体思路如下: 首先利用pipenv创建虚拟环境,然后在pipenv的虚拟环境里面安装pyinstaller以及项目相关的第三方依赖,最后 使用 pipenv 虚拟环境里面的 pyinstaller 打包,这样项目文件的体积就下来了。 具体操作步骤如下: 1)安装pipenvpip install pipenv
2)在项目文件夹中打开cmd建立虚拟环境
pipenv install
3)进入虚拟环境
pipenv shell
4)在虚拟环境中安装pyinstaller
pip install pyinstaller
5)在虚拟环境中安装项目相关的第三方依赖(三种方式任选一)
pip install pandas
pip install pandas==1.0.3 #指定版本
pip install -r requirements.txt #根据requirements.txt安装
6)使用pyinstaller打包,具体流程参考前面章节 注意, 要在虚拟环境里安装 pyinstaller。如果你没有在虚拟环境中安装pyinstaller,你同样可以使用pyinstaller命令,但是调用的是你系统原本的那个python编译器,内含很多关联库,导致即使在虚拟环境中,你打包的exe文件仍然非常大。 参考: [1] https://www.jianshu.com/p/049926906d83 [2] https://www.jianshu.com/p/ef6e23f8e2c1 [3] https://blog.csdn.net/qq_19707521/article/details/104755943#comments_12760510 [4] https://blog.csdn.net/weixin_44697140/article/details/106911389 [5] https://www.lizenghai.com/archives/62806.html [6] https://blog.csdn.net/weixin_42052836/article/details/82315118#Pyinstaller%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95 [7] http://legendtkl.com/2015/11/06/pyinstaller/ [8] https://blog.csdn.net/muslim377287976/article/details/103956110 [9] https://www.zhihu.com/question/268397385 [10] https://www.jianshu.com/p/dcbb01e8c78a