2019年10月23日 星期三

通过pyinstaller打包编译好的pyd文件到exe (轉貼)

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

前段时间写了个数据采集的项目,吭吭哧哧可算是写的差不多可以用了,纯后端,前端由其他同事来搞,和数据库还有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怎么使用就不说了, 这不是重点,如果想知道的请移步 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文件
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文件整体拷贝进去,省去了手动复制的麻烦,最后,删除无用的文件,完工!!!
参考文章:

沒有留言:

張貼留言