使用pyinstaller將flask應(yīng)用打包

Pyinstaller

用戶將python程序打包成各個平臺可直接運行的程序涣易,也可以算作是對代碼加密的一種方式裤唠。pyinstaller的安裝及使用方式請參考官網(wǎng)

注:該文章的系統(tǒng)環(huán)境是ubuntu

將flask應(yīng)用打包

項目結(jié)構(gòu)

這是我開發(fā)的一個項目惰赋,并且已經(jīng)成功打包并上線運行


項目結(jié)構(gòu)
  • 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 獨角獸仇矾,從RubyUnicorn移植過來的咒钟。
  • uWSGI 比較全能的一個WSGI server
  • mod_wsgi 這個包提供了一個Apache模塊若未,并實現(xiàn)了與wsgi兼容的接口,可以讓python程序運行在Apache web server之上倾鲫。
  • CherryPy CherryPyPython的一個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)載請注明出處膜宋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末窿侈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子秋茫,更是在濱河造成了極大的恐慌,老刑警劉巖乃秀,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躺涝,死亡現(xiàn)場離奇詭異释漆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門埠胖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人止吐,你說我怎么就攤上這事最住。” “怎么了愈污?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵耀态,是天一觀的道長。 經(jīng)常有香客問我暂雹,道長首装,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任杭跪,我火速辦了婚禮仙逻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘涧尿。我一直安慰自己系奉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布姑廉。 她就那樣靜靜地躺著缺亮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪庄蹋。 梳的紋絲不亂的頭發(fā)上瞬内,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機(jī)與錄音限书,去河邊找鬼虫蝶。 笑死,一個胖子當(dāng)著我的面吹牛倦西,可吹牛的內(nèi)容都是我干的能真。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼粉铐!你這毒婦竟也來了疼约?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蝙泼,失蹤者是張志新(化名)和其女友劉穎程剥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汤踏,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡织鲸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了溪胶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搂擦。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哗脖,靈堂內(nèi)的尸體忽然破棺而出瀑踢,到底是詐尸還是另有隱情,我是刑警寧澤才避,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布橱夭,位于F島的核電站,受9級特大地震影響工扎,放射性物質(zhì)發(fā)生泄漏徘钥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一肢娘、第九天 我趴在偏房一處隱蔽的房頂上張望呈础。 院中可真熱鬧,春花似錦橱健、人聲如沸而钞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臼节。三九已至,卻和暖如春珊皿,著一層夾襖步出監(jiān)牢的瞬間网缝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工蟋定, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留粉臊,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓驶兜,卻偏偏與公主長得像扼仲,于是被迫代替她去往敵國和親远寸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容