最近做个兴趣小项目,需要用python来get/post https请求,用到了requests模块,因为要在windows下使用,考虑到易用性和小白玩家的需求,使用py2exe生成exe。参考链接看起来很简单,统共分三步:1,安装py2exe;2,准备一个配置脚本,我们叫他build_exe.py;3,执行一条命令python build_exe.py py2exe
先把这个简单的初级版本放这里:
#usage: python build_exe.py py2exe
from distutils.core import setup
import py2exe
setup(console=[‘myqqbot.py’])
但是这里其实还是有坑的,安装py2exe要注意,并不是直接pip install py2exe就可以了,py2exe是人家python3专用的,python2的是py2exe_py2,悲剧吧,所以装包前最好还是pip search下,免得浪费时间还不知道错在哪。配置脚本貌似简单,大家应该能想到这里面其实是有很多玩法的,以至于需要一个脚本而不是直接传参。
生成过程很顺利,不过几秒exe文件已经躺在dist目录下了。但是双击他,console一闪而过,都不知道发生了什么。Windows就是这么变态,反正把你当小白,不给你任何错误信息。在超级难用的cmd里面用命令行调用exe后发现了问题:
requests.exceptions.SSLError: [Errno 2] No such file or directory
万能的stackoverflow告诉我们,不要慌我有药,药在这。so大夫告诉我们在exe文件执行的时候环境变量发生了改变,找不到这个pem文件了,我们需要给一个静态的pem文件路径,或者设置一个REQUESTS_CA_BUNDLE环境变量,因为不知道这个exe文件会在什么环境下执行,不能保证所在的环境中有这个pem文件,所以我们必须在打包的时候打一个pem文件进去。到此产生两个新问题:1,如何获取cacert.pem文件;2,怎么用py2exe打包,当然你肯定能想到直接放到生成的dist目录下面就可以了,但是这样是不是太low了,我们要有更elegant的方法。
获取pem文件,so大神给出了简约而不简单的实现:
import certifi
print certifi.where()
仅此两行,完整的pem文件路径就出炉了。
打包pem文件,这就要改一下上面的build_exe.py了,新玩法不得不get一下了:
#usage: python build_exe.py py2exe
from distutils.core import setup
import py2exe
setup(console=[‘myqqbot.py’],
data_files=[(“certifi”, [“cacert.pem”])] #1
#,(“fonts”, glob.glob(“fonts\\*.fnt”))], #2
)
上面代码说明一下:#1,这个括号里面两个字段的方法其实是给了一个子目录名“certifi”, 这里一定要把文件名放在[]里面,因为默认一个目录下可以同时收录多个文件的。当然可以不要这个子目录,可以这样写data_files=[“cacert.pem”],直接写文件名的方法就是默认这个文件在工程根目录下,也就是和build_exe.py在一个目录。所以这里其实可以写绝对路径直接指向python安装目录下的那个pem文件。#2,可以同时指定另外的子目录和需要打包的文件,还可以用glob这个方法来做模糊匹配。
结合路径查找和打包脚本,一个优美的实现如下:
#usage: python build_exe.py py2exe
from distutils.core import setup
import py2exe
import certifi
setup(console=[‘myqqbot.py’],
data_files=[certifi.where()])
因为考虑到exe执行环境问题,所以我们打包了个pem文件到exe文件的执行目录下,这样我们只能将exe执行时的REQUESTS_CA_BUNDLE环境变量设置为当时执行目录的绝对路径+pem文件相对执行目录的相对路径。
比如:os.environ[‘REQUESTS_CA_BUNDLE’] = os.path.join(os.path.abspath(‘.’), ‘certifi\cacert.pem’)
这里又挖了新坑,如果在工程目录直接执行py脚本而不是在dist目录执行exe文件,那么当前目录下根本没有pem文件,那么程序就直接byebye了,这不是我们追求完美的程序员能接受的,我们还要调试程序呢,每次编译成exe再执行,开玩笑呢,鱼和熊掌我们都要:
cacert_path = os.path.join(os.path.abspath(‘.’), ‘certifi\cacert.pem’)
if os.path.exists(cacert_path):
os.environ[‘REQUESTS_CA_BUNDLE’] = cacert_path
于是,我们py和exe都可以愉快的执行了~~
至于cacert.pem到底是什么鬼,这个接下来再研究了