通过Pyinstaller打包编译好的pyd文件到exe

前段时间写了个数据采集的项目,吭吭哧哧可算是写的差不多可以用了,纯后端,前端由其他同事来搞,和数据库还有 kepserver 打交道,也遇到了不少的坑。

kepserver 的坑先不说,先说说打包程序的时候遇到的。

先说环境:开发环境 win10 64 位专业版,Python版本:3.7.4,数据库版本 SQL server 2014,kepserver 6.4

由于是用 Python 写的,所以打包就是个问题,目标服务器没有 Python 环境,只有一个数据库。所以就需要将程序打包为 exe 文件直接发过去(有环境我也不想给代码啊,哈哈)

由于对 pyinstaller 比较熟悉,而且个人感觉兼容性好像比较好,所以就用它了。

先来一张文件结构截图(不要吐槽我的文件命名,第一次写项目没有经验):

config.ini 是配置文件,main 是一个包,执行方式呢就目前来说,Python run_ua_sync.py就可以了,为了方便起见(其实是懒),写了个start.bat,直接通过命令行执行cmd.exe /K Python run_ua_sync.py,作用就是新开一个cmd,然后通过cmd执行后边的命令,这样有一个好处就是可以在程序抛异常的时候可以方便的看到堆栈跟踪的错误提示,不会直接闪退。

pyinstaller 怎么使用就不说了, 这不是重点,如果想知道的请移步 pyinstaller官网

首先第一步:在项目目录命令行直接执行pyinstaller -F run_ua_sync.py -i 1.ico就可以了,然后就会在当前目录生成一个对应的run_ua_sync.exe然后就可以直接发给对方运行了。然后就结束了?不不不,怎么可能这么快结束,pyd文件还没上场呢。

考虑到 Python 运行效率和源代码保护的问题,直接发过去肯定是不行的。因为 pyinstaller 直接打包好的文件是可以被反编译为pyc文件的,所以在这里我们要把我们写好的py文件编译为pyd文件。

pyd文件是什么呢?pyd文件单看文件结构来说的话就是 windows 上的dll文件,有标准的PE头,通过 Cython 编译而来。cython 会在 windows 上将py文件编译为pyd文件,在 Linux 上编译为so文件。有关 Cython 的解释详见cython官网cython文档

我们继续往下,既然提到了编译,肯定需要编译器了,所以这时候就要安装我们的宇宙最强 IED Visual Studio了。为什么要安装它呢?傻瓜化安装,简单粗暴,只要在安装的时候勾选 C++ 支持就可以了。

接下来就可以安装 Cython 了,在命令行中执行pip install cython就可以了,和安装Python的其他依赖是一样的。安装好之后我们要新建一个setup.py文件,来指定需要编译的和需要排除的文件。

setup.py 文件的内容为

1
2
3
4
5
6
7
from distutils.core import setup
from Cython.Build import cythonize

setup(
name='Hello world app',
ext_modules=cythonize(module_list="main/*.py"),
)

setup 函数中的name参数为文件的名字,不过据我测试好像无所谓,填一个就行。ext_modules参数为需要编译的py文件。因为我需要编译的是一个包,好多个文件,而不是一个文件,所以要用通配符来表示。

建好文件以后,命令行中运行python setup.py build_ext --inplace就可以开始编译了。如果这里有提示其他的异常的话最好去看一下VS的环境变量,因为这里涉及到编译器和链接器的环境变量的设置,如果设置不对会找不到编译脚本的。我这里是一次过,没什么问题。

这里我遇到了第一个坑。因为 main 文件夹是一个包,所以我新建了一个空白__ini__.py文件,告诉Python解释器这是一个包。但是在编译的时候提示了错误。如下图:

错误原因未知,也没有查询到有用的提示。所以这里我选择了忽略,忽略掉__init__.py文件的编译,反正是空白的,不编译应该也不影响。

忽略掉之后 setup.py 的内容就变为了

1
2
3
4
5
6
7
from distutils.core import setup
from Cython.Build import cythonize

setup(
name='Hello world app',
ext_modules=cythonize(module_list="main/*.py" , exclude='main/__init__.py'),
)

在exclude参数上传入__init__.py,让编译器在编译的时候忽略它。

据我测试还没有发现忽略之后有什么异常。

如果看到下面的提示就是已经编译好了。可以去到main文件夹中看一下是不是生成了对应的pyd文件。

……

正在创建库 build\temp.win-amd64-3.6\Release\main\util_ua_sync.cp36-win_amd64.l
ib 和对象 build\temp.win-amd64-3.6\Release\main\util_ua_sync.cp36-win_amd64.exp
正在生成代码
已完成代码的生成

正常情况下的话main文件夹应该有三种文件,.py文件,.c文件,.pyd文件。

.c文件是.py文件转换得来的,通过编译器和链接器将.c文件编译为.pyd文件。

这时候如果删掉所有的.py文件和.c文件程序仍然可以正常运行。因为 Python 导入文件的时候.pyd文件的优先级是高于.py文件的。

编译的活干完了,下面就应该打包exe程序了。这时候执行pyinstaller run_ua_sync.py -i 1.ico

为什么这里没有加-F参数呢,因为这里还是会出问题,所以不打包成单文件,打包成一个文件夹,方便查找问题。

打包完成,看看打包好的文件夹里都有什么。

第二个坑来了!

双击 run_ua_sync.exe 运行程序,报错如下:

1
2
3
4
5
6
7
Traceback (most recent call last):
File "run_ua_sync.py", line 2, in <module>
from main.EnumWindow import EnumWindows
File "main\EnumWindow.py", line 5, in init main.EnumWindow
import win32gui
ModuleNotFoundError: No module named 'win32gui'
[2252] Failed to execute script run_ua_sync

提示找不到win32gui这个包。

经过一番尝试,找到了原因,这是因为编译成pyd文件之后,pyd文件是二进制文件,所以 pyinstaller 在查找需要导入的包的时候会无法分析pyd文件,导致不知道pyd文件里边导入了什么第三方的依赖,却能分析了run_ua_sync.py文件中所需要导入的依赖,所以只拷贝了run_ua_sync.py文件所需要的pyd文件,却没有提前把pyd所依赖所需要的文件打包的虚拟环境中去,所以在运行时会提示找不到。从哪里看出来的呢?看main文件夹中的文件数量。

这是 pyinstaller 打包好的main文件夹中的pyd文件,文件数量正好等于run_ua_sync.py文件中所导入的模块的数量

这是编译好的main文件夹中的pyd文件,为什么会少这么多呢?

原因在于,pyinstaller 打包的时候只分析了run_ua_sync.py文件中需要导入的包,所以在导入时就直接导入了run_ua_sync.py所需要的pyd文件就完事了。这样肯定是不行的。而且pyd文件里所需要的依赖文件仍然没有导入。

所以我们来解决问题,把 main 文件夹中所有的pyd文件复制到 pyinstaller 打包好的main文件夹中去,然后在打包的时候加上隐藏导入–-hidden-import参数。

所以需要执行的命令就变成了这样:

1
pyinstaller -F run_ua_sync.py --hidden-import win32gui --hidden-import logging.handlers --hidden-import configparser --hidden-import pymssql --hidden-import opcua

pyd文件中依赖的第三方包全部通过参数传进去,然后将编译好的pyd文件全部拷贝进去就可以解决上边的问题了。

最后贴一下我最终的bat文件,编译好之后新建一个临时文件夹,将pyd文件移动到临时文件夹中,删除多余的.c文件,重命名main文件夹,防止 pyinstaller 打包源代码,然后进行正常的 pyinstaller 打包流程,打包是加上add-data参数添加编译好的pyd文件,打包完成,将源代码文件夹名字还原,删掉临时文件夹和一些编译的中间文件,完成!

1
2
3
4
5
6
7
8
9
10
11
12
13
python setup.py build_ext --inplace
mkdir temp
move main\*.pyd temp
del main\*.c
del run_ua_sync.exe
rename main aaa
pyinstaller -F run_ua_sync.py --hidden-import win32gui --hidden-import logging.handlers --hidden-import configparser --hidden-import pymssql --hidden-import opcua --distpath ./ --clean --add-data ./temp;main -i 1.ico
del run_ua_sync.spec
rename aaa main
rmdir /s /q temp
rmdir /s /q build
rmdir /s /q __pycache__
pause

参考文章:

http://yshblog.com/blog/117

https://www.pyinstaller.org

https://cython.org/

http://docs.cython.org/en/latest/

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接https://blog.d77.xyz/archives/75cf9cb3.html