Pyinstaller打包通用流程

Pyinstaller打包通用流程

前言

什么是Pyinstaller

pyinstaller.jpg

Pyinstaller是用于打包python项目的一个工具, 可以将项目代码打包成可执行文件, 在其他机器上使用.
通俗的说, 没打包的时候运行程序的命令是:python3 main.py arg1 arg2 ....那么打包完后可以这么执行./main arg1 arg2 ..., main是你打包后的可执行文件名. arg1 arg2 ...就是运行程序的参数,可以是sys.argv, argparser或者其它命令行参数工具.

就个人使用情况来看,Pyinstaller有两大特点(未必是优点):

  1. 部署方便, 目标机器可以没有各种繁杂的第三方python库, 甚至不需要python环境,你带着一个可执行程序就可以去部署了.
  2. 代码安全, 带着可执行程序去运行可以避免代码泄露, 但是如果要完全保护代码还要考虑防反编译.

我为什么写这篇文章

就官方教程和网上数以万计的博客来看, 打包流程真的很简单, 首先把你的祖传代码改好并确定好入口文件(这里假设是main.py),然后执行命令pyinstaller -F main.py 最后去dist目录拿可执行文件main(windows下可能是exe后缀)就可以愉快运行了.

但是在这个简单的过程中,确总会出各种错误,其中主要原因是使用Pyinstaller的场景往往都是工业界部署, 需要打包一个项目或工程, 项目复杂必然容易出错.

本人自参加工作以来,经常参与打包部署, 遇到了很多坑, 因此专门写了这篇文章来分享靠谱的打包流程和常见Bug解决办法. 本人主要工作环境是Linux, 因此本文对Linux下的打包更具指导意义.

本文主要内容

本文主要分成两部分,第一部分讲通用靠谱的打包流程, 这是我无数次打包总结的一套流程, 希望能帮助到大家.
第二部介绍Pyinstaller打包常见Bug和相关Tips.

pyinstaller打包的流程

Step1 工具准备

打包环境需要安装pyinstaller库和setuptools库, pip install即可.
这里强烈建议使用 pyinstaller 3.5版本和setuptools 44.0版本,否则有一定几率会出现bug.
pyinstaller高版本可能会在你使用hooks时报错.
setuptools高版本可能会在你打包过程中出现pkg_resources.py2_warn的错误

Step2 配置打包项

通常情况下写好代码,装好必备库基本就可以执行pyinstaller -F main.py了.但是考虑到项目复杂要做很多配置, 我们先来生成一个打包配置文件, 执行命令pyi-makespec -F main.py, 然后你就会在main.py的同级目录下看到main.spec文件. 这个文件的主要作用就是指定打包的各种配置, 下面贴一份一个spec文件内容:

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['main.py'],
             pathex=['/home'],
             binaries=[], # 打包动态链接库文件(so或dll)
             datas=[], # 打包程序需要的数据(文本\音视频等)
             hiddenimports=[], # 一些难以打包进去的库放到里面(通常是复杂的库)
             hookspath=[], # 指定hook文件夹,能够搜索添加库所需的所有文件
             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,
          [],
          exclude_binaries=True,
          name='main',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='main')

spec文件内容还是挺多的,语法就是python,因此修改该文件的时候请遵循python语法. 内容虽多,不过真正常用的就四项分别是 binaries,datas,hiddenimportshookspath. 它们的主要功能我都加在了注释里, 下面我会详细讲述这个四个选项应该如何配置,这也是本文的重要内容之一.

(1) binaries

binaries用于添加程序运行时所需的一些动态链接库文件,举个例子你打包后的程序报错说找不到xxx.so文件,那么你可以先通过find命令找到这个文件地址然后把它放到binaries列表里面:

binaries=['/home/xx/xxx/xx.so']

(2) datas

如果你的打包程序需要播放好多音频文件(假设都在resources目录内), 那你就可以把这个音频文件夹地址放到datas列表中,格式如下:

datas = [('./resources/','songs')]

除了指定音频文件目录,我们还加了一个字符串songs,这是表示你的可执行程序释放文件时把resources文件夹里的内容释放到songs文件夹里.通常情况下执行可执行程序时, 程序会在/tmp目录建立一个_MEI开头临时目录,并将程序运行所需文件都释放到这里,所以你所有的音频文件都可以在这个临时目录里的songs文件夹找到.

写到这里各位应该会发现一个关键点: 你的代码需要判断是自己是如何被执行的, 通过打包后的可执行程序执行的?还是通过main.py执行的?, 那不然没法确定资源访问目录, 解决办法: 如果getattr(sys,"frzozen",False)为真,则是在可执行文件里执行的,然后通过sys._MEIPASS获取上文提到的临时文件目录.

最后,本人不建议将数据打包到程序中,代码和数据分离是编程基本原则,数据加在可执行程序里,不仅让程序体积变大,还会使修改\替换数据变的复杂(具体修改方法取决于你的代码加载文件机制,一个骚操作是趁程序释放文件之时,迅速定位目录位置进行文件替换>~<), 所以能分离就分离吧!

(3) hiddenimports

如果你的程序报xxx模块找不到的错误,那么往往就是某个程序隐式调用了该模块,使得pyinstaller没有扫描到. 这个时候我们就把他显示加进去,举个例子,程序报错找不到numpy库,那么我们就做如下操作:

hiddenimports=['numpy']

有时候不仅要把xx库加进去,还要把xx.yy加进去,比如 hiddenimports=['tensorflow','tensorflow.contrib'],
具体根据报错信息来确定

(4) hookspath

hookspath的作用是搜索某个库所需的文件并把他们添加到打包程序里. hookspath指定一个文件夹路径(通常情况下文件夹名就叫hooks),hooks文件夹里有若干python文件,以hook-库名格式命名,比如对于gevent, 就要命名为hook-gevent.py. 强烈推荐文件内容这么写:

from PyInstaller.utils.hooks import collect_all  
  
  
def hook(hook_api):  
    packages = ['gevent']  
    for package in packages:  
        datas, binaries, hiddenimports = collect_all(package)  
        # hook_api.add_datas(datas)  # 注释掉是因为通常用不到
        # hook_api.add_binaries(binaries)
        hook_api.add_imports(*hiddenimports)

简单解释下,借助collect_all函数可以分析出这个库需要的所有文件和库, 以gevent为例, 执行

datas,  binaries,  hiddenimports  =  collect_all('gevent')

那么:

  • 你将会获得该库所需的所有数据文件,比如__greenlet_primitives.pxd, __hub_local.pxd
  • 你将会获得该库所需的所有二进制文件,比如__greenlet_primitives.cp37-win_amd64.pyd
  • 你将会获得该库所需的所有子模块, 比如gevent.threadpool, gevent._semaphore

然后借助上面代码中的hook_api.add_xxx函数把他们添加进去,我注释掉了两行,是因为有时候这两行不需要,实战时可以根据报错按需添加.

可以看出hooks可替代hiddenimports, 但还是建议优先使用hiddenimports, 这也是为了减小程序体积.

Step3 打包&测试

如果你把上述配置都做好了,那么恭喜你,基本上打包和运行就很难出错了.
打包命令是pyinstaller -F main.spec. 注意是spec文件, 不是py文件
然后去dist目录里找到main文件,最后./main args测试即可.
另外附上被打包项目的目录结构:

ProjectName
│  main.py # 入口文件
│  main.spec # 配置文件
│
├─codes # 你的祖传代码
└─hooks # hook文件
  └─────hook-gevent.py
  └─────hook-tensorflow.py

常见Bug和相关Tips

下面是本人打包时遇到的一些bug和解决思路以及个人打包经验, 供大家参考:

  1. 遇到各种not foundimport error的错误,基本通过hiddenimports,hooksbinaries解决.(这个错误基本占了Pyinstlaller所有报错的 90%....)
  2. 遇到这种罕见错误:struct.error: 'i' format requires -2147483648 <= number <= 2147483647是指你的打包文件太大了,超出2GB限制了,详情看这个issue. 解决方法,要么精简模块,要么慎用hooks功能,别把啥东西都往程序里塞,这也是建议优先hiddenimports的原因.
  3. 能用hiddenimports就别用hooks,减小体积
  4. 打包程序多输出调试信息方便定位bug.
  5. 打包前务必认真测试,打包后出了bug就要重新打包了
  6. 打包深度学习程序等的时间在十分钟左右,保持耐心
  7. tensorflow最好1.14,实测大于1.14会出问题
  8. 打包时会有Qt failed的相关信息不用理会,和python的图形界面编程有关
  9. Pyinstaller程序使用GPU务必保证环境变量设置的没问题
  10. 同样是Pyinstaller程序使用GPU的问题,打包环境和运行环境显卡驱动版本最好一致,那不然有可能用不了GPU(这个本人还未充分验证),如果真遇到了这个情况,欢迎使用nvdocker.
  11. 如果一些bug怎么都解决不了,尝试切换不同库版本(比如pyinstaller,setuptools和你的程序使用的库), 甚至在纯净的docker里打包试试

最后感谢各位阅读, 希望能帮到你们.

文章可以转载, 但请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350