PyInstaller 系列 - 基本用法

版权声明:所有博客文章除特殊声明外均为原创,允许转载,但要求注明出处。

这是本系列的第一篇,介绍 PyInstaller 的基础知识。

PyInstaller 是一个跨平台的 Python 应用打包工具,支持 Windows/Linux/MacOS 三大主流平台,能够把 Python 脚本及其所在的 Python 解释器打包成可执行文件,从而允许最终用户在无需安装 Python 的情况下执行你的程序。

PyInstaller 这个软件有几点容易引起误解的地方。首先,Installer 的名字可能会让用户望文生义地觉得这是个安装程序制作软件,类似 NSIS/WIX/InstallShield 等等,但实际上 PyInstaller 的作用是打包而不是安装。这可能是因为类似 py2exe 这样的好名字早就被其他项目用掉了吧。其次,PyInstaller 制作出来的执行文件并不是跨平台的。如果你需要分别为三大平台打包的话,那么你就要在三个平台上分别运行 PyInstaller,不要指望“一次编译,到处运行”(当然可以利用虚拟机简化多环境配置)。

介绍 PyInstaller 的资料在网上并不少,但大多是一些简单的运行和命令行说明,缺乏较为深入和系统性的讲解。同时,许多同学使用 PyInstaller 的过程中会遇到各种各样的问题,如果你不明白它的运行原理的话,有些问题就很难靠自己解决。这也是我编写本系列文章的原因。

本文系列中的实践部分,如无特别说明的话,均以 Windows 为系统平台————这应该也是大多数 PyInstaller 使用的环境。我自己的机器是 Windows 10,如果你用的是 Windows 7/8,也没有关系,PyInstaller 在 Windows 系列上的用法没什么不同。Mac 是另外一个较大的平台,但本文不会涉及,有问题的同学可以参考官方手册中针对 Mac 平台的特别说明。此外,现在已经是 2018 年,故本文不会考虑 Python 2.x 系列的兼容问题。

接下来介绍 PyInstaller 的安装和运行。

安装 PyInstaller

Python 3 以上版本安装 PyInstaller 最方便的方法当然是通过 PIP。直接运行如下命令即可:

pip install PyInstaller

然后等待安装结束。PyInstaller 会安装一些相关的库,例如在 Windows 下会同时安装 pypiWin32(pywin32的更新版本)。

可以用如下命令确认 PyInstaller 安装成功:

pyinstaller --version
> 3.3.1

运行 PyInstaller

PyInstaller 最简单的运行形式,只需要指定作为程序入口的脚本文件。假定这个文件叫做 main.py:

pyinstaller main.py

运行之后,我们会看到目录中多了这些内容:

  • main.spec 文件,其前缀和脚本名相同。它本质上是一个 Python 文件,指定了打包时所需的各种参数,熟悉格式以后也可以手工修改,其角色类似于 setup.py;
  • build 子目录,其中存放打包过程中生成的临时文件。有几个文件需要关注一下:
  • warnxxxx.txt:该文件记录了生成过程中的警告/错误信息。在一切正常的情况下,该文件也可能包含若干类似 missing module 的信息,这些信息并不表示出错,但如果 PyInstaller 运行有问题的话,你就需要检查这个文件来获取错误的详细内容了。
  • xref-xxxx.html:该文件输出了 PyInstaller 分析脚本得到的模块依赖关系图。如果出现类似找不到模块的信息,你可以查找这个文件来获得一些线索。但该文件的内容比较庞大,一般情况下很少会直接使用它。
  • dist 子目录,存放生成的最终文件。如果使用单文件模式————后面会讲到),那么这里将只有单个执行文件;如果使用目录模式的话,那么这里还会有一个和脚本同名的子目录,其下才是真正的可执行文件(以及相关的其他附属文件)。双击可执行文件就会启动程序。

Dist 目录内容

以上是在我机器上生成的目录内容(部分)。可以发现,对于基本的应用程序来说,Python 解释器本体(pythonxx.dll)是其中的大头。当然程序文件(main.exe)体积也不小,不过同学们也不要被吓怕了——占用空间的都是一些恒定的 Bootstrapper 内容。当你继续增加代码时,文件体积只会以 KB 为单位缓慢增加,不会再有如此恐怖的增长了。此外就是一些操作系统相关的辅助文件(这里是 Windows 的 UCRT 库)。

PyInstaller 命令行选项

在上面的例子里,我们用最简单的方式使用了 PyInstaller。实际上 PyInstaller 有相当多的命令行开关,可以从各个方面调整打包过程的行为。用如下命令就能看到所有支持的开关项:

pyinstaller --help

由于可用的选项如此之多,这里并不会逐个讲解,只主要介绍经常使用的几个开关。其他选项的详细内容可以参考官方手册。

  • -y | --noconfirm 直接覆盖输出文件,而无需提示。在多次重复运行命令时可避免烦人的反复确认。

  • -D | --onedir 生成包含执行文件的目录(这是默认行为)

  • -F | --onefile 和上一个选项相对,生成单一的可执行文件。注意,这个选项会带来一些特殊的行为,并且我个人强烈地不推荐你使用这个选项。具体的原因我们在后面的文章中讨论。

  • -i | --icon [.ico | .exe | .icns] 为 Windows/Mac 平台的执行文件指定图标。如果是 .exe 的话,还可以在后面加上 ,id 参数来指定具体的图标ID。

  • --version-file [filename] 添加文件版本信息。后面我们会通过一个具体的例子说明其用法。

  • -c | --console | --nowindowed 通过控制台窗口运行程序 并且分配标准输入/输出,(默认行为)。

  • -w | --windowed | --noconsole 和上一个选项相反,不创建控制台窗口,也不分配标准输入/输出。它们主要用来运行 GUI 程序。需要说明的是,没有输入输出会给调试带来一定困难,因此即便你编写的是 GUI 程序,也建议在调试时禁用这个选项,在最终发布时再打开。

  • --add-data [file:dir] 添加数据文件。如果有多个文件需要添加的话,该选项可以出现多次。注意,文件参数的格式为文件名+输出目录名,用路径分隔符分割。路径分割符,从技术上来讲就是 os.pathsep,在 Windows 下使用 ;,其他系统下则使用 :。 如果输出到和脚本相同的目录,则使用 . 作为输出目录。

  • --add-binary [file:dir] 添加二进制文件,即运行程序所需的 .exe/.dll/.so 等。其选项格式和 --add-data 相同,不再赘述。

通过 Shell 脚本/批处理运行 PyInstaller

看到上述这么多命令,你应该可以想象,通过命令行输入这么多参数应该是一件多么痛苦的事情。这个问题有两个解决办法。第一,我们可以把需要的命令保存成 Shell/批处理脚本。第二,也可以通过运行 .spec 文件来达到同样的效果。目前,我们还没有介绍 .spec 文件的内容,作为示例,我们先使用第一个方法。

为了避免命令行过长,在脚本中最好是使用换行符。类 Unix 系统和 Windows 系统的换行符是不同的(此外也要注意路径分隔符的写法)。Linux 中一个脚本的例子大概是这样的:

pyinstaller --noconfirm --onedir --windowed \
    --add-data="README.txt:." \
    --add-data="sample.png:img" \
    --add-binary="mylib.dll:lib" \
    --icon="main.ico" \
    --version-file="version.txt" \
    main.py

而 Windows 版本则是这样:

pyinstaller --noconfirm --onedir --windowed ^
    --add-data="README.txt;." ^
    --add-data="sample.png;img" ^
    --add-binary="mylib.dll;lib" ^
    --icon="main.ico" ^
    --version-file="version.txt" ^
    main.py

使用 UPX 优化执行文件大小

有不少用户会觉得 PyInstaller 生成的文件太大,不便于发布。由于 Python 解释器本身的体积很难裁剪,所以 PyInstaller 提供了一个方案。如果 PyIntaller 在系统路径上发现了 UPX,那么它会用 UPX 来压缩执行文件。如果你确实有 UPX,但又并不希望裁剪的话,可以用--no-upx 开关来屏蔽它。

尽管 UPX 对大多数程序都能有不错的压缩率,但作为代价,使用 UPX 以后程序执行速度可能会有所减慢。此外,个别程序也存在不兼容 UPX 的情况,因此请你自己权衡是否值得使用。

为执行文件添加版本信息

上述大多数命令行选项都是相当直观的,相信读者参考脚本的例子就能看懂,不需要多作解释。但有一个选项有点复杂,值得拿出来说一下,那就是 --version-file

在 Windows 下,为应用程序添加版本信息是一种专业的表现,并且对软件的排错和更新也可能会有所帮助。但版本信息是一个复杂的结构————技术性的介绍可参考MSDN(https://msdn.microsoft.com/en-us/library/ff468916(v=vs.85).aspx )————自己生成版本记录文件是比较困难的。PyInstaller 考虑到了这一点,因此它为你提供了一个用于提取版本记录的辅助命令:pyi_grab_version。如果你的 PyInstaller 是通过 PIP 安装的,那么该程序应该位于 Pythonxx\Scripts 子目录下,并且如果 Python 已经添加到系统 PATH 的话,你可以直接运行此命令:

pyi-grab_version

注意中划线和下划线的写法,如果找不到命令的话,请检查一下 Scripts 子目录。

这里作为例子,我们直接从系统程序里“偷”一个版本信息过来:

pyi-grab_version c:\windows\notepad.exe version.txt

然后打开生成的 version.txt, 会看到类似下面的内容:

# UTF-8
VSVersionInfo(
  ffi=FixedFileInfo(
    # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
    # Set not needed items to zero 0.
    filevers=(10, 0, 17134, 1),
    prodvers=(10, 0, 17134, 1),
  ...
  kids=[
    StringFileInfo(
      [
      StringTable(
        u'040904B0',
        [StringStruct(u'CompanyName', u'Microsoft Corporation'),
        StringStruct(u'FileDescription', u'Notepad'),
        StringStruct(u'FileVersion', u'10.0.17134.1 (WinBuild.160101.0800)'),
        StringStruct(u'InternalName', u'Notepad'),
        StringStruct(u'LegalCopyright', u'© Microsoft Corporation. All rights reserved.'),
        StringStruct(u'OriginalFilename', u'NOTEPAD.EXE'),
        StringStruct(u'ProductName', u'Microsoft® Windows® Operating System'),
        StringStruct(u'ProductVersion', u'10.0.17134.1')])
      ]), 
    VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
  ]
)

文件略长,这里只列出重要的部分,其结构还是很清楚的。通常,我们需要修改的是其中的版本信息和一些辅助性的描述。将文中内容修改如下:

# UTF-8
VSVersionInfo(
  ffi=FixedFileInfo(
    filevers=(1, 0, 1, 1),
    prodvers=(1, 0, 1, 1),
    ...
    ),
  kids=[
    StringFileInfo(
      [
      StringTable(
        u'040904B0',
        [StringStruct(u'CompanyName', u'Sample Corp'),
        StringStruct(u'FileDescription', u'PyInstaller application'),
        StringStruct(u'FileVersion', u'1.0.0.1'),
        StringStruct(u'InternalName', u'PyInstaller application'),
        StringStruct(u'LegalCopyright', u'Sample Corp.'),
        StringStruct(u'OriginalFilename', u'main.exe'),
        StringStruct(u'ProductName', u'PyInstaller application'),
        StringStruct(u'ProductVersion', u'1.0.0.1')])
      ]), 
    VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
  ]
)

然后编译程序。从资源管理器找到发布目录下的文件(本例中应为 dist/main/main.exe),右键查看属性:

.EXE 版本信息

可见版本信息已经成功添加到 .EXE 文件中了。

在下一篇文章中,我们将介绍什么是单文件/单目录模式,以及它们之间的区别。

PyInstaller 系列索引