Pyinstaller
用户将python程序打包成各个平台可直接运行的程序,也可以算作是对代码加密的一种方式。pyinstaller的安装及使用方式请参考官网。
注:该文章的系统环境是ubuntu
将flask应用打包
项目结构
这是我开发的一个项目,并且已经成功打包并上线运行
-
api
所有的代码都在里面 -
app.py
只有一行代码,from api import create_app
开始打包
下面我们来将该项目打包pyinstaller -F app.py -name app
, 通过这个命令,我们就能将整个项目打包成一个名为app的bin文件。直接运行./app
,你会发现程序没有运行,因为app.py里面只是单纯的引入了app模块,如果你想通过flask run
来执行的话,抱歉,app是个bin文件,不是python模块,会提示找不到app的,简单的解决办法就是在app.py
文件中添加以下代码.
from api import create_app
if __name__ == "__main__":
app = create_app()
app.run()
然后在执行打包命令pyinstaller -F app.py -name app
,这个时候我们的app就可以直接运行了./app
,想要在启动的时候指定端口,主机名等等的参数,使用click.
使用gunicorn
总所周知,flask
使用的是Werkzeug来作为它的WSGI server
,但是性能很一般,生产环境一般会使用其他的WSGI server
, 网上查到有以下WSGI server
:
-
Gunicorn 独角兽,从
Ruby
的Unicorn
移植过来的。 -
uWSGI 比较全能的一个
WSGI server
。 -
mod_wsgi 这个包提供了一个
Apache
模块,并实现了与wsgi
兼容的接口,可以让python程序运行在Apache web server
之上。 -
CherryPy
CherryPy
是Python
的一个HTTP Framework
,然后它也有WSGI server
。
可能还有其他的一些WSGI server
,对于这几种,哪个好,我也不知道,我只对于gunicorn
熟悉,那么要使用gunicorn
,app.py
需添加以下代码:
import gunicorn.app.base
class StandaloneApplication(gunicorn.app.base.BaseApplication):
"""
Custom application
"""
def init(self, parser, opts, args):
pass
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super(StandaloneApplication, self).__init__()
def load_config(self):
config = dict([(key, value) for key, value in iteritems(self.options)
if key in self.cfg.settings and value is not None])
for key, value in iteritems(config):
self.cfg.set(key.lower(), value)
def load(self):
return self.application
if __name__ == "__main__":
options = {
"bind": "127.0.0.1:8000"
}
StandaloneApplication(app, options=options).run()
接下来再执行pyinstaller -F app.py -name app
,./app
就会使用gunicorn
来运行服务了,对于gunicorn
的参数,依然使用click
来搞定。然而,当你开开心心运行程序的时候,突然报错了:
gunicorn.glogging
是啥?它为什么找不到?我要去哪里找它?这是gunicorn
的日志包,但是pyinstaller
在打包的时候没有将它一起打入进去,所以运行是找不到,这里我们需要在打包的时候加个参数:
pyinstaller -F app.py --name app --hidden-import=gunicorn.glogging
这是啥意思呢,因为gunicorn
自身的代码,并没有直接引入这个包,所以需要手动添加,--hidden-import
参数含义请翻阅官方文档。接下来运行./app
,还是报错,为什么路途就这么不顺呢?
这个包是gunicorn
默认的工作类,pyinstaller
在打包的时候也没有将它一起打入进去
pyinstaller -F app.py --name app \
--hidden-import=gunicorn.glogging \
--hidden-import=gunicorn.workers.sync
再次执行./app
,程序就完美使用gunicorn
来运行了。如果你想使用其他的worker_class
,请在打包的时候传入对应的包名,如:
pyinstaller -F app.py --name app \
--hidden-import=gunicorn.glogging \
--hidden-import=gunicorn.workers.sync \
--hidden-import=gunicorn.workers.ggevent
添加命令行工具
像flask一样
从flask 0.11
版本开始,就内建了一个命令行工具flask
,而我们在开发项目的时候,也会添加一些自定义命令,然后通过flask
来执行。为了让我们的打包后的可执行文件能够实现这一功能,修改app.py
代码:
...
if __name__ == "__main__":
...
app.cli()
但是酱紫之后,服务如何来启动呢,我的解决办法是添加一个run
命令到app.cli
里面,大家如果有更好的方法,还望不吝赐教。
...
def run():
"""运行服务"""
options = {
...
}
StandaloneApplication(app, options=options).run()
if __name__ == "__main__":
app.cli.add_command(run)
app.cli()
打包之后,运行./app
和./app run
,运行十分顺利。
支持db命令
flask_migrate
数据库迁移库是个相当棒的工具,flask
命令会自动去添加db
命令,我们也可以把它添加到我们的命令中去:
...
from flask_migrate.cli import db
if __name__ == "__main__":
...
app.cli.add_command(db)
...
之后当你兴高采烈的运行db
命令的时候,又一个拦路虎出现了
找不到flask
应用,我不是app = create_app()
已经创建了么,为啥还要去找FLASK_APP
这个环境变量呢,其实不单单是db
命令会报这个错,就连我们自己写的命令也可能会报这个错,我们先来查看源代码flask_migrate/cli.py
,大概在85行的位置:
...
@with_appcontext
def migrate(directory, message, sql, head, splice, branch_label, version_path,
rev_id, x_arg):
"""Autogenerate a new revision file (Alias for 'revision --autogenerate')"""
_migrate(directory, message, sql, head, splice, branch_label, version_path,
rev_id, x_arg)
这里使用了with_appcontext
这个装饰器,它来自于flask/cli.py
文件:
def with_appcontext(f):
@click.pass_context
def decorator(__ctx, *args, **kwargs):
with __ctx.ensure_object(ScriptInfo).load_app().app_context():
return __ctx.invoke(f, *args, **kwargs)
return update_wrapper(decorator, f)
这个装饰器的作用就是让被装饰的函数在app
的上下文去执行,__ctx.ensure_object(ScriptInfo).load_app()
这个函数就是flask
根据FLASK_APP
环境变量,或者默认的文件名app.py
, wsgi.py
,去找到app
,所以db
使用的app
都是它自己去找到位置然后定义。如果使用的是flask.cli.AppGroup
来定义自己的命令,那么也是一样的逻辑。所以现在要解决的问题是如何把我们手动创建的app
传入进去。很直接的我想到的是current_app
,只要把我们的app
压入栈就可以了。
# overide.py
import click
from functools import update_wrapper
from flask import current_app
from flask.cli import with_appcontext as origin_with_appcontext
def override_with_appcontext(f):
@click.pass_context
def decorator(__ctx, *args, **kwargs):
with current_app.app_context():
return __ctx.invoke(f, *args, **kwargs)
return update_wrapper(decorator, f)
# If the app starts up in Pyinstaller binary mode, the bootloader will set sys.frozen attribute.
if hasattr(sys, "frozen"):
with_appcontext = override_with_appcontext
print("Use override with_appcontext")
else:
with_appcontext = origin_with_appcontext
print("Use original with_appcontext")
# app.py
...
ctx = app.app_context()
ctx.push()
app.cli()
ctx.pop() # 这里的pop运行不到这里来
这里我兼容pyinstaller
打包的运行的和常规运行两种,然后在需要上下文的命令函数加上重写后的with_appcontext
就可以了,而对于flask_migrate.cli.db
,我采用暴力的方式,直接拷贝了它的源代码,然后使用重写的with_appcontext
,然后再打包就可以了。
其他注意点
非python文件的使用
如果你代码里面读取了其他文件的内容,那么在打包的时候,需要把这些文件加上,通过--add-data
来添加, 代码中通过下面代码来判断
base_path = getattr(sys, "_MEIPASS",os.path.realpath(os.path.dirname(__file__)))
。
gunicorn使用gevent
gunicorn
使用gevent
的时候,需要在代码最前面加上
from gevent import monkey
monkey.patch_all(subprocess=True)
本文作者: Lim
版权声明: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。