Pyinstaller
用戶將python程序打包成各個平臺可直接運行的程序涣易,也可以算作是對代碼加密的一種方式裤唠。pyinstaller的安裝及使用方式請參考官網(wǎng)。
注:該文章的系統(tǒng)環(huán)境是ubuntu
將flask應(yīng)用打包
項目結(jié)構(gòu)
這是我開發(fā)的一個項目惰赋,并且已經(jīng)成功打包并上線運行
-
api
所有的代碼都在里面 -
app.py
只有一行代碼忘朝,from api import create_app
開始打包
下面我們來將該項目打包pyinstaller -F app.py -name app
, 通過這個命令,我們就能將整個項目打包成一個名為app的bin文件泉坐。直接運行./app
为鳄,你會發(fā)現(xiàn)程序沒有運行,因為app.py里面只是單純的引入了app模塊腕让,如果你想通過flask run
來執(zhí)行的話孤钦,抱歉,app是個bin文件纯丸,不是python模塊偏形,會提示找不到app的,簡單的解決辦法就是在app.py
文件中添加以下代碼.
from api import create_app
if __name__ == "__main__":
app = create_app()
app.run()
然后在執(zhí)行打包命令pyinstaller -F app.py -name app
觉鼻,這個時候我們的app就可以直接運行了./app
俊扭,想要在啟動的時候指定端口,主機(jī)名等等的參數(shù)坠陈,使用click.
使用gunicorn
總所周知萨惑,flask
使用的是Werkzeug來作為它的WSGI server
,但是性能很一般,生產(chǎn)環(huán)境一般會使用其他的WSGI server
, 網(wǎng)上查到有以下WSGI server
:
-
Gunicorn 獨角獸仇矾,從
Ruby
的Unicorn
移植過來的咒钟。 -
uWSGI 比較全能的一個
WSGI server
。 -
mod_wsgi 這個包提供了一個
Apache
模塊若未,并實現(xiàn)了與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()
接下來再執(zhí)行pyinstaller -F app.py -name app
行冰,./app
就會使用gunicorn
來運行服務(wù)了,對于gunicorn
的參數(shù)伶丐,依然使用click
來搞定悼做。然而,當(dāng)你開開心心運行程序的時候哗魂,突然報錯了:
gunicorn.glogging
是啥肛走?它為什么找不到?我要去哪里找它录别?這是gunicorn
的日志包朽色,但是pyinstaller
在打包的時候沒有將它一起打入進(jìn)去,所以運行是找不到组题,這里我們需要在打包的時候加個參數(shù):
pyinstaller -F app.py --name app --hidden-import=gunicorn.glogging
這是啥意思呢葫男,因為gunicorn
自身的代碼,并沒有直接引入這個包崔列,所以需要手動添加梢褐,--hidden-import
參數(shù)含義請翻閱官方文檔。接下來運行./app
峻呕,還是報錯利职,為什么路途就這么不順呢?
這個包是gunicorn
默認(rèn)的工作類瘦癌,pyinstaller
在打包的時候也沒有將它一起打入進(jìn)去
pyinstaller -F app.py --name app \
--hidden-import=gunicorn.glogging \
--hidden-import=gunicorn.workers.sync
再次執(zhí)行./app
猪贪,程序就完美使用gunicorn
來運行了。如果你想使用其他的worker_class
讯私,請在打包的時候傳入對應(yīng)的包名,如:
pyinstaller -F app.py --name app \
--hidden-import=gunicorn.glogging \
--hidden-import=gunicorn.workers.sync \
--hidden-import=gunicorn.workers.ggevent
添加命令行工具
像flask一樣
從flask 0.11
版本開始热押,就內(nèi)建了一個命令行工具flask
,而我們在開發(fā)項目的時候斤寇,也會添加一些自定義命令桶癣,然后通過flask
來執(zhí)行。為了讓我們的打包后的可執(zhí)行文件能夠?qū)崿F(xiàn)這一功能娘锁,修改app.py
代碼:
...
if __name__ == "__main__":
...
app.cli()
但是醬紫之后牙寞,服務(wù)如何來啟動呢,我的解決辦法是添加一個run
命令到app.cli
里面,大家如果有更好的方法间雀,還望不吝賜教悔详。
...
def run():
"""運行服務(wù)"""
options = {
...
}
StandaloneApplication(app, options=options).run()
if __name__ == "__main__":
app.cli.add_command(run)
app.cli()
打包之后,運行./app
和./app run
惹挟,運行十分順利茄螃。
支持db命令
flask_migrate
數(shù)據(jù)庫遷移庫是個相當(dāng)棒的工具,flask
命令會自動去添加db
命令连锯,我們也可以把它添加到我們的命令中去:
...
from flask_migrate.cli import db
if __name__ == "__main__":
...
app.cli.add_command(db)
...
之后當(dāng)你興高采烈的運行db
命令的時候归苍,又一個攔路虎出現(xiàn)了
找不到flask
應(yīng)用,我不是app = create_app()
已經(jīng)創(chuàng)建了么运怖,為啥還要去找FLASK_APP
這個環(huán)境變量呢拼弃,其實不單單是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)
這個裝飾器的作用就是讓被裝飾的函數(shù)在app
的上下文去執(zhí)行吗购,__ctx.ensure_object(ScriptInfo).load_app()
這個函數(shù)就是flask
根據(jù)FLASK_APP
環(huán)境變量医男,或者默認(rèn)的文件名app.py
, wsgi.py
,去找到app
捻勉,所以db
使用的app
都是它自己去找到位置然后定義镀梭。如果使用的是flask.cli.AppGroup
來定義自己的命令,那么也是一樣的邏輯踱启。所以現(xiàn)在要解決的問題是如何把我們手動創(chuàng)建的app
傳入進(jìn)去报账。很直接的我想到的是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
打包的運行的和常規(guī)運行兩種透罢,然后在需要上下文的命令函數(shù)加上重寫后的with_appcontext
就可以了,而對于flask_migrate.cli.db
冠蒋,我采用暴力的方式羽圃,直接拷貝了它的源代碼,然后使用重寫的with_appcontext
抖剿,然后再打包就可以了朽寞。
其他注意點
非python文件的使用
如果你代碼里面讀取了其他文件的內(nèi)容,那么在打包的時候斩郎,需要把這些文件加上脑融,通過--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
版權(quán)聲明: 著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處膜宋。