在使用pyinstaller库的过程中遇到几个问题,看了下源码,发现造成这几个问题的原因类似,感觉挺有意思,之前说过,pyinstaller其实就是分析python文件中的import 语句,然后打包成pyd或都是dll等库文件,但是对于一些python中使用动态导入或者是使用指定路径等形式,或者换句话说就是:在运行中才能确定导入了哪些库或者是使用了哪些dll,这种情况下pyinstaller是无法自动辨别的,下面遇到的三个问题都是由于动态导入的问题,这个时候就需要使用pyinstaller更高级的用法了.
问题
错误一: No executor by the name “threadpool” was found
在上一篇介绍jobstore时我们知道apscheduler中默认的执行器为threadpool,而且有3种初始化写法,具体请参考这篇官网,而那对于flask-apscheduler,默认的初始化方式也很json风格
1 | class Config(object): |
上面指定了默认的执行器为threadpool,按照flask-apscheduler用户手册来说这样写没有问题,事实上单独写成脚本执行也没有问题,但是一整合到flask web中,就会报如下错误,提示找不到threadpool执行器:
查看flask-apscheduler源码,有如下函数:
1 | def _load_config(self): |
该函数从app中get到配置,然后通过字典形式传入_scheduler中,_scheduler是一个BackgroundScheduler()对象,而这个对象又是继承于BaseScheduler(),看BaseScheduler类这完全没问题,那为何找不到threadpool呢?
问题二:No modules named ‘reportlab.graphics.barcode.common’
同样的问题,单独脚本执行可以,使用pyinstaller打包flask成all-in-one就提示找不到库了,当时还以为是reportlab库需要适配,也是追了reportlab的源码,没发现什么问题,reportlab出问题的源码如下:
1 | def _BCW(doc,codeName,attrMap,mod,value,**kwds): |
很明显上面的代码中有code = 'from %s import %s' % (mod,codeName)
,动态导入方式.
问题三:dlopen() failed to load a library: cairo / cairo-2
1 | try: |
上面最开始那句是cairocffi的源码,可以看出这里使用了os.path.join(os.path.dirname(_file_,’cairo.dll’),其实就是使用该目录下的cairo.dll文件,这也是只能在脚本运行时才能确定路径,所以pyinstaller运行时会产生异常.
那么最重要的问题来了,同样也是解决上面3个问题的方法
解决
pyinstaller之hiddenimports
从字面上可以理解,隐藏式导入就是可以不以代码为标准直接导入指定的模块,问题二:可以直接使用hiddenimports导入pyinstaller不能自动导入的模块:
1 | hiddenimports = [ |
这样pyinstaller在打包的时候会把上面指定的模块也打包进去,是不是很方便
pyinstaller之打包二进制文件
问题三则是pyinstaller无法打到dll文件,这个时候可以使用binaries指定,
1 | a = Analysis(['xxx.py'], |
pyinstaller则会在当前目录下查找cairo.dll打包进exe,exe执行的时候会把cairo.dll解压到pyinstaller的临时生成的解压路径,而且也把问题三的源码修改成了case3,不需要再使用os.path就能找到了
而问题一则需要改成如下声明:
1 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore |
总结
总之就是一句话,在打包好的exe运行所需要的环境需要在pyinstaller解压路径下存在,对于在程序运行时才能确定的模块则需要额外处理了,可以指定路径(不是所有的机器都有环境,要不然就不需要一键打包了),可以hiddenimports,也可以hooks,hooks意为勾子,也是pyinstaller的一种查找模块机制,下次再研究.