Flask從0到1快速後臺服務開發
版本說明:
Python:3.7
Flask:1.0.2
前言
Flask是一個使用 Python 編寫的輕量級 Web 應用框架。其 WSGI 工具箱採用 Werkzeug ,模板引擎則使用 Jinja2 ,具體詳情查看官網:http://flask.pocoo.org/。
接觸Flask有一段時間了,在工作中使用Flask開發了幾個輕量級的後臺服務,相比較Django框架,Flask更加的輕量,爲漸進式框架,適合快速開發。這裏不做深入的源碼研究,只是記錄一下在工作中使用Flask的經驗技巧,從0到1快速進行後臺開發。
1 環境準備
1.1 Conda創建Python開發環境
這裏爲方便演示,使用conda創建一個名字爲flask,版本爲3.5的新環境。如果沒有安裝conda,可以從官網下載安裝即可,conda官網地址:https://www.anaconda.com/。
conda create --name flask python=3.7
1.2 創建Flask項目
使用PyCharm創建一個名爲flask-demo的項目,並選擇我們剛纔創建的python環境。
1.3 安裝Flask
flask可以使用python的pip直接安裝,新版PyCharm選擇Interpreter之後,點擊Terminal可以直接切換到該環境。
pip install flask==1.0.2
2 快速創建一個Web服務
使用Flask創建一個web服務很簡單,只需要通過Flask()創建一個Flask實例app,然後通過app.route()裝飾器設置路由方法,最後通過app.run()啓動內置的開發服務器即可。下面創建一個名爲simple_app.py的python的文件,內容如下:
from flask import Flask
# create a flask app
app = Flask(__name__)
@app.route("/")
def index():
return "Hello world !"
if __name__ == '__main__':
# run server
app.run(host="0.0.0.0", port=5000)
右擊運行main方法即可啓動服務,訪問http://0.0.0.0:5000/
3 Flask請求處理
上面我們已經啓動了一個簡單的服務,在web服務裏有個關鍵的地方就是對請求的處理,如:獲取請求信息,返回請求結果等。
3.1 指定路由請求類型
常見的請求類型:GET、POST、PUT、DELETE、PATCH等等,Flask允許我們指定某個請求可以通過哪些類型進行訪問。可以在路由裝飾器中傳入methods需要的參數,該參數是一個列表,如只允許GET請求,可以設置methods=[‘GET’],如允許GET和POST,可以設置methods=[‘GET’,‘POST’]。如設置下面/methods這個路由只允GET請求類型的訪問。
@app.route("/methods", methods=["GET"])
def methods():
return "Only allow GET request types"
3.2 獲取請求參數
請求傳參有很多方法,可以通過URL直接傳參、通過body傳參、Header、Cookie、Session等。Flask中關於請求相關的上下文信息,保存在兩個對象裏,一個是request裏,另一個是session裏。我們可以從這兩個對象裏獲取所有我們想要獲取的參數。如下源碼所示:
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
引入對象:
from flask import request
from flask import session
這裏做點補充,上面提到Flask的上下文信息,從源碼中我們可以看出,除了request對象和session對象之外,flask還提供了current_app和g兩個對象。
變量名稱 | 上下文 | 說明 |
---|---|---|
current_app | 應用上下文 | 當前Flask應用的應用實例 |
g | 應用上下文 | 處理請求時用作臨時存儲的對象,每次請求都會重設這個變量 |
requset | 請求上下文 | 請求對象,分裝了客戶端發出的HTTP請求中的內容 |
session | 請求上下文 | 用戶會話,值作爲一個字典,存儲請求之前需要"記住"的值 |
request請求對象
屬性或方法 | 說明 |
---|---|
form | 字典,存儲請求提交的所有表單數據 |
args | 字典,存儲通過URL傳遞的所有參數 |
values | 字典,form和args的合集 |
cookies | 字典,存儲請求的所有cookie |
headers | 字典,存儲請請的所有HTTP首部 |
files | 字典,存儲請求上傳的所有文件 |
get_data() | 返回請求主題緩衝的數據 |
get_json() | 返回一個Python字典,包含解析請求body後得到的JSON |
blueprint | 藍圖名稱 |
endpoint | 處理請求的Flask端點名稱,Flask把視圖函數的名稱稱作路由端點的名稱 |
method | HTTP請求方法,例如GET\POST |
scheme | URL方案(http活https) |
is_secure() | 通過安全的連接(HTTPS)發送請求時返回True |
host | 請求主機的主機名,如客戶端定義了端口號,還包括端口號 |
path | URL的路徑部分 |
query_string | URL的查詢參數部分,如:?name=joke&age=18 |
full_path | URL的路徑和查詢參數部分 |
url | 客戶端請求的完整URL |
base_url | 同url,但沒有查詢字符串部分 |
remote_addr | 客戶端的IP地址 |
environ | 請求的原始WSGI環境字典 |
3.2.1 獲取URL上的參數
對於url上參數,例如/params/url?name=joke,我們要想獲取參數,可以使用request.args方法獲取一個ImmutableMultiDict類型的參數列表,也可以通過get方法直接獲取該參數的值,如下所示:
@app.route("/params/url")
def params_url():
print(request.args)
print(request.args.get("name"))
return ""
url上的參數除了?和&傳參之外,也支持RESTFul風格的動態傳參,如/params/rest/<id>,類似<id>這樣的動態參數,默認解析爲string類型,當然我們可以指定其它類型,如指定id爲int類型,只是匹配整型的url,如/rest/1。Flask支持的類型:string、int、flot和path類型,path類型是一種特殊的字符串,與string類型不同的是,它可以包含正斜線。
@app.route("/params/rest/", defaults={'id': '1'})
@app.route("/params/rest/<id>")
def params_rest(id):
return jsonify({"id": id})
如上代碼,我們可以通過defaults設置默認值,設置id默認值爲1。當我們訪問http:xxxx:5000/params/rest時會返回如下結果:
同樣可以傳入參數,如我們訪問http:xxxx:5000/params/rest/2時會返回如下結果:
3.2.2 獲取body裏的參數
對於body裏的數據,我們可以使用request.data直接獲取bytes(在python2中返回的是str類型,類python3中返回的是bytes,並且要注意在python3.5之前,json.loads(str)裏支持傳入str類型,不支持bytes)類型數據,讓後根據content_type進行相應的類型轉換。如下所示,判斷content_type是否爲application/json,然後將其轉爲json格式
@app.route("/params/body", methods=['POST'])
def params_body():
print(request.content_type)
if request.content_type == 'application/json':
print(json.loads(request.data))
return ""
另外,如果我們直道請求參數的類型是json,可以直接使用request.json直接獲取json類型的數據
print(request.json)
注意:通過request.json或者request.get_json()得到的json數據可能會亂序。建議使用request.data,然後通過json.loads()獲取json,如下所示:
conf = json.loads(data, encoding='UTF-8', object_pairs_hook=OrderedDict)
3.2.3 獲取表單數據
對於form表單數據,我們可以使用request.form獲取一個ImmutableMultiDict類型的參數列表,然後根據參數名獲取參數值
@app.route("/params/form", methods=['POST'])
def params_form():
print(request.form)
print(request.form['name'])
return ""
3.2.4 獲取文件格式
對於文件格式的參數,我們可以是用request.files獲取一個參數列表,然後根據文件參數名獲取某個文件,如request.files[‘flie’]。
@app.route("/params/file", methods=['POST'])
def params_file():
file = request.files['file']
# get file type
print(file.content_type)
# get file name
print(file.filename)
# save file by bytes
with open(file.filename, 'wb') as f:
f.write(file.stream.read())
return ""
如上代碼,我們可以使用file.stream.read()讀取字節類型的數據,然後將其寫出到文件。同樣也可以使用file.save(路徑)方法寫出文件
# save file by method
file.save(file.filename)
3.3 返回請求響應
通常我們的請求響應不過幾種,返回一個頁面,返回一個json字符串,返回一個文件。對於Flask的響應,我們可以直接返回1個參數作爲內容,如
return "hello world!"
也可以返回2個參數,第二個參數爲響應的狀態碼,如
return "hello world!",400
也可以返回3個參數,第三個參數爲響應的頭信息,參數以字典的形式指定,如
return "hello world!",400,{"Server":"Werkzeug/0.15.2 Python/2.7.16"}
也可以通過make_reponse()方法自定義響應對象,後面會提到。
Flask響應對象
屬性和方法 | 說明 |
---|---|
status_code | HTTP 數字狀態碼 |
headers | 一個類似字典的對象,包含響應發送的所有首部 |
set_cookie() | 爲響應添加一個cookie |
delete_cookie() | 刪除一個cookie |
content_length | 響應主體的長度 |
content_type | 響應主體的媒體類型 |
set_data() | 使用字符串或字節值設定響應 |
get_data() | 獲取響應主體 |
3.3.1 返回一個頁面
要想要flask返回一個頁面,只需要return render_template(“模板名”,**參數)即可,比如我在templates目錄下有一個名爲index.html的模板文件,模板引擎是Jinja2,相關語法可以百度。內容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h1>Hello {{ name }} !</h1>
</body>
</html>
我可以通過render_template方法進行渲染並返回。
@app.route("/")
def index():
return render_template("index.html", **{"name": "joke"})
3.3.2 返回json格式
要想flask返回json格式數據,可以使用flask提供的jsonify方法格式化之後返回。
@app.route("/params/json", methods=['GET'])
def params_json():
return jsonify({"name": "joke"})
注意使用jsonify返回的json是會自動排序的,如果不想排序,可以使用
Response(json.dumps({"name": "joke"}), mimetype='application/json')
3.3.3 返回文件
要想flask返回文件,即文件下載,可以使用flask提供的send_file()方法
@app.route("/download", methods=['GET'])
def download():
return send_file("test.gif", as_attachment=True)
3.3.4 自定義響應
Flask給我提供了一個Response類,可以方便我們自由設置響應,如設置狀態碼、設置返回內容、設置header等等。我們可以通過兩種方式創建response對象,實例化Response()類,或者通過make_response(body,status,headers)方法。
Response(response=None,
status=None,
headers=None,
mimetype=None,
content_type=None,
direct_passthrough=False,)
make_response(body=None,
status=None
,headers=None)
使用make_response()方法
@app.route("/response", methods=['GET'])
def response_test():
data = {
"test": "123"
}
# res = Response(data)
res = make_response(json.dumps(data), 500, {'Content-Type': 'application/json'})
return res
使用Response()
@app.route("/response", methods=['GET'])
def response_test():
data = {
"test": "123"
}
res = Response(data,status=500,headers={'Content-Type': 'application/json'})
#res = make_response(json.dumps(data), 500, {'Content-Type': 'application/json'})
return res
3.5 cookie的獲取與設置
上面講過,Flask關於請求的信息大多封裝到了request裏,同樣cookie信息也是保存到了request裏。
3.5.1 獲取cookie
cookie獲取如上圖所示,通過request.cookies就可以獲取一個字典對象,裏面包含了cookie信息。
requset.cookies['test']
3.5.2 設置cookie
在上文中的response對象的屬性和方法表格中,提到有set_cookie()方法,這個方法就是用來設置cookie的,那麼該方法如何使用,需要如何傳參呢?先看一下源碼。
def set_cookie(
self,
key,
value="",
max_age=None,
expires=None,
path="/",
domain=None,
secure=False,
httponly=False,
samesite=None,
):
pass
參數說明:
參數名稱 | 說明 |
---|---|
key | 設置cookie的key |
value | 設置cookie的value |
max_age | 設置最大過期時長,單位秒,多少秒之後過期,默認爲None |
expires | 設置過期時間,什麼時間點過期,可以設置datatime對象或者時間戳 |
path | 將cookie限制爲給定路徑,默認情況下它將跨越整個域 |
domain | 設置cookie的域範圍,如果想設置跨域cookie,如設置domain=’.example.com’,允許’www.example.com’和’foo.example.com’等類似的域訪問。否則,cookie只能由設置的域訪問。 |
secure | 如果設爲“True”,則cookie只能通過HTTPS獲得 |
httponly | 禁止JavaScript訪問cookie。這是cookie標準的擴展,可能並非所有瀏覽器都支持 |
samesite | 限制cookie的範圍,使其僅在請求是“同一站點”時附加到請求 |
使用:
@app.route("/response", methods=['GET'])
def response_test():
data = {
"test": "123"
}
res = Response(data, status=500, headers={'Content-Type': 'application/json'})
res.set_cookie("test_key", "test_value", max_age=20)
return res;
3.5.3 刪除cookie
刪除cookie使用delete_cookie(key)即可。
3.6 session的獲取與設置
session在flask中是一個神奇的存在,它的本質其實就是經過加密的cookie。所以要想使用session,我們需要給flask設置鹽值祕鑰SECRET_KEY,flask使用它來進行加密解密。設置SECRET_KEY可以直接在app配置中添加config,如:
app.config['SECRET_KEY']='xxxxx'
上文中也有提及,flask提供了幾種上下文對象,其中session也被作爲單獨的上下文對象在flask應用中提供,通過下面方式,拿到該對象。
from flask import session
對於session的操作,類似於操作字典,可以使用如下方法和屬性
3.6.1 獲取session
獲取session有兩種方式,直接獲取
session['session_key']
這種方式,有種弊端,當session_key對應的session不存在時,會報異常。可以使用get()方法獲取
session.get('session_key')
這種方式,不會拋出異常,如果不存在會返回None。
3.6.2 設置session
設置session我們可以像給字典賦值一樣,給session賦值。
session['session_key']='session_value'
3.6.3 刪除session
刪除session我們可以使用pop()方法
session.pop('session_key')
3.6.4 清空所有session
要想清空session,可以使用clear()方法
session.clear()
3.6.5 設置session過期時間
在Flask中session的過期機制是這樣的,如果沒有設置sesion過期時間,那麼默認瀏覽器關閉時銷燬session。我們可以通過設置permanent參數爲True,來延長過期時間,默認爲31天,當然我們也可以通過給app.config設置PERMANENT_SESSION_LIFETIME來更改過期時間,這個值的數據類型是datetime.timedelay類型。
設置session爲31天
session['session_key']='session_value'
session.permanent=True
自定義時長
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
3.7 請求鉤子
講完了請求和響應,這裏補充一下flask中的幾種請求鉤子,鉤子的作用很常見,比如我們需要在執行某個請求之前,或者之後進行一些邏輯處理。Flask提供的鉤子是通過裝飾器實現,提供如下四種鉤子。
- before_request:註冊一個函數,在每次請求之前執行。
- before_first_request:註冊一個函數,只在處理第一個請求之前運行。可以通過這個鉤子添加服務器初始化任務。
- after_request:註冊一個函數,如果沒有未處理的異常拋出,在每次請求之後運行。
- teardow_request:註冊一個函數,即使有未處理的異常拋出,也在每次請求之後運行。
4 Flask藍圖
藍圖官網介紹:https://dormousehole.readthedocs.io/en/latest/blueprints.html
關於Flask藍圖的描述,這裏就不做介紹,簡單來書,藍圖可以方便我們將一個項目進行模塊化,詳細介紹可以參考官網。在項目中,主要是使用藍圖進行版本區分,比如v1版、v2版。
4.1 創建藍圖
下面我們創建一個名爲v1的藍圖,並添加應用前綴:/api/v1
from flask import Blueprint, jsonify
v1_blueprint = Blueprint("v1", __name__, url_prefix="/api/v1")
@v1_blueprint.route("/", defaults={'id': '1'})
@v1_blueprint.route("/<id>")
def show_id(id):
return jsonify({'id': id})
4.2 註冊藍圖
將藍圖註冊到Flask應用
from v1 import v1_blueprint
# create a flask app
app = Flask(__name__)
# register blueprint
app.register_blueprint(blueprint=v1_blueprint)
訪問:http://0.0.0.0:5000/api/v1/
5 自定義Flask紅圖
如果說藍圖是將一個項目按照應用來模塊化,那麼我們可以使用紅圖將每個應用按照功能進行模塊化。藍圖是Flask原生就提供的,但是紅圖需要我們自己來實現,紅圖是在藍圖的基礎做的進一步細分。紅圖的概念,是參考網上資料實現的。那麼紅圖有什麼應用場景呢,比如上述我們定義的v1藍圖下面,我們要按照功能再次進行模塊細分,分爲普通用戶模塊和管理員模塊,這時候我們就可以使用紅圖了。
5.1定義紅圖插件
創建一個lib目錄,然後創建redprint.py文件,最後創建如下類
class Redprint:
def __init__(self, name):
self.name = name
self.mound = []
def route(self, rule, **options):
def decorator(f):
self.mound.append((f, rule, options))
return f
return decorator
def register(self, bp, url_prefix=None):
if url_prefix is None:
url_prefix = "/" + self.name
for f, rule, options in self.mound:
endpoint = options.pop("endpoint", f.__name__)
bp.add_url_rule(url_prefix + rule, endpoint, f, **options)
5.2 創建紅圖
創建api/v1/admin包,並創建endpoint.py文件,內容如下:
from flask import jsonify
from lib.redprint import Redprint
# create redprint
admin_redprint = Redprint("admin")
@admin_redprint.route("/")
def admin():
return jsonify({"type": "admin"})
創建api/v1/user包,並創建endpoint.py文件,內容如下:
from flask import jsonify
from lib.redprint import Redprint
# create redprint
user_redprint = Redprint("user")
@user_redprint.route("/")
def user():
return jsonify({"type": "user"})
目錄結構如下圖所示:
5.3 註冊紅圖
在/api/v1/包下的_init_.py文件裏註冊紅圖,需要先創建blueprint然後註冊到紅圖。
from flask import Blueprint
from api.v1.admin.endpoint import admin_redprint
from api.v1.user.endpoint import user_redprint
def create_blueprint_v1():
# create blueprint
v1_blueprint = Blueprint("v1", __name__, url_prefix="/api/v1")
# register redprint
admin_redprint.register(bp=v1_blueprint, url_prefix="/admin")
user_redprint.register(bp=v1_blueprint, url_prefix="/user")
return v1_blueprint
5.4 註冊藍圖
藍圖的註冊方式,與之前方式相同,只不過藍圖,需要通過create_blueprint_v1()方法創建。
# register blueprint by redprint
app.register_blueprint(blueprint=create_blueprint_v1())
5.5 測試紅圖
啓動應用,訪問http://localhost:5000/api/v1/user/
訪問http://localhost:5000/api/v1/admin/
由上圖可以看出,我們定義的紅圖可以生效。
6 ORM插件 Flask-SQLAlchemy
通過上面幾個小節,我們已經可以快速的創建一個web服務,能夠處理簡單的請求並返回相應的內容。而且可以使用藍圖和紅圖,模塊化項目,使項目結構更加清晰。接下來將進一步深入,Flask使用Flask-SQLAlchemy插件對數據庫進行CRUD操作。這裏不對Flask-SQLAlchemy進行深入研究,詳細API可以參考官網https://flask-sqlalchemy.palletsprojects.com/en/2.x/。
6.1 安裝Flask-SQLAlchemy
使用pip安裝flask-sqlalchemy
pip install flask-sqlalchemy -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
6.2 Flask應用加載SQLAlchemy
6.2.1 配置數據庫
爲了方便演示,這裏使用輕量級數據庫sqlite作爲演示數據庫。假設數據庫文件在當前目錄,名爲test.db。簡單配置如下:
# config db
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
詳細配置項可以參考:http://www.pythondoc.com/flask-sqlalchemy/config.html
6.2.2 創建DB實例
創建SQLAlchemy實例,首先創建database包,然後在_init_.py包裏創建DB實例,如下代碼所示:
# create db from app
db = SQLAlchemy()
6.2.3 創建表模型
因爲是ORM框架,類似Spring JPA,框架可以根據實體類進行關係映射。在Python裏也是通過類型進行映射,所以首先我們要創建模型類。關於模型創建以及模型關係,可以參考官網:http://docs.jinkan.org/docs/flask-sqlalchemy/models.html。如我們要創建一個User表,裏面有id、name、age三個屬性,在database包下創建一個models.py的文件,然後創建類如下:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
age = db.Column(db.Integer)
def __init__(self, id, name, age):
self.id = id
self.name = name
self.age = age
def dict(self):
return {
"id": self.id,
"name": self.name,
"age": self.age
}
def __repr__(self):
return 'User %r' % self.name
6.2.4 初始化數據庫
數據庫初始化之前需要將SQLAlchemy實例與Flask進行整合
# init db
db.init_app(app)
Flask啓動時如果表不存在自動創建
# crate db table
with app.app_context():
db.create_all()
如下所示,重啓應用後表自動創建了。
6.2.5 基本的CURD操作
上面我們已經將SQLAlchemy整合到Flask裏了,那麼我們如果對數據庫進行增刪改查操作呢,相面將簡單演示一下。詳細內容可以查看:http://www.pythondoc.com/flask-sqlalchemy/queries.html
6.2.5.1 新增記錄
上面我們創建了一張User表,現在我們向這張表裏插入一條記錄,該如何操作?繼續上面紅圖裏的/api/v1/user/endpoint.py。接收一個POST請求,獲取請求參數id、name、age的值然後插入數據庫,如下代碼所示:
@user_redprint.route("", methods=['POST'])
def add_user():
data = request.get_json()
u = User(data['id'], data['name'], data['age'])
db.session.add(u)
db.session.commit()
return "success"
PostMan測試請求:
6.2.5.2 查詢記錄
上面我們已經能夠插入一條記錄了,那麼我們如何能查詢到剛纔查詢的記錄呢。如下代碼爲查詢所有記錄
@user_redprint.route("", methods=['GET'])
def get_users():
users = User.query.all()
return jsonify([user.dict() for user in users])
PostMan測試請求
6.2.5.3 更新記錄
上面提到新增一條記錄用session.add(),其實更新一條記錄也可以用add,如下代碼所示:
@user_redprint.route("", methods=['PUT'])
def update_user():
data = request.get_json()
u = User.query.filter(User.id == data['id']).one_or_none()
u.name = data['name']
u.age = data['age']
db.session.add(u)
db.session.commit()
return "success"
PostMan請求
6.2.5.4 刪除記錄
添加記錄用session.add,那麼刪除記錄,我們可以用session.delete。如下代碼所示,根據名字刪除記錄
@user_redprint.route("", methods=['DELETE'])
def delete_user():
name = request.args.get("name")
u = User.query.filter(User.name == name).first()
db.session.delete(u)
db.session.commit()
return "success"
PostMan請求
7 定時調度插件Flask-APScheduler
前面記錄了Flask對請求的處理以及數據庫的CRUD操作,已經能完成一個簡單的後臺開發了。下面將進行一些擴展,定時任務。相信在平時的項目裏經常會用到定時任務,在Java裏我們我們可以使用Quartz,它能與Spring很好的整合。對於Python裏可以使用APScheduler,官網文檔:https://apscheduler.readthedocs.io/en/latest/。而Flask-APScheduler是對APScheduler的封裝擴展,使其能與Flask更好的融合。提起這個插件,我有些許頭疼,竟沒找到詳細的官方文檔,只定位到了Git倉庫的地址:https://github.com/viniciuschiele/flask-apscheduler,而且查某度和Google得到的文章幾乎千篇一律,沒有詳細的介紹。其實Flask-APScheduler的使用與APScheduler類似,這裏就花一點時間,整理彙總一下,我對於Flask-APScheduler插件的使用記錄。本節將從以下幾個方面進行整理:APScheduler特性、動態管理定時任務、定時的幾種方式、執行器的配置、持久化定時任務。
7.1 Flask-APScheduler特性
從Git的README可以看出,APScheduler有一下幾個特性:
- 從Flask的配置中加載scheduler配置
- 從Flask的配置中加載定義的的定時任務
- 允許指定調度程序將在其上運行的主機名
- 提供REST API 去管理調度任務
- 爲REST API 提供權限認證
下面將詳細的落地這些特性。
7.1.1 特性一:從Flask的配置中加載scheduler配置
意思就是說,關於scheduler的配置,是從Flask應用的上下文中獲取的,也就是說,配置是統一在Flask應用中指定的,即通過app.config指定的。如我們在config/目錄下創建一個scheduler.py文見用來存放關於Flask-APScheduler相關的配置。簡單添加一個Executors的配置,關於Executors的詳細配置,在後面後詳細講解。
class SchedulerConfig(object):
SCHEDULER_EXECUTORS = {
'default': {'type': 'threadpool', 'max_workers': 20}
}
將配置添加到Flask應用
app.config.from_object(SchedulerConfig)
創建調度器
# create scheduler
scheduler = APScheduler()
初始化調度器
# init scheduler
scheduler.init_app(app=app)
scheduler.start()
7.1.2 特性二:從Flask的配置中加載定義的的定時任務
該特性意思是可以從配置中加載事先定義好的定時任務,比如我有一個print_test(name)方法,每隔1秒打印一下name,代碼如下:
def print_test(name):
print(name)
在SchedulerConfig類中添加如下配置:
JOBS = [
{
'id': 'job1',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': 'interval',
'seconds': 1
}
]
啓動測試:
7.1.3 特性三:允許指定調度程序將在其上運行的主機名
默認Flask-APScheduler允許在所有的主機名上運行,即
SCHEDULER_ALLOWED_HOSTS = ['*']
我們可以通過修改該參數,限制允許執行的主機名,比如我當前的主機名爲shirukaideimac.local,我設置SCHEDULER_ALLOWED_HOSTS=[‘localhost’],那麼調度程序將不會執行。
7.1.4 特性四:提供REST API 去管理調度任務
Flask-APScheduler 提供REST API方便我們去管理調度任務。但是需要我們手動開啓,在配置中添加如下配置
SCHEDULER_API_ENABLED=True
重啓服務,訪問http://localhost:5000/{api_prefix}即可得到scheduler的基本信息。這裏api_prefix默認爲scheduler,可以通過SCHEDULER_API_PREFIX參數進行自定義。
關於Flask-APScheduler提供了哪些REST API,可以在flask_apscheduler/scheduler.py裏查看,代碼如下:
def _load_api(self):
"""
Add the routes for the scheduler API.
"""
self._add_url_route('get_scheduler_info', '', api.get_scheduler_info, 'GET')
self._add_url_route('add_job', '/jobs', api.add_job, 'POST')
self._add_url_route('get_job', '/jobs/<job_id>', api.get_job, 'GET')
self._add_url_route('get_jobs', '/jobs', api.get_jobs, 'GET')
self._add_url_route('delete_job', '/jobs/<job_id>', api.delete_job, 'DELETE')
self._add_url_route('update_job', '/jobs/<job_id>', api.update_job, 'PATCH')
self._add_url_route('pause_job', '/jobs/<job_id>/pause', api.pause_job, 'POST')
self._add_url_route('resume_job', '/jobs/<job_id>/resume', api.resume_job, 'POST')
self._add_url_route('run_job', '/jobs/<job_id>/run', api.run_job, 'POST')
這裏簡單總結一下:
7.1.4.1 獲取調度信息
API: /{api_prefix}
請求類型:GET
請求參數:無
描述:獲取調度信息
結果:
{
"current_host": "shirukaideimac.local",
"allowed_hosts": [
"*"
],
"running": true
}
7.1.4.2 獲取所有job列表
API: /{api_prefix}/jobs
請求類型:GET
請求參數:無
描述:獲取所有的job
結果:
[
{
"id": "job1",
"name": "job1",
"func": "config.scheduler:print_test",
"args": [
"joke"
],
"kwargs": {},
"trigger": "interval",
"start_date": "2019-05-21T17:43:32.105979+08:00",
"seconds": 1,
"misfire_grace_time": 1,
"max_instances": 1,
"next_run_time": "2019-05-21T17:52:25.105979+08:00"
}
]
7.1.4.3 新增job
API: /{api_prefix}/jobs
請求類型:POST
請求參數:
{
"id":"job2",
"func":"config.scheduler:print_test",
"args":["linda"],
"trigger":"interval",
"seconds":5
}
描述:新增定時任務
結果:
{
"id": "job2",
"name": "job2",
"func": "config.scheduler:print_test",
"args": [
"linda"
],
"kwargs": {},
"trigger": "interval",
"start_date": "2019-05-21T17:46:27.750697+08:00",
"seconds": 5,
"misfire_grace_time": 1,
"max_instances": 1,
"next_run_time": "2019-05-21T17:46:27.750697+08:00"
}
7.1.4.4 獲取某個job信息
API: /{api_prefix}/jobs/{job_id}
請求類型:GET
請求參數:job_id
描述:獲取某個job的信息
結果:
{
"id": "job2",
"name": "job2",
"func": "config.scheduler:print_test",
"args": [
"linda"
],
"kwargs": {},
"trigger": "interval",
"start_date": "2019-05-21T17:46:27.750697+08:00",
"seconds": 5,
"misfire_grace_time": 1,
"max_instances": 1,
"next_run_time": "2019-05-21T17:49:37.750697+08:00"
}
7.1.4.5 更新指定job
API: /{api_prefix}/jobs/{job_id}
請求類型:PATCH
請求參數:job_id,注意請求參數裏不能包含id。
{
"func":"config.scheduler:print_test",
"args":["simple"],
"trigger":"interval",
"seconds":5
}
描述:更新指定的job
結果:
{
"id": "job2",
"name": "job2",
"func": "config.scheduler:print_test",
"args": [
"simple"
],
"kwargs": {},
"trigger": "interval",
"start_date": "2019-05-21T17:56:59.183372+08:00",
"seconds": 5,
"misfire_grace_time": 1,
"max_instances": 1,
"next_run_time": "2019-05-21T17:56:59.183372+08:00"
}
7.1.4.6 暫停某個job
API: /{api_prefix}/jobs/{job_id}/pause
請求類型:POST
請求參數:job_id
描述:暫停某個job
結果:
{
"id": "job2",
"name": "job2",
"func": "config.scheduler:print_test",
"args": [
"simple"
],
"kwargs": {},
"trigger": "interval",
"start_date": "2019-05-21T17:56:59.183372+08:00",
"seconds": 5,
"misfire_grace_time": 1,
"max_instances": 1,
"next_run_time": null
}
7.1.4.7 恢復某個job
API: /{api_prefix}/jobs/{job_id}/pause
請求類型:POST
請求參數:job_id
描述:恢復某個job
結果:
{
"id": "job2",
"name": "job2",
"func": "config.scheduler:print_test",
"args": [
"simple"
],
"kwargs": {},
"trigger": "interval",
"start_date": "2019-05-21T17:56:59.183372+08:00",
"seconds": 5,
"misfire_grace_time": 1,
"max_instances": 1,
"next_run_time": "2019-05-21T18:02:59.183372+08:00"
}
7.1.4.8 刪除某個job
API: /{api_prefix}/jobs/{job_id}
請求類型:DELETE
請求參數:job_id
描述:刪除某個job
結果:無
7.1.2 特性五:爲REST API 提供權限認證
flask默認提供了基於HTTP Basic Auth的權限認證。需要開啓權限認證,我們需要添加如下配置:
SCHEDULER_AUTH = HTTPBasicAuth()
實現認證方法
@scheduler.authenticate
def authenticate(auth):
return auth['username'] == 'admin' and auth['password'] == 'admin'
如果不帶auth發送請求,會出現如下錯誤。
需要帶入認證信息。
7.2 動態管理定時任務
在7.1小節講特性的時候,講到我們可以通過配置添加定時任務,在配置中的JOBS的列表中添加job信息即可,當然在Flask-APScheduler提供的REST API裏我們也可以通過給定的API對定時任務進行添加、暫停、恢復、以及刪除等管理操作。同樣,Flask-APScheduler也提供代碼層級的API讓我們來實現定時任務的管理。
7.2.1 獲取調度信息
# get scheduler info
scheduler_info = OrderedDict([
('current_host', scheduler.host_name),
('allowed_hosts', scheduler.allowed_hosts),
('running', scheduler.running)
])
print(scheduler_info)
7.2.2 獲取所有job列表
方法:
get_jobs(self, jobstore=None)
參數說明:
jobstore:爲存儲器名稱,不指定爲獲取所有存儲器裏的job
返回值:
job列表,裏面包含job實例。
[<Job (id=job1 name=job1)>]
7.2.3 新增job
方法:
add_job(self, id, func, **kwargs)
參數說明:
id:爲指定job的id
func:爲需要執行的方法,可以是方法名,也可以是字符串,字符串的話需要寫成"包路徑:方法名"的格式。**kwargs:其他kv格式的參數,如args、trigger、seconds等。
返回值:
單個job實例
job2 (trigger: interval[0:00:05], next run at: 2019-05-22 10:39:27 CST)
demo:
# add job
scheduler.add_job(id='job2', func=print_test, args=('dear',), trigger='interval', seconds=5)
7.2.4 獲取某個job信息
方法:
get_job(self, id, jobstore=None)
參數說明:
Id :爲指定job的id
jobstore:想要從哪個存儲器裏獲取,默認爲None從所有的存儲器獲取。
返回值:
單個job實例
job2 (trigger: interval[0:00:05], next run at: 2019-05-22 10:49:26 CST)
demo:
# get job
print(scheduler.get_job("job2"))
7.2.5 更新指定job
方法:
modify_job(self, id, jobstore=None, **changes):
參數說明:
Id :爲指定job的id
jobstore:想要從哪個存儲器裏修改,默認爲None從所有的存儲器。
**changes:更新的內容,如args等
返回值:
單個job實例
job2 (trigger: interval[0:00:05], next run at: 2019-05-22 10:49:26 CST)
demo:
# modify job
scheduler.modify_job("job2", args=("hello",))
7.2.6 暫停某個job
方法:
pause_job(self, id, jobstore=None):
參數說明:
Id :爲指定job的id
jobstore:想要從哪個存儲器裏暫停,默認爲None從所有的存儲器。
返回值:
單個job實例
demo:
# pause job
scheduler.pause_job("job2")
7.2.7 恢復某個job
方法:
resume_job(self, id, jobstore=None):
參數說明:
Id :爲指定job的id
jobstore:想要從哪個存儲器裏恢復,默認爲None從所有的存儲器。
返回值:
單個job實例
demo:
# resume job
scheduler.resume_job("job2")
7.2.8 刪除某個job
方法:
remove_job(self, id, jobstore=None):
參數說明:
Id :爲指定job的id
jobstore:想要從哪個存儲器裏移除,默認爲None從所有的存儲器。
返回值:
單個job實例
demo:
# remove job
scheduler.remove_job("job2")
7.3 定時的幾種方式:觸發器
上面我們在介紹特性以及API的過程中,使用了相同的定時觸發器interval。Flask-APScheduler與APScheduler一樣支持以下三種觸發器:
觸發器 | 描述 |
---|---|
date | 日期:觸發任務運行的具體日期 |
interval | 間隔:觸發任務運行的時間間隔 |
cron | 週期:觸發任務運行的週期 |
下面將分別介紹三種觸發器的使用,統一使用配置的方式,添加定時任務。
7.3.1 date觸發器
date觸發器,是指定任務在特定的日期執行。使用date觸發器,需要指定兩個參數,一個是trigger、另一個是run_date, trigger:‘date’,run_date可以有三種值類型。
7.3.1.1 run_date類型爲字符串
我們可以指定run_date的值爲字符串類型,例如:“2019-05-22 11:58:00”,可以寫成如下配置:
{
'id': 'date_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': 'date',
'run_date': '2019-05-22 11:58:00'
}
7.3.1.2 run_date類型爲date
指定run_date的值類型爲date時,只能精確到天,指定定時任務在具體哪一天執行。
{
'id': 'date_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': 'date',
'run_date': date(2019, 5, 22)
}
7.3.1.3 run_date類型爲datetime
指定run_date的值類型爲datetime時,可以精確到毫秒。
{
'id': 'date_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': 'date',
'run_date': datetime(2019, 5, 22, 12, 5, 0, 0)
}
7.3.2 interval觸發器
interval觸發器,是設置任務間隔多長時間運行一次。在前面的例子中我們一直使用的是是interval。它有幾個比較常用的參數,間隔參數:seconds、minutes、hours分別是間隔幾秒、間隔幾分鐘、間隔幾小時,這幾個參數只能設置也一個。時間範圍範數:start_date、end_date。設置定時任務運行的時間範圍。浮動參數:jitter,給每次觸發添加一個隨機浮動秒數,一般適用於多服務器,避免同時運行造成服務擁堵。
例如:
{
'id': 'job1',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': 'interval',
'minutes': 1,
'start_date': '2019-05-22 14:00:00',
'end_date': '2019-05-22 16:00:00',
'jitter': 10
}
7.3.3 cron觸發器
可以說cron觸發器是很強大了,常用的定時任務框架,大多都支持cron定時調度。APScheduler對crontab表達式進行了一層分裝,我們可以傳入如下參數
class apscheduler.triggers.cron.CronTrigger(
year=None,
month=None,
day=None,
week=None,
day_of_week=None,
hour=None,
minute=None,
second=None,
start_date=None,
end_date=None,
timezone=None,
jitter=None)
當省略時間參數時,在顯式指定參數之前的參數會被設定爲*,之後的參數會被設定爲最小值,week 和day_of_week的最小值爲*。比如,設定day=1, minute=20等同於設定year=’*’, month=’*’, day=1, week=’*’, day_of_week=’*’, hour=’*’, minute=20, second=0,即每個月的第一天,且當分鐘到達20時就觸發。
表達式類型
表達式 | 參數類型 | 描述 |
---|---|---|
* | 所有 | 通配符。例:minutes=*即每分鐘觸發 |
*/a | 所有 | 可被a整除的通配符。 |
a-b | 所有 | 範圍a-b觸發 |
a-b/c | 所有 | 範圍a-b,且可被c整除時觸發 |
xth y | 日 | 第幾個星期幾觸發。x爲第幾個,y爲星期幾(英文縮寫) |
last x | 日 | 一個月中,最後個星期幾觸發 |
last | 日 | 一個月最後一天觸發 |
x,y,z | 所有 | 組合表達式,可以組合確定值或上方的表達式 |
注!month和day_of_week參數分別接受的是英語縮寫jan– dec 和 mon – sun
比如設置定時任務在每年的6月、7月、8月、11月和12月的第三個週五,00:00、01:00、02:00和03:00觸發。配置定時任務如下所示:
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': 'cron',
'month': '6-8,11-12',
'day': '3rd fri',
'start_date': '2019-05-22 14:00:00',
'end_date': '2019-05-22 16:00:00',
'jitter': 10
}
當然也可以使用crontab表達式,不過需要from_crontab方法創建trigger,如下代碼所示:
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'jitter': 10
}
7.4 Executor 執行器的配置
關於執行器這一塊,我查閱的資料不是很詳細。APScheduler提供這幾種類型處理器:asyncio、gevent、processpool、threadpool、tornado、twisted。
from pkg_resources import iter_entry_points
_executor_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.executors'))
print(_executor_plugins)
{'asyncio': EntryPoint.parse('asyncio = apscheduler.executors.asyncio:AsyncIOExecutor [asyncio]'), 'debug': EntryPoint.parse('debug = apscheduler.executors.debug:DebugExecutor'), 'gevent': EntryPoint.parse('gevent = apscheduler.executors.gevent:GeventExecutor [gevent]'), 'processpool': EntryPoint.parse('processpool = apscheduler.executors.pool:ProcessPoolExecutor'), 'threadpool': EntryPoint.parse('threadpool = apscheduler.executors.pool:ThreadPoolExecutor'), 'tornado': EntryPoint.parse('tornado = apscheduler.executors.tornado:TornadoExecutor [tornado]'), 'twisted': EntryPoint.parse('twisted = apscheduler.executors.twisted:TwistedExecutor [twisted]')}
通常我們使用額是threadpool和processpool。可以通過如下的配置進行配置:
SCHEDULER_EXECUTORS = {
'default': {
'type': 'threadpool',
'max_workers': 20
},
'process': {
'type': 'processpool',
'max_workers': 10
}
}
創建job的時候,可以通過executor參數執行該job所使用的執行器,如下代碼所示。
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'executor': 'process'
}
7.5 JobStore持久化定時任務
Flask-APScheduler支持定時任務的持久化,默認是使用內存存儲定時任務,也支持基於SQLAlchemy的關係型數據庫、非關係的MongoDB、Redis、Rethinkdb、另外也支持Zookeeper。
from pkg_resources import iter_entry_points
_jobstore_plugins = dict((ep.name, ep) for ep in iter_entry_points('apscheduler.jobstores'))
print(_jobstore_plugins)
{'memory': EntryPoint.parse('memory = apscheduler.jobstores.memory:MemoryJobStore'), 'mongodb': EntryPoint.parse('mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore [mongodb]'), 'redis': EntryPoint.parse('redis = apscheduler.jobstores.redis:RedisJobStore [redis]'), 'rethinkdb': EntryPoint.parse('rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore [rethinkdb]'), 'sqlalchemy': EntryPoint.parse('sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore [sqlalchemy]'), 'zookeeper': EntryPoint.parse('zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore [zookeeper]')}
7.5.1MemoryJobStore
該存儲器是APScheduler默認的,不需要手動指定。當然也可以通過配置文件進行顯示指定,配置如下所示:
SCHEDULER_JOBSTORES = {
'default': MemoryJobStore()
}
7.5.2 SQLAlchemyJobStore
在前面講Flask的ORM框架的時候,我們提到過Flask-SQLAlchemy,這裏Flask-APScheduler可以基於它進行關係型數據庫的定時任務持久化, MySQL、SQLite、Oracle、Postgresql等。這裏爲了方便演示,使用SQLite進行持久化。在使用SQLAlchemyJobStore之前首先要安裝該插件。
pip install flask-sqlalchemy
配置如下所示:
SCHEDULER_JOBSTORES = {
'default': MemoryJobStore(),
'sqlalchemy': SQLAlchemyJobStore(url='sqlite:///test.db')
}
並且在配置定時任務的時候,顯示的指定該job所使用的jobstore。
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'executor': 'process',
'jobstore':'sqlalchemy'
}
啓動應用之後,會發現自動創建了一個名爲apscheduler_jobs的表。如下圖所示:
7.5.3 RedisJobStore
同樣我們也可以使用Redis進行持久化,首先需要安裝Python的Redis包。
pip install redis
配置:
SCHEDULER_JOBSTORES = {
# 'default': MemoryJobStore(),
# 'sqlalchemy': SQLAlchemyJobStore(url='sqlite:///test.db'),
'redis': RedisJobStore(host='localhost', port=6379)
}
顯示指定:
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'executor': 'process',
'jobstore': 'redis'
}
如下圖所示,發現redis裏寫入了APScheduler相關的數據。
7.5.4 RethinkDBJobStore
關於rethindb,https://rethinkdb.com/。APScheduler同樣支持使用RethinkDB做定時任務持久化。
依然首選需要安裝RethinkDB的包。
pip install rethinkdb
配置:
SCHEDULER_JOBSTORES = {
# 'default': MemoryJobStore(),
# 'sqlalchemy': SQLAlchemyJobStore(url='sqlite:///test.db'),
# 'redis': RedisJobStore(host='localhost', port=6379),
'rethinkdb': RethinkDBJobStore(host='localhost', port=28015)
}
顯示在job裏指定jobstore
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'executor': 'process',
'jobstore': 'rethinkdb'
}
重啓應用,發現在rethinkdb裏寫入了定時任務的相關信息。
7.5.5 MongoDBJobStore
依然需要安裝mongo的python包
pip install pymongo
配置:
SCHEDULER_JOBSTORES = {
# 'default': MemoryJobStore(),
# 'sqlalchemy': SQLAlchemyJobStore(url='sqlite:///test.db'),
# 'redis': RedisJobStore(host='localhost', port=6379),
# 'rethinkdb': RethinkDBJobStore(host='localhost', port=28015)
'mongodb': MongoDBJobStore(host='localhost',port=27017)
}
顯示的在job中指定:
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'executor': 'process',
'jobstore': 'mongodb'
}
重啓應用,查看數據庫如下所示:
7.5.6 ZooKeeperJobStore
依然是需要安裝zookeeper的python客戶端,這裏使用的是kazoo
pip install kazoo
配置:
SCHEDULER_JOBSTORES = {
# 'default': MemoryJobStore(),
# 'sqlalchemy': SQLAlchemyJobStore(url='sqlite:///test.db'),
# 'redis': RedisJobStore(host='localhost', port=6379),
# 'rethinkdb': RethinkDBJobStore(host='localhost', port=28015)
# 'mongodb': MongoDBJobStore(host='localhost',port=27017)
'zookeeper': ZooKeeperJobStore(hosts='localhost:2181')
}
顯示的在job中指定jobstore:
{
'id': 'cron_trigger',
'func': 'config.scheduler:print_test',
'args': ('joke',),
'trigger': CronTrigger.from_crontab('* * * * *'),
'executor': 'process',
'jobstore': 'zookeeper'
}
進入zookeeper命令行,查看。
8 Socket插件Flask-SocketIO
相信websocket在平時的web開發中,也用到不少。這裏主要介紹一下Flask裏的Flask-SocketIO插件,該插件支持三種異步模式:eventlet、gevent、threading。
- eventlet是性能最佳的選項,支持長輪詢和WebSocket傳輸。
- gevent在許多不同的配置中得到支持。gevent包完全支持長輪詢傳輸,但與eventlet不同,gevent沒有原生的WebSocket支持。要添加對WebSocket的支持,目前有兩種選擇:安裝gevent-websocket 包爲gevent添加WebSocket支持,或者可以使用帶有WebSocket功能的uWSGI Web服務器。gevent的使用也是一種高性能選項,但略低於eventlet。
- theading需要注意的是它缺乏其他兩個選項的性能,因此它只應用於簡化開發工作流程,此選項僅支持長輪詢傳輸。
Flask-SocketIO會根據安裝的內容自動檢測要使用的異步框架。優先選擇eventlet,然後是gevent。對於gevent中的WebSocket支持,首選uWSGI,然後是gevent-websocket。如果既未安裝eventlet也未安裝gevent,則使用Flask開發服務器。更多關於Flask-SocketIO的使用可以查看官網:https://flask-socketio.readthedocs.io/en/latest/。
8.1 快速使用
首先需要安裝Flask-SocketIO的包
pip install flask-socketio
初始化
# create a flask app
app = Flask(__name__)
# create socketio
socketio = SocketIO()
if __name__ == '__main__':
# init scheduler
scheduler.init_app(app=app)
scheduler.start()
scheduler_api()
# init socketio
socketio.init_app(app=app)
# run server
socketio.run(app=app, host='0.0.0.0', port=5000, debug=False)
8.2 演示Demo
官網文檔關於Flask-SocketIO的說明已經很詳細了,就不做多餘的copy。使用Flask-SocketIO接收消息、發送消息、廣播、房間等功能都可以參考官網例子。這裏就簡單寫一個hello world級別的demo,演示一下Flask-SokcetIO如何使用。該demo主要功能就是實時獲取內存使用情況,並將信息推送給前臺,如下所示:
主要思路是:當用戶點擊 [開始監控]按鈕時,觸發socket連接,後臺socket接收連接事件之後,啓動後臺任務每兩秒鐘獲取一次內存信息,然後推送給前臺,前臺接收到消息後實時展示。當用戶點擊[停止監控]按鈕是,觸發socket銷燬,後臺socket藉口銷燬事件之後,停止監控內存。
8.2.1 獲取內存信息
這裏使用psutil獲取內存信息,需要先安裝此包。
pip install psutil
獲取內存信息並解析成json
def get_virtual_memory():
"""
獲取內存使用情況
:return: dict
"""
memory_info = psutil.virtual_memory()
return {attr: getattr(memory_info, attr) for attr in dir(memory_info) if
not attr.__contains__("_") and not isinstance(getattr(memory_info, attr), type(len))}
json格式如下所示:
{
"active": 5771452416,
"available": 6745636864,
"free": 762896384,
"inactive": 5699289088,
"percent": 73.8,
"total": 25769803776,
"used": 17115865088,
"wired": 11344412672
}
8.2.2 編寫異步方法
該方法主要功能是每2秒獲取一次內存信息,然後推送給前臺。這裏使用tasks字典來存放任務狀態,
使用emit()方法推送消息。代碼如下所示:
def background_task(sid):
# add sid to tasks
tasks[sid] = True
while tasks[sid]:
info = get_virtual_memory()
socketio.emit("server_response", {'data': info}, namespace='/ws')
socketio.sleep(2)
if not tasks[sid]:
tasks.pop(sid)
8.2.3 SocketIO監聽connect事件
後臺使用@socketio.on(“connect”, namespace="/ws")監聽connect事件,並使用socketio.start_background_task()啓動後臺任務。代碼如下所示:
@socketio.on("connect", namespace="/ws")
def handle_connect():
"""
handle connect
:return:
"""
sid = getattr(request, 'sid')
socketio.start_background_task(background_task, sid)
emit("connect", {'data': '連接成功'})
8.2.4 SocketIO監聽disconnect事件
後臺使用@socketio.on(“disconnect”, namespace="/ws")監聽disconnect事件,並設置tasks狀態爲False用以停止後臺任務。代碼如下:
@socketio.on("disconnect", namespace="/ws")
def handle_disconnect():
sid = getattr(request, 'sid')
if sid in tasks:
tasks[sid] = False
8.2.5 前臺展示
前臺主要使用socket.io.js與後臺通信。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flask Demo</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.dev.js"></script>
</head>
<body>
<div class="container">
<br>
<h1 class="text-center">實時監控內存使用情況</h1>
<div class="row">
<div style="margin-bottom: 5%" class="col-md-12 text-center head-title-font">
<button id="start-monitor" class="btn btn-primary" style="width: 10%">開始監控</button>
<button id="stop-monitor" class="btn btn-danger" style="width: 10%">停止監控</button>
<hr>
<table data-toggle="table" class="table table-striped table-bordered">
<thead>
<tr id="table_head">
</tr>
</thead>
<tbody>
<tr id="table_content">
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const url = "ws://localhost:5000/ws";
let socket = null;
//開始監控
$("#start-monitor").click(function () {
socket = io(url)
socket.on('connect', function (msg) {
if (!$.isEmptyObject(msg)) {
alert("開啓監控")
socket.on('server_response', function (msg) {
let headHtml = ''
let content = ''
for (let key in msg['data']) {
headHtml += '<th>' + key + '</th>'
content += `<td>${msg['data'][key]}</td>`
}
$("#table_head").html(headHtml)
$("#table_content").html(content)
});
}
});
})
// 停止監控
$("#stop-monitor").click(function () {
socket.disconnect()
})
</script>
</body>
</html>
9 使用自定義紅圖打造層次結構分明的項目
之前小節講解了如果使用Flask快速創建一個Web服務,並介紹了Flask的幾種擴展。可以看出Flask是一個漸進式的web服務框架,我們可以根據需求動態的進行組件擴展。這一節主要介紹一下如何使用自定義的紅圖打造一個層次結構分明的項目。爲什麼要打造一個層次分明的項目,對我個人來說,受spring mvc框架的影響,希望把項目能夠按照架構分層,像視圖層、邏輯層、數據訪問層等,更重要的一點,層次分明的項目,可讀性更強。
9.1 項目結構及說明
這裏創建了一個小的項目,功能層面上實現了博客、用戶、標籤的CURD操作。提供了RESTful風格的接口。項目集成了上面講述的blueprint、redprint用於進行項目的層次劃分,集成了Flask-SocketIO插件用以提供WebSocket服務,集成了Flask-SQLAlchemy用以對關係型數據庫的操作,集成了Flask-APScheduler插件用以執行定時任務。項目結構如下所示:
├── README.md
├── application # 應用代碼文件夾
│ ├── __init__.py # 初始化應用
│ ├── api # api包,用以提供RESTful接口
│ │ ├── __init__.py
│ │ └── v1 # v1版本包
│ │ ├── __init__.py # 初始化v1藍圖,註冊紅圖
│ │ ├── blog # 博客相關接口
│ │ │ ├── business.py
│ │ │ └── endpoints.py
│ │ ├── socketio.py
│ │ ├── tag # 標籤相關接口
│ │ │ ├── business.py
│ │ │ └── endpoints.py
│ │ └── user # 用戶相關接口
│ │ ├── business.py
│ │ └── endpoints.py
│ ├── apsheduler
│ │ ├── __init__.py
│ ├── config # 應用相關配置目錄
│ │ ├── __init__.py
│ │ ├── logging.conf # 日誌配置文件
│ │ ├── scheduler.py # flask-apscheduler配置文件
│ │ └── setting.py # 應用配置文件
│ ├── database
│ │ ├── __init__.py # 初始化數據庫
│ │ └── models.py # 模型類,對象關係映射
│ ├── libs # 組件包
│ │ ├── __init__.py
│ │ ├── error.py # 處理錯誤請求,返回結構化數據
│ │ ├── ok.py # 處理正常請求,返回結構化數據
│ │ └── redprint.py # 紅圖插件,提供紅圖路由
│ ├── socketio # flask-socketio擴展
│ │ ├── __init__.py
│ └── templates # 用以存放模板以及靜態資源。
│ ├── index.html
│ └── static
│ ├── css
│ └── js
├── application.db # sqlite數據庫文件
├── logs
│ └── application.log # 應用日誌
├── manager.py # 應用入口腳本,提供服務啓動、數據庫初始化、數據庫情況等命令
└── requirements.txt # 依賴包清單
9.2 代碼及接口說明
關於項目搭建,以及每一模塊的詳細說明,這裏就不做詳細描述,完整的代碼已經上傳到GitHub上了,項目地址:https://github.com/shirukai/flask-framework-redprint.git
接口的話這裏生成了一份PostMan的文檔。https://documenter.getpostman.com/view/2759292/S1TSYymv 可以在本地將服務起來,進行接口測試。
10 使用flask-restplus插件打造RESTFul風格項目
上面我們已經使用Redprint打造了一個層次接口分明的項目,並且也具有了一定的RESFul風格,能夠滿足大部分的項目開發。但是提供的REST API管理起來並不容易,在Spring項目裏,我們可以使用Swagger來管理API,同樣Flask也支持Swagger,因爲是漸進式框架, 同樣需要我們安裝支持swagger的擴展,這裏使用的是flask-restplus,官方網址:https://flask-restplus.readthedocs.io/en/stable/,它的主旨是以最少的設置進行最佳實踐,快速構建REST API並提供一個連貫的裝飾器和工具集來描述我們的API並正確公開其文檔。
10.1 安裝flask-restplus
像其它擴展一樣,flask-restplus可以通過pip直接安裝
pip install flask-restplus
或者使用easy_install
easy_install flask-restplus
10.2 項目結構及說明
這裏對之前的使用紅圖創建的項目進行改造,使用flask-restplus來替代紅圖的作用。主體結構不變,主要對endpoints以及v1.__init__.py進行改造,具體實現可以參考代碼。
├── README.md
├── application
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ └── v1
│ │ ├── __init__.py # 註冊restplus的namespace
│ │ ├── blog
│ │ │ ├── business.py
│ │ │ └── endpoints.py
│ │ ├── restplus.py # restplus初始化
│ │ ├── serializers.py # 串行口用以格式化請求參數和返回值
│ │ ├── socketio.py
│ │ ├── tag
│ │ │ ├── business.py
│ │ │ └── endpoints.py
│ │ └── user
│ │ ├── business.py
│ │ └── endpoints.py
│ ├── apsheduler
│ │ ├── __init__.py
│ ├── config
│ │ ├── __init__.py
│ │ ├── logging.conf
│ │ ├── scheduler.py
│ │ └── setting.py
│ ├── database
│ │ ├── __init__.py
│ │ └── models.py
│ ├── libs
│ │ ├── __init__.py
│ │ ├── error.py
│ │ ├── ok.py
│ │ └── redprint.py
│ ├── socketio
│ │ ├── __init__.py
│ └── templates
│ ├── index.html
│ └── static
│ ├── css
│ └── js
├── application.db
├── logs
│ └── application.log
├── manager.py
└── requirements.txt
10.3 代碼及接口說明
代碼同樣放到了GitHub上,可以下載代碼參考,項目地址:https://github.com/shirukai/flask-framwork-restplus.git。
剛纔也提到過,RESTPlus會自動爲我們生成接口文檔,當我們啓動項目之後,可以訪問http://localhost:5000/api/v1查看swagger。
11 Flask項目發佈
Flask自動的app.run()啓動的web服務是用來開發的,並不適合生成環境,所以官方不建議使用app.run()作爲生產的容器。關於Flask的項目發佈,官方也提供了幾種方式,具體的可以參考:https://dormousehole.readthedocs.io/en/latest/deploying/。這裏就不一一講解,因爲這個地方我接觸的也不多,暫且只寫一下使用uWSGI進行Flask項目的發佈吧。
11.1 安裝uWSGI
使用pip安裝uwsgi
pip install uwsgi
11.2 uwsgi命令的方式啓動flask項目
這裏以flask-framework-redprint這個項目爲例子,使用uwsgi命令行啓動服務。
uwsgi --http :18666 --wsgi-file manager.py --callable app
11.3 使用配置文件的方式啓動flask項目
上面使用命令可以簡單的啓動一個flask服務,但是如果命令參數比較多,使用命令就比較繁瑣,這時候我們可以通過配置文件的方式啓動。給我們的flask項目設置一個uwsgi配置文件。同樣是以flask-framework-redprint爲例,在項目根目錄創建一個名爲uwsgi.ini的配置文件,內容如下:
[uwsgi]
wsgi-file = manager.py
callable = app
gevent = 1000
http-websockets = true
master = true
http = 0.0.0.0:18666
啓動服務
uwsgi uwsgi.ini
效果與命令行一樣。
12 Flask項目容器化
docker,docker快到碗裏來,flask,flask快到docker裏去。目前我司項目部署大多都是使用docker,這裏不禁要感慨一下,曾幾何時使用spring mvc,需要一大把的xml文件,去配置bean,去配置mybatis等等,各種配置文件,搞不好哪裏就出問題了,而且啓動的時候還需要打成war放到tomcat裏,繁瑣易出錯。現在使用spring boot簡化了太多的配置,而且自帶web容器,方便到愛不釋手,再加上docker加持,從開發到生產節省了太多的精力了。說這麼多,只是想表達,新技術給我們帶來的極大的便利。接觸docker不長,但已經被深深的吸引,上文在講Flask定時任務插件時,提到的幾種持久化方式,像Redis、RethinkDB、MongoDB、還有Zookeeper等,我都是通過docker部署的,幾乎是一條命令,就部署完成了,節省了太多部署步驟。所以這裏也簡單講一下,如何將我們的Flask服務容器化。
12.1 使用Dockerfile創建容器鏡像
在項目根目錄創建名爲Dockerfile的文件,內容如下:
FROM python:3
ARG SERVER_PORT=18666
MAINTAINER shirukai "[email protected]"
# set work dir
WORKDIR flask-framework-redprint
# copy server files
COPY . .
# install dependencies
RUN pip install --no-cache-dir -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
# expose server port
EXPOSE ${SERVER_PORT}
# set time zone
ENV TZ Asia/Shanghai
# start flask service when the container starts
CMD uwsgi uwsgi.ini
在項目路徑執行docker命令創建鏡像
docker build -t flask-framework-redprint:v1 .
等待創建完成,我們可以使用docker images查看我們的鏡像。
12.2 啓動容器
鏡像創建完成之後,我們就可以啓動我們的docekr容器了,使用如下命令運行
docker run -itd -p 18666:18666 flask-framework-redprint:v1
查看容器狀態
docker ps
查看容器日誌
docekr logs 8ca71365a64e
訪問localhost:18666查看
總結
Flask的知識點整理到這裏總算要告一段落,前前後後花了差不多一個月的時間,斷斷續續。期間在圖書館拜讀了基本關於Flask的書《Flask Web開發》和《Flask Web開發實戰:入門、進階與原理解析》,怎麼說呢,這兩本書其實大同小異,內容差不多,後者實戰的東西更多一點,沒有深入閱讀。從這兩本書中,補充了不少知識,比如請求鉤子、關係模型等。之前也接觸過Python的另一個熱門的Web框架Django,那會學習起來比較費勁,因爲剛接觸Python,需要學習語法也需要學習框架,對於Django框架的學習只停留在了會使用的階段。在學習Flask的時候,輕鬆了不少,一個原因是框架本身的輕量級,另一個原因是有了語言基礎,學習起來並不費勁。本文是我對Flask的學習記錄,從0到1的學習過程,希望有所幫助,不足或者有錯誤的地方,煩請指正。