前段时间写了个数据采集的项目,吭吭哧哧可算是写的差不多可以用了,纯后端,前端由其他同事来搞,和数据库还有kepserver打交道,也遇到了不少的坑。
kepserver的坑先不说,先说说打包程序的时候遇到的。
先说环境:开发环境win10 64位专业版,Python版本:3.6.8,数据库版本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 -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
文件
复制
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
稍微解释一下,第一步编译pyd
文件没什么好说的,编译好了移动pyd
文件到一个新的文件夹里,然后删掉多余的.c
文件。
重命名main文件夹,防止在打包的时候将源代码打包进去。pyinstaller参数添加了–-add-data
参数,用于将pyd
文件整体拷贝进去,省去了手动复制的麻烦,最后,删除无用的文件,完工!!!
参考文章: