Python3中采用PyInstaller打包工程项目

对自己完成的工程项目进行打包,因为是第一次尝试,踩了各种各样的坑。以防下次继续踩坑,把相关问题以及解决办法记录下来。此次打包采用Python3.6,PyInstaller3.6,Windows64位系统。接下来就是整篇文章的精华了。

一、PyInstaller安装

  PyInstaller包的安装可以在Anaconda环境下以conda install pyinstaller进行安装,在PyCharm中可以通过pip install pyinstaller进行安装。安装成功后就可以着手进行打包了。当然打包需要用到以下一些相关命令了。

-h,--help查看该模块的帮助信息
-F,-onefile产生单个的可执行文件
-D,--onedir产生一个目录(包含多个文件)作为可执行程序
-a,--ascii不包含 Unicode 字符集支持
-d,--debug产生 debug 版本的可执行文件
-w,--windowed,--noconsolc指定程序运行时不显示命令行窗口(仅对 Windows 有效)
-c,--nowindowed,--console指定使用命令行窗口运行程序(仅对 Windows 有效)
-o DIR,--out=DIR指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件
-p DIR,--path=DIR设置 Python 导入模块的路径(和设置 PYTHONPATH 环境变量的作用相似)。也可使用路径分隔符(Windows 使用分号,Linux 使用冒号)来分隔多个路径
-n NAME,--name=NAME指定项目(产生的 spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字

  常用到的命令为-F、-D、-i、-p、-w等,其中-i用于指定生成项目的图标,需要使用绝对路径。对于打包结果较大的项目,选用-d生成目录相比单可执行文件的打包方式,执行速度更快,但包含更加多的文件。本文的例子选中-D方式打包。

二、Python项目打包

  打包分为单文件打包以及工程文件打包,单文件打包直接在命令窗口中采用pyinstaller -D filename.py,具体命令的添加可以参考表格中的命令。工程文件打包稍微有一些麻烦,主要是先生成主窗口的.spec文件,并修改相应的内容,最终执行.spec文件就可以逐步实现打包。以下是本次打包的工程目录,主要文件均在moleculeSystem,还包括img文件夹,tempFile文件夹等众多文件。

Python3中采用PyInstaller打包工程项目

1.SPEC文件的生成

通过使用命令pyi-makespec -w xxx.py能够生成相应的xxx.spec文件,具体如下:

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

block_cipher = None

a = Analysis([‘mainWin.py‘],
             pathex=[‘D:\\Python\\untitled1\\moleculeSystem‘],
             binaries=[],
             datas=[],
             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,
          [],
          exclude_binaries=True,
          name=‘mainWin‘,
          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=‘mainWin‘)

spec文件中主要包含4个class: Analysis, PYZ, EXE和COLLECT.

  • Analysis以py文件为输入,它会分析py文件的依赖模块,并生成相应的信息
  • PYZ是一个.pyz的压缩包,包含程序运行需要的所有依赖
  • EXE根据上面两项生成
  • COLLECT生成其他部分的输出文件夹,COLLECT也可以没有

首先给出举例项目的spec文件:

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

block_cipher = None
SETUP_DIR = ‘D:\\Python\\untitled1\\‘

a = Analysis([‘mainWin.py‘],
             pathex=[‘D:\\Python\\untitled1\\moleculeSystem‘],
             binaries=[],
             datas=[(SETUP_DIR+‘img‘,‘img‘),(SETUP_DIR+‘temp_img‘,‘temp_img‘),(SETUP_DIR+‘mol\\totalFile‘,‘mol\\totalFile‘),(SETUP_DIR+‘tempFile‘,‘tempFile‘),(‘D:\\Anaconda\\Anaconda3\\Lib\\site-packages\\vtkmodules‘,‘vtkmodules‘),(SETUP_DIR+‘AtomsInfo.txt‘,‘AtomsInfo.txt‘),(SETUP_DIR+‘BondInfos.txt‘,‘BondInfos.txt‘)],
             hiddenimports=[‘pkg_resources‘],
             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,
          [],
          exclude_binaries=True,
          name=‘mainWin‘,
          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=‘mainWin‘)

1)py文件打包配置

针对多目录多文件的python项目,打包时候需要将所有相关的py文件输入到Analysis类里。Analysis类中的pathex定义了打包的主目录,对于在此目录下的py文件可以只写文件名不写路径。如上的spec脚本,将所有项目中的py文件路径以列表形式写入Analysis,这里为了说明混合使用了绝对路径和相对路径。

2) 资源文件打包配置

资源文件包括打包的python项目使用的相关文件,如图标文件,文本文件等。对于此类资源文件的打包需要设置Analysis的datas,如例子所示datas接收元组:datas=[(SETUP_DIR+‘img‘,‘img‘)]。元组的组成为(原项目中资源文件路径,打包后路径),例子中的(SETUP_DIR+‘img‘,‘img‘)表示从D:\Python\untitled1\下的img文件夹文件打包后放入打包结果路径下的img目录。

3)Hidden import配置

pyinstaller在进行打包时,会解析打包的python文件,自动寻找py源文件的依赖模块。但是pyinstaller解析模块时可能会遗漏某些模块(not visible to the analysis phase),造成打包后执行程序时出现类似No Module named xxx。这时我们就需要在Analysis下hiddenimports中加入遗漏的模块,如例子中所示。

4)递归深度设置

在打包导入某些模块时,常会出现"RecursionError: maximum recursion depth exceeded"的错误,这可能是打包时出现了大量的递归超出了python预设的递归深度。因此需要在spec文件上添加递归深度的设置,设置一个足够大的值来保证打包的进行,即

import sys
sys.setrecursionlimit(5000)

5)去除不必要的模块import

有时需要让pyinstaller不打包某些用不到的模块,可通过在excludes=[]中添加此模块实现
3.使用spec文件打包

pyinstaller -D xxx.spec

打包生成两个文件目录build和dist,build为临时文件目录完成打包后可以删除;dist中存放打包的结果,可执行文件和其它程序运行的关联文件都在这个目录下。

Python3中采用PyInstaller打包工程项目

三、打包出现的问题

1.PyQt plugins缺失

使用PyQt编写UI交互界面的python代码在进行打包时可能会出现一些特别的问题。

执行使用了PyQt的打包程序,常会出现这样的错误,提示缺少Qt platfrom plugin “windows”,如下图

 Python3中采用PyInstaller打包工程项目

打包后程序运行后,使用png格式的图标可以正常显示,但使用的ico格式图标不显示。这两个错误产生的问题都是因为打包时没有将PyQt相关的动态链接库目录生成到打包目录下,因此可以通过将这些需要的文件目录拷贝到打包生成目录下,解决plugin缺失问题。以使用PyQt5编写的python软件打包为例,完成打包后的结果目录下包含PyQt5文件夹,将PyQt5\Qt\plugins下的所有内容(如下图)拷贝到打包结果目录。这样就可以解决PyQt plugins缺失的问题。

Python3中采用PyInstaller打包工程项目

 2.动态链接库缺失问题

一般的,打包后可能会缺失某些动态链接库,造成执行程序出错,如

ImportError: DLL load failed: 找不到指定的模块
在打包过程中一般会有与此相关的warning提示(lib not found)无法找到这些动态链接库。例如在32位版本的打包中,可能会出现scipy模块相关的dll文件无法找到。这时就需要在打包的spec文件中指定动态链接库路径,使其关联到打包后的路径中。
binaries=[(‘路径‘,‘.‘)]

3.Failed to execute script pyi_rth_pkgres

Python3中采用PyInstaller打包工程项目

打包运行xxx.exe文件出现上述问题,是有可能因PyInstaller版本不是最新的造成的,可以通过GitHub网站https://github.com/pyinstaller/pyinstaller/archive/develop.zip下载,下载后解压到某一文件夹中,采用命令行的方式进入解压后的文件夹中,使用命令python setup.py install进行安装。

4.Failed to execute script pyi_rth_certifi

出现此错误的原因是缺少了ssl证书,因此可以从Python官网上下载相应python版本的压缩包,解压后将解压包中的_ssl.pyd(复制到Anaconda目录下DLLs下覆盖原文件),将

libcrypto-1_1.dll、libssl-1_1.dll(复制到Anaconda根目录)即可解决问题。

5.no modules named xxxx

出现该问题是打包后的根目录中没有所需要的模块,因此可以将对应安装库中的文件复制到根目录中,此错误就不再出现。或者可以在hiddenimports中加入缺失的包。或者在datas中作同样的处理。

6.Failed to execute script xxxx

出现该问题是在引用同级目录中的文件时出现,在有python文件出增加__init__.py文件即可解决,该文件可什么都不写。

7.路径冻结

增加一个py文件,例如叫frozenPath.py

import sys
import os
 
def app_path():
    """Returns the base application path."""
    if hasattr(sys, ‘frozen‘):
        # Handles PyInstaller
        return os.path.dirname(sys.executable)
    return os.path.dirname(__file__)

其中的app_path()函数返回一个程序的执行路径,为了方便我们将此文件放在项目文件的根目录,通过这种方式建立了相对路径的关系。源代码中使用路径时,以app_path()的返回值作为基准路径,其它路径都是其相对路径。以本文中使用的python项目打包为例,如下所示:

import frozenPath
# 根目录路径
appPath = frozenPath.app_path()
background = QtGui.QPixmap(appPath+"/img/1.png")

这样不论打包的文件放在哪台电脑上运行都不会出现路径错误。

8.始终还有问题存在

可以采用以下命令解决

pyinstaller --hidden-import=pkg_resources -F xxxx.py

9.顺便再记个PyQt5中QSplitter部件无法显示其他窗口的背景

原因在于主窗口部件无法显示背景,子窗口部件能够显示,基于此可以解决这种问题。

# 设置背景图片
        background = QtGui.QPixmap(appPath+"/img/1.png")
        palette1 = QtGui.QPalette()
        palette1.setBrush(self.backgroundRole(),QtGui.QBrush(background))
        self.handle.setPalette(palette1)
        self.handle.setAutoFillBackground(True)
        # 设置背景透明
        self.main_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)

10.安装PyQt5

使用豆瓣能够告诉下载

下载PyQt5
pip install PyQt5 -i https://pypi.douban.com/simple
下载PyQt5-tools
pip install PyQt5-tools -i https://pypi.douban.com/simple

希望这些能够对还在打包程序路上挣扎的同志们有所帮助。