簡述
在上一節中,我們編寫了抓取新聞的爬蟲指令碼,每天早上八點定時抓取新聞儲存到 MySQL資料庫中。直接用DataGrip連下資料庫,就可以檢視爬取到的新聞了。不過, 並不是我們想要的最終形態。我希望新聞篩選的工作可以直接在手機上進行,而不是 每次都要開啟電腦,開啟DataGrip黑乎乎的頁面,篩選,複製貼上。在寫這個APP之前, 要先學點web的東東,寫點介面。Python中有比較流行的兩個Flask和Django,筆者選擇 的比較輕量級的Flask。在寫《如何用Python投機倒把幾天“暴富”》,有些同學私信我有 沒有Flask的教程推薦。索性本節就來過一過Flask,下一節再來利用Flask來寫API介面和 靜態頁面,以及直接生成公號文章樣式。內容較多,比較枯燥,建議先收藏後慢慢觀看~
1、Flask簡介
一個輕量級的“微核心”Web應用框架,基於Werkzeug實現的WSGI套件和Jinja2模板引擎, 核心精簡,易於擴充套件,花費較少的學習成本,即可能開發一個簡單的網站。
相關文件:
- Flask官網:flask.pocoo.org/
- Flask Github倉庫:github.com/pallets/fla…
- Flask官方文件:flask.pocoo.org/docs/1.0
- Flask官方文件(中文版):dormousehole.readthedocs.io/en/latest/
- Flask學習資源:github.com/humiaozuzu/…
2、Flask開發環境搭建
直接通過pip命令安裝即可:
pip install flask
複製程式碼
這裡要注意「全域性安裝」和「虛擬環境安裝」的區別,之前有很多讀者都問過這樣的問題:
我明明在命令列裡已經執行了pip install xxx庫,但是進去pycharm,還是提示模組找不到?
對此,有兩種可選的解決方案:
- 方案一:在PyCharm處的終端處執行pip安裝命令(注:前面有個venv)
- 方案二:勾選 inherit global stie-packages
個人更傾向於第一種,為了解決維護不同專案對應不同版本的問題,Python使用了虛擬環境的概念, 在虛擬環境中安裝第三方包只會作用到虛擬環境中,全域性的Python直譯器不受影響。在Python3中, 虛擬環境已成為一個內建模組,建立一個帶虛擬環境的檔案示例如下:
mkdir Test
cd Test
python -m venv venv
複製程式碼
執行完上述命令後,Python會執行venv包,建立一個venv的虛擬環境,上面的兩個venv引數分別為:
- Python虛擬環境包的名稱,固定寫venv
- 應用於這個特定的虛擬環境的名稱,可以改成你喜歡的名字,不過筆者習慣命名為venv,切換到別 的專案時,都能快速的找到對應的虛擬環境。
虛擬環境建立後,需要啟用後才能進入,通過下述命令「啟用虛擬環境」:
source venv/bin/activate
複製程式碼
執行完後會看到終端字首多了個venv,啟用虛擬環境後,終端會話的環境配置就會被修改, 此時鍵入Python或者pip,實際上呼叫的都是虛擬環境中的Python直譯器。一個應用場景: 開啟多個終端除錯多個應用,每個終端視窗可以啟用不同的虛擬環境,且不相互干擾。
注:如果你使用的是Python2或者Windows系統,如果想使用虛擬環境,要先通過pip命令 安裝先安裝一波virtualenvwrapper:pip install virtualenvwrapper。然後建立虛擬環境: virtualenv venv,最後是啟用虛擬環境:venv\Scripts\activate.bat。
3、最簡單的Hello Flask
新建一個hello.py的指令碼,內容如下:
# coding=utf-8
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello Flask!"
if __name__ == "__main__":
app.run()
複製程式碼
在終端鍵入下述命令執行指令碼:
python hello.py
複製程式碼
執行後可以看到輸出如下資訊:
瀏覽器開啟:http://127.0.0.1:5000,可以看到如圖所示的Hello Flask!
服務啟動後就會進入輪詢,等待並處理請求,知道程式被終止,也可以直接按Ctrl+C停掉。 接著逐行解析一波程式碼:
- 第1行:用於宣告Python原始檔的編碼語法,該資訊後續會被Python解析器用於解析原始檔, 一般統一用utf-8。
- 第2行:引入Flask類。
- 第4行:例項化一個Flask類例項app,該例項接收包或模組名稱作為引數,一般直接傳入
__name__
, 讓flask.helpers.get_root_path函式通過傳入這個名字確定程式的根目錄,以便獲得靜態檔案和 模板檔案的目錄。 - 第6-8行:app:route裝飾器會將URL和執行的檢視函式的關係儲存到app.url_map屬性上。 這三行簡單點說就是:當瀏覽器訪問伺服器程式的根地址("/")時,Flask例項執行檢視函式hello(), 然後返回:Hello Flask!
- 第10行:其他檔案引用此檔案時,不會執行這個判斷內的程式碼。
- 第11行:啟動服務,預設Flask只監聽本地127.0.0.1地址和5000埠,如果你想修改埠,可以 傳入引數「port=埠號」,想支援遠端訪問的話,則傳入「host="0.0.0.0"」,你還可以設定 除錯模式,只需傳入引數「debug=True」,啟用除錯模式後,伺服器會在程式碼修改後會自動重新 載入,並在發生錯誤的時候提供一個能獲取錯誤上下文及可執行程式碼的除錯頁面。
4、flask-script模組
該模組的作用是:通過命令列的形式來操作Flask,使用命令列傳參,而不僅僅是通過 app.run()函式來傳。直接通過pip命令即可安裝此模組:
pip install flask-script
複製程式碼
新建一個manage.py的檔案:
from flask_script import Manager
from flask import Flask
app = Flask(__name__)
manager = Manager(app)
if __name__ == '__main__':
manager.run()
複製程式碼
接著命令列鍵入:
python manage.py runserver -h ip -p 埠
複製程式碼
除此之外的引數還有:-d(開啟除錯模式),-r(程式碼修改後自動載入),--help(檢視幫助資訊)。
5、路由與檢視
上面這種:
@app.route("/")
def hello():
return "Hello Flask!"
複製程式碼
定義了URL到Python函式間的對映關係,這種對映關係就叫路由。
0x1 動態路由
有時,可能有一些具有相同規則的URL,比如:
app.route("/login")
app.route("/register")
複製程式碼
我們可以把這類URL抽象成一個URL模式,示例如下:
@app.route('/<do>')
def hello(do):
return "<h1>當前請求:%s </h1>" % do
複製程式碼
Flask支援這種動態形式的路由,動態部分預設是字串,你也可以指定引數型別, 比如只接受整數:<int:id>,更多規則如下表所示:
欄位標記 | 描述 | 示例 |
---|---|---|
string | 預設,接受任何沒有斜杆"/"的文字 | <string:name> |
int | 整數 | <in:id> |
float | 浮點數 | <float:price> |
path | 和string類似,但也接受斜槓 | <path:address> |
uuid | 只接受uuid字串 | <string:uuid> |
any | 可以指定多種路徑,但需要傳入引數 | <any(int,string):params> |
另外有一點要注意:唯一URL,比如下面代表的是兩個不同的URL:
CodingBoy.io/article/
CodingBoy.io/article
複製程式碼
0x2 URL構造
在Flask中,可以使用url_for()函式來構造URL,接受一個檢視函式名作為第一個引數, 也接受對應URL規則變數部分的命名引數,未知變數部分會被新增到URL末尾作為查詢 引數。這裡務必注意一點:操作的是函式,不是路由裡的路徑!!!! 通過函式構建,而不是直接用字串拼接,主要出於下面兩個目的:
- 未來需要修改時,只需一次性修改URL,而不用到處替換;
- URL構建會自動轉義特殊字元和Unicode資料,而不用我們自己處理;
使用示例如下:
with app.test_request_context():
print(url_for('hello', uid=1))
print(url_for('hello', uid=2, kind='測試'))
複製程式碼
輸出結果如下:
/?uid=1
/?uid=2&kind=%E6%B5%8B%E8%AF%95
複製程式碼
如果你想生成一個絕對路徑,可以新增「_external=True」引數。
注:test_request_context可以在互動模式下產生請求上下文,不用app.run() 來執行這個專案,直接執行也會有Falsk上下文
0x3 請求方法限定
HTTP支援多種請求方法,預設情況下,路由只響應GET請求,如果不信可以自行用 PostMan對介面發起一個POST請求,結果如圖所示:
響應碼是405,表示不允許使用這種請求方法請求此URL,可以直接在 app.route裝飾器中設定methods引數來修改。修改後的程式碼示例如下:
@app.route("/", methods=['GET', 'POST'])
def hello():
return "Hello Flask!"
複製程式碼
當然也支援其他的請求方法:PUT,HEAD,DELETE,OPTIONS,PATCH,想支援多種方法用逗號隔開即可。
6、模板
在Web開發中,我們經常會用到模板引擎,什麼是模板?就是一個包含響應文字的檔案, 用佔位符(變數)標識動態部分,告訴模板引擎,其具體值需要從使用的資料中獲取。
0x1 檢視與模板的關係
在前面的例子中,檢視函式的主要作用是生成請求的響應,而實際開發中,檢視函式有 兩個作用:「處理業務邏輯」和「返回響應內容」。在大型專案中,如果把業務邏輯 和表現內容放在一起的話,會增加程式碼的複雜度和維護成本。而模板的作用就是: 「承擔檢視函式返回響應內容這一部分的職責,從而使得程式碼結構清晰,耦合度低。」 使用真實值替換變數,再(控制)返回最終得到的字串,這個過程稱作「渲染」。 Flask中預設使用「Jinja2」這個模板引擎來渲染模板。
0x2 Jinja2語法
通過一個簡單的例子,引入模板,下面是一個沒有使用模板的簡單程式:
# coding=utf-8
from flask import Flask
app = Flask(__name__)
@app.route("/<user>")
def hello(user):
if user == 'admin':
return "<h1>管理員,您好!<h1>"
else:
return "<h1>%s, 您好!</h1>" % user
if __name__ == "__main__":
app.run()
複製程式碼
接著我們使用Jinja2模板進行改寫,預設情況下,Flask會在「專案的templates子資料夾」 中尋找模板,我們新建一個templates資料夾,然後新建一個index.html,內容如下:
"<h1>{{name}},您好!<h1>"
複製程式碼
接著修改下hello.py檔案,修改後的內容如下:
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/<user>")
def hello(user):
if user == 'admin':
return render_template("index.html", user="管理員")
else:
return render_template("index.html", user=user)
if __name__ == "__main__":
app.run()
複製程式碼
接著執行hello.py,瀏覽器鍵入以下地址,對應輸出結果如下:
http://127.0.0.1:5000/admin # 輸出結果:管理員,您好!
http://127.0.0.1:5000/jay # 輸出結果:jay,您好!
複製程式碼
以上就是一個簡單的模板使用示例,通過呼叫Flask提供的render_template函式, 生成了一個模板,第一個引數是模板的名稱,隨後的引數是鍵值對,表示模板中 變數對應的真實值。接著詳細講解一波Jinja2的語法:
- 1.變數
Jinja2使用**{{變數名}}
**來表示一個變數,除了基本資料型別外還可以使用列表,欄位
或物件等複雜型別,示例如下:
<h1> 賬號:{{ user['name'] }},密碼:{{ user['passwd'] }}
複製程式碼
- 2.控制結構
Jinja2提供了多種控制結構,比如常見的判斷和迴圈結構,用於修改模板的渲染流程。 我們把上面判斷的邏輯也放到模板裡,示例如下:
{# 註釋,不會輸出到瀏覽器中 #}
{% if user == 'admin' or user == '管理員' %}
<h1> 管理員,您好! </h1>
{% else %}
<h1> {{ user }},您好!</h1>
{% endif %}
{# 迴圈列印 #}
{% for num in num_list %}
<h2>{{ num }}</h2>
{% endfor %}
複製程式碼
接著修改下hello.py檔案:
@app.route("/<user>")
def hello(user):
return render_template("index.html", user=user, num_list=[1, 2, 3])
複製程式碼
鍵入不同的URL,對應瀏覽器的輸出結果如下:
http://127.0.0.1:5000/admin
管理員,您好!
1
2
3
http://127.0.0.1:5000/jay
jay,您好!
1
2
3
複製程式碼
- 3.巨集
在Python中如果有一些很常用的程式碼,我們會習慣性地抽取成一個函式,在Jinji2中, 可以使用巨集來實現。語法如下:
# 建立巨集
{% macro 標籤名(key=value) %}
常用程式碼
{% end macro %}
# 呼叫巨集
{{ 標籤名(key=value) }}
複製程式碼
如果巨集比較多,可以抽到單獨的HTML中,再import進來。
{% import 'xxx.html' as 別名 %}
{{ 別名.標籤名(key=value) }}
複製程式碼
- 4.模板繼承
另一種程式碼複用的方式:模板繼承,和Python中類繼承一樣,需要一個基模板,用
{% block XXX %}{% endblock %}
標識一個程式碼塊,可以在子模組中過載。
程式碼示例如下:
# 基模板:base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% block head %}
<title>{% block title %} Title {% endblock %} </title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
# 子模板:son.html
{% extends "base.html" %}
{% block title %}Son{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block body %}
<h1>Hello Flask!</h1>
{% endblock %}
複製程式碼
程式碼簡述:
第一行程式碼使用extends命令,表明該模板繼承自base.html,接著重寫了title,head和 body程式碼塊,上面的super()函式用於獲取基模板原先的內容。
- 5.inclue
可以用include語句包含一個模板,在渲染時會把include語句對應位置新增被包含的模板, 使用示例如下:
{% include 'header.html' %}
複製程式碼
另外還可以使用「ignore missing」標記,如果模板不存在,Jinja2會忽略此語句,示例如下:
{% include 'header.html' ignore missing %}
複製程式碼
- 6.賦值
可以使用set標籤來進行賦值,示例如下:
{% set a,b = 1,2}
<h1>{{ a }}{{ b }}</h1>
複製程式碼
- 7.內建過濾器
過濾器的本質就是函式,有時需要修改,格式化或者執行變數間的計算,而在模板中是不能 直接呼叫Python中的某些方法的,可以用到過濾器。模板的過濾器支援鏈式呼叫:
{{ "hello flask" | reverse | upper}},
複製程式碼
這串程式碼就是反轉+轉換成大寫,常見的內建過濾器有字串和列表兩種。 字串操作過濾器如下表所示:
過濾器 | 描述 |
---|---|
safe | 禁用轉義 |
capitalize | 把變數值的首字母轉成大寫,其餘字母轉小寫 |
lower | 把值轉成小寫 |
upper | 把值轉成大寫 |
title | 把值中的每個單詞的首字母都轉成大寫 |
reverse | 字串反轉 |
format | 格式化輸出 |
striptags | 渲染之前把值中所有的HTML標籤都刪掉 |
truncate | 字串截斷 |
列表操作過濾器如下表所示:
過濾器 | 描述 |
---|---|
first | 取第一個元素 |
last | 取最後一個元素 |
length | 獲取列表長度 |
sum | 列表求和 |
sort | 列表排序 |
- 8.自定義過濾器
直接在py檔案中編寫,程式碼示例如下:
# 1.定義過濾器
def do_listreverse(li):
temp_li = list(li)
temp_li.reverse()
return temp_li
# 2.新增自定義過濾器
app.add_template_filter(do_listreverse, 'listreverse')
複製程式碼
總結:
巨集,繼承和包含都用實現程式碼複用,巨集功能類似於函式,可以傳參,需要定義呼叫; 繼承本質是程式碼替換,一般用來實現多個頁面中重複不變的區域;而包含是直接將 目標模板檔案整個渲染出來。
7、請求與相應
在Flask中,HTTP請求被封裝成了Request物件,而HTTP響應則被封裝成了Response物件。 因此Flask應用開發的邏輯處理,都是基於這兩個物件。
0x1 Request請求
Flask會將WSGI伺服器轉發的http請求資料封裝為一個Request物件,這個物件中包含了請求的 相關資訊,可以通過下表中的屬性來獲取對應的資訊。
屬性 | 描述 | 資料型別 |
---|---|---|
form | 記錄請求中的表單資料。 | MultiDict |
args | 記錄請求中的查詢引數。 | MultiDict |
cookies | 記錄請求中的cookie。 | Dict |
headers | 記錄請求中的報文頭。 | EnvironHeaders |
method | 記錄請求使用的HTTP方法。 | string |
environ | 記錄WSGI伺服器轉發的環境變數。 | Dict |
url | 記錄請求的URL地址。 | string |
- 1.讀取request的查詢引數
瀏覽器以GET請求的方式提交的表單資料,Flask會將其儲存在request例項的args,也可以用values屬性 來查詢,讀取程式碼示例如下:
# coding=utf-8
from flask import Flask, request, json
app = Flask(__name__)
@app.route("/index")
def index():
return '''
<form method="GET" action="/login">
<input type="text" placeholder="賬號" name="user"> <br />
<input type="text" placeholder="密碼" name="pawd"> <br />
<input type="submit" value="登入">
</form>
'''
@app.route("/login")
def login():
msg = ""
if 'user' in request.args:
msg += request.args['user'] + ':'
msg += request.values.get('pawd','')
return msg
if __name__ == "__main__":
app.run(debug=True)
複製程式碼
程式碼執行後,開啟:http://127.0.0.1:5000/index,瀏覽器:
輸入賬號密碼後,跳轉到:http://127.0.0.1:5000/login?user=zpj&pawd=123, 瀏覽器:
- 2.讀取request的表單資料
瀏覽器以GET請求的方式提交的表單資料,Flask會將其儲存在request例項的form中,可以使用[]操作符 讀取指定鍵值。讀取程式碼示例如下:
# coding=utf-8
from flask import Flask, request, json
app = Flask(__name__)
@app.route("/index")
def index():
return '''
<form method="POST" action="/login">
<input type="text" placeholder="賬號" name="user"> <br />
<input type="text" placeholder="密碼" name="pawd"> <br />
<input type="submit" value="登入">
</form>
'''
@app.route("/login", methods=['POST'])
def login():
msg = ""
msg += request.form['user'] + ':'
msg += request.form['pawd']
return msg
if __name__ == "__main__":
app.run(debug=True)
複製程式碼
程式碼執行後,開啟:http://127.0.0.1:5000/index,瀏覽器:
輸入賬號密碼後,跳轉到:http://127.0.0.1:5000/login,瀏覽器:
0x2 Response響應
和Request對應,Response用於給瀏覽器傳送響應資訊,根據檢視函式的返回結果。這個檢視函式 就是我們路由下面的函式,檢視函式的返回值會自動轉換成一個響應物件,轉換邏輯如下:
- 1、返回值是合法的響應物件,從檢視直接返回;
- 2、返回值是字串,會用字串和預設引數建立以字串為主體,返回碼為200,MIME型別為 text/html的werkzeug.wrappers.Response響應物件。
- 3、返回值是元組,其中的元素可以提供額外資訊,但格式必須是(response,status,headers) 的形式,至少包含一個元素,status值會覆蓋狀態碼,headers可以是字典或列表,作為額外的訊息頭。
- 4、如果都不是,Flask會假設返回值是合法的WSGI程式,通過Response.force(rv.request.environ) 轉換為一個請求物件。除了通過return方式返回,還可以顯式地呼叫make_response函式返回,好處是可以 設定一些額外的資訊,示例如下:
def hello():
resp = make_response("Hello Flask!", 250)
return resp
複製程式碼
另外,現在的API介面都是返回JSON格式的,可以用jsonify包裝下,修改後的示例程式碼如下:
# coding=utf-8
from flask import Flask, make_response, jsonify
from werkzeug.wrappers import Response
app = Flask(__name__)
@app.route("/", methods=['GET', 'POST'])
def hello():
resp = make_response({'code': '200', 'msg': '請求成功', 'data': [{'data_1': ['資料', '資料'], 'data_2': ['資料', '資料']}]})
return resp
class JsonResponse(Response):
@classmethod
def force_type(cls, response, environ=None):
if isinstance(response, dict):
response = jsonify(response)
return super(JsonResponse, cls).force_type(response, environ)
app.response_class = JsonResponse
if __name__ == "__main__":
app.run(debug=True)
複製程式碼
請求介面輸出如下:
{
"code": "200",
"data": [
{
"data_1": [
"資料",
"資料"
],
"data_2": [
"資料",
"資料"
]
}
],
"msg": "請求成功"
}
複製程式碼
也可以用Flask的json模組的dumps()函式將陣列或字典物件轉換為JSON字串,程式碼示例如下:
data = {'code': '200', 'msg': '請求成功', 'data': [{'data_1': ['資料', '資料'], 'data_2': ['資料', '資料']}]}
return json.dumps(data),200,[('Content-Type','application/json;charset=utf-8')]
複製程式碼
- 3.設定Cookie Response類中提供了set_cookie()函式用於設定客戶端的cookie,如果要設定cookie,需要我們自己構建Response例項 (通過make_response),可選引數如下:
set_cookie(
key, //鍵
value='', //值
max_age=None, //秒為單位的cookie壽命,None表示http-only
expires=None, //失效時間,datetime物件或unix時間戳
path='/', //cookie的有效路徑
domain=None, //cookie的有效域
secure=None,
httponly=False)
複製程式碼
8、重定向與會話
Web開發中經常需要處理重定向和會話,Flask中內建了「redirect」和「session」來對它們進行處理。
0x1 重定向
頁面重定向非常常見,最常用的就是登陸狀態判斷,如果沒登陸將網頁重定向到登入頁,Flask中可以使用redirect 物件對其進行處理,狀態碼預設為302,可以傳入code引數來修改,一般是:301,302,303,305和307, 簡單的程式碼示例如下:
# coding=utf-8
from flask import Flask, redirect
app = Flask(__name__)
user_name = ''
@app.route('/article')
def article():
if user_name == '':
# 如果使用者名稱為空,重定向跳轉到登入頁
return redirect('/login')
else:
return "文章頁"
@app.route("/login")
def login():
global user_name
user_name = 'admin'
return "登入成功!"
if __name__ == "__main__":
app.run(debug=True)
複製程式碼
執行後操作流程如下:
瀏覽器鍵入:http://127.0.0.1:5000/article
自動跳轉到:http://127.0.0.1:5000/login,顯示登入成功
再次訪問:http://127.0.0.1:5000/article,顯示文章頁
複製程式碼
0x2 會話
我們可以把資料儲存在使用者會話(session)中,使用者會話是一種私有儲存,預設情況下儲存在客戶端cookie中。 會話主要為了解決兩個問題:「訪問者的標識和訪問者資訊記錄」。瀏覽器第一次訪問伺服器,伺服器在其 cookie中設定一個唯一的會話ID,瀏覽器後續對伺服器的訪問頭中將自動包含該資訊,伺服器通過這個ID號來 區分不同的訪問者。session依賴於cookie,一般儲存在伺服器,Flask提供了session物件來操作使用者會話, 可以使用[]操作符讀取或者設定指定鍵值,預設情況下,Flask將會話物件加密後儲存在客戶端的cookie裡, 因此必須要應用例項的secret_key屬性配置一個加密種子才能使用session。用法示例如下:
# 設定session
session['name'] = 'jay'
# 讀取session
session.get('name')
# 配置加密種子(兩種方法二選一)
app.secret_key = '123456'
app.config['SECRET_KEY']='123456'
複製程式碼
9、靜態檔案管理
靜態檔案就是那些不會被改變的檔案,例如:圖片,CSS樣式檔案,JavaScript指令碼檔案和字型檔案等。 Flask預設會在根目錄中名為static的子目錄中尋找靜態檔案,所以如果需要用到靜態檔案可以建立一個 static的資料夾,然後把靜態檔案丟裡面。可以參考下面這樣的結構來組織專案:
static/
css/
lib/
bootstrap.css
style.css
home.css
js/
lib/
jquery.js
chart.js
home.js
img/
logo.svg
favicon.ico
複製程式碼
另外,不要在模板中寫死靜態檔案路徑,應該使用url_for函式生成路徑,示例如下:
url_for('static', filename='css/style.css')
複製程式碼
當然,如果你想修改靜態檔案的真實目錄,可以在Flask建構函式中傳入引數:static_folder='資料夾名'。 另外,為了獲得更好的處理能力,建議使用Nginx或其他Web伺服器管理靜態檔案,圖片這類資源可以 託管到CDN平臺上。(比如七牛雲)
10、藍圖
藍圖(Blueprint),定義了可用於單個應用的檢視,模板,靜態檔案等等的集合。通俗點理解就是 一個實現應用模組化的好工具,使用藍圖能使得專案層次更加清晰,更易於開發和維護,通常作用於 相同URL字首的路由。先來看一個沒使用藍圖的示例:
# coding=utf-8
from flask import Flask
app = Flask(__name__)
@app.route('/user/index')
def user_index():
return 'user_index'
@app.route('/user/show')
def user_show():
return 'user_show'
@app.route('/user/add')
def user_add():
return 'user_add'
@app.route('/admin/index')
def admin_index():
return 'admin_index'
@app.route('/admin/show')
def admin_show():
return 'admin_show'
@app.route('/admin/add')
def admin_add():
return 'admin_add'
if __name__ == "__main__":
app.run(debug=True)
複製程式碼
上面的程式碼挺整齊的,不過有幾個問題:
- 如果user和admin的功能不止上面的幾個,而是好幾百呢,程式碼會非常龐大臃腫。
- 大型的專案都是多人協作的,所有人都在這裡檔案裡開發的話,處理合並衝突會很頭痛。
- 如果哪天這兩個使用者模組不要了,還需要一行行的去找,然後刪程式碼。
我們使用藍圖來把user和admin拆分成兩個不同的.py檔案,admin.py 檔案內容如下:
# coding=utf-8
from flask import Blueprint
admin = Blueprint('admin', __name__,url_prefix='/admin')
@admin.route('/index')
def admin_index():
return 'admin_index'
@admin.route('/show')
def admin_show():
return 'admin_show'
@admin.route('/add')
def admin_add():
return 'admin_add'
複製程式碼
user.py 檔案內容如下:
# coding=utf-8
from flask import Blueprint
user = Blueprint('user',__name__)
@user.route('/index')
def user_index():
return 'user_index'
@user.route('/show')
def user_show():
return 'user_show'
@user.route('/add')
def user_add():
return 'user_add'
複製程式碼
註冊藍圖,hello.py的程式碼內容如下:
# coding=utf-8
from flask import Flask
from admin import *
from user import *
app = Flask(__name__)
app.register_blueprint(admin)
app.register_blueprint(user, url_prefix='/user')
if __name__ == "__main__":
print(app.url_map)
app.run(debug=True)
複製程式碼
利用app.url_map函式,檢視所有的路由,列印結果如下:
Map([<Rule '/admin/index' (GET, HEAD, OPTIONS) -> admin.admin_index>,
<Rule '/admin/show' (GET, HEAD, OPTIONS) -> admin.admin_show>,
<Rule '/admin/add' (GET, HEAD, OPTIONS) -> admin.admin_add>,
<Rule '/user/index' (GET, HEAD, OPTIONS) -> user.user_index>,
<Rule '/user/show' (GET, HEAD, OPTIONS) -> user.user_show>,
<Rule '/user/add' (GET, HEAD, OPTIONS) -> user.user_add>,
<Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>])
複製程式碼
url_prefix
這個引數用於設定request.url
中的url字首,另外只有滿足字首的請求才會
通過註冊的藍圖的檢視方法處理請求並返回。可以寫在子模組中,也可以在register_blueprint
註冊藍圖的時候傳入,只需傳入一次!之後開啟http://127.0.0.1:5000/admin/index,可以看到
如圖所示的結果:
10、g物件和鉤子函式
有時在處理請求前後,執行某些特定程式碼是非常有用的,這就用到了請求鉤子,比如:請求前建立db連結, 驗證使用者身份等,flask提供了註冊通用函式的功能,只需要寫一個請求鉤子——函式,整個程式例項全域性都被應用, 比如請求前驗證使用者狀態的例子,沒登陸跳轉登入頁面等。鉤子函式需要藉助Flask的全域性變數g,g作為中間變數, 在鉤子函式和檢視函式間傳遞資料。
0x1 g物件
g,global,g物件是專門用來儲存使用者資料的,儲存的資料在全域性都可以使用。程式碼示例如下:
from flask import g, request
@app.route('/')
def index():
user = request.args.get('user')
g.user = user # 儲存使用者資料
複製程式碼
0x2 鉤子函式
Flask提供下述四種鉤子函式:
- before_first_request:在第一次請求前呼叫,可以在此方法內做一些初始化操作。
- before_request:在每次請求前呼叫,一般做校驗,如果校驗不成功,可以在這個方法內直接響應,直接return的話,不會執行檢視函式。
- after_request:在執行完檢視函式之後會呼叫,並把檢視函式生成的響應傳入,可以在此方法中對響應做最後一步同意的處理。
- teardown_request:每一次請求後都會呼叫,會接收一個引數——伺服器出現的錯誤資訊。
寫一個程式來驗證下鉤子函式的執行流程(執行後,訪問兩次):
127.0.0.1 - - [30/Aug/2018 10:53:42] "GET / HTTP/1.1" 200 -
before_first_request
before_request
after_request
teardown_request
127.0.0.1 - - [30/Aug/2018 10:53:45] "GET / HTTP/1.1" 200 -
before_request
after_request
teardown_request
複製程式碼
11、上下文
上下文相當於一個容器,儲存了Flask程式執行過程中的一些資訊,根據管理機制分為兩種:
-
請求上下文(RequestContext) Request:請求的物件,封裝了Http請求的內容; Session:根據請求中的cookie,過載訪問者相關的會話資訊。
-
程式上下文(AppContext) g:處理請求時用作臨時儲存的物件,儲存的是當前請求的全域性變數,不同的請求會有不同的全域性變數! current_app:當前執行程式的程式例項,儲存的是應用程式的變數,比如可以使用current_app.name獲取當前應用的名稱, 也可以在current_app中儲存一些配置資訊,變數等,使用示例:current_app.text = 'value'。
Flask中,而關於上下文的管理可以分為三個階段:
- 請求進來時:將request,session封裝到RequestContext類中,app, g封裝在AppContext類中, 並通過LocalStack將RequestContext和AppContext放入Local類中。
- 檢視函式:通過localproxy -> 偏函式 -> localstack -> local取值。
- 請求結束前:執行save.session() -> 各自執行pop() -> 清除local中的資料。
12、異常處理
在開發中,後臺發生異常,但又不想把異常顯示給使用者看,或者需要同一處理的時候,可以使用abort()函式 主動丟擲異常,再捕獲異常,然後返回一個美化後的頁面,最常見的就是404了,程式碼示例如下:
@app.route("/test")
def test():
abort(404)
@app.errorhandler(404)
def error(e):
return "一個精美的404頁面"
複製程式碼
程式碼執行後,瀏覽器鍵入:http://127.0.0.1:5000/test,可以看到如圖所示的結果:
另外,你也可以把所有異常處理些寫到一個藍圖中,程式碼示例如下:
# coding=utf-8
from flask import Blueprint, abort
exception = Blueprint('exception', __name__)
@exception.errorhandler(404)
def error(e):
return "一個精美的404頁面"
# 註冊藍圖
from error import exception
app.register_blueprint(exception, url_prefix='/error')
複製程式碼
13、ORM框架——SQLAlchemy
使用物件對映關係(Object-Relational Mapper)ORM框架來運算元據庫。所謂的ORM框架就是:
「將底層的資料操作指令抽象成高層的物件導向操作」
不用再寫繁瑣的SQL操作語句,利用ORM框架可以簡化成對Python物件的操作。
表對映成類
,行作為例項
,欄位作為屬性
.
ORM在執行物件操作時會將對應操作轉換為資料庫原生語句。Python中用得最廣泛的ORM框架是SQLAlchemy。
0x1 安裝flask-sqlalchemy
直接使用pip命令安裝即可
pip install flask-sqlalchemy
複製程式碼
另外SQLAlchemy本身無法運算元據庫,依賴於pymysql等第三方庫,裡面有個Dialect模組專門用於 和資料API交流,根據配置檔案的不同而呼叫不同的資料庫API,從而實現對資料庫的操作。資料庫使用 URL限定,常見的資料庫引擎與其對應的URL如下表所示:
資料庫引擎 | URL |
---|---|
MySQL | mysql+pymysql://username:password@hostname/database |
Postgres | postgresql://username:password@hostname/database |
SQLite (Unix,開頭四個斜線) | sqlite:////absolute/path/to/database |
SQLite (Windows) | sqlite:///c:/absolute/path/to/database |
Postgres | postgresql://username:password@hostname/database |
Oracle | oracle://username:password@hostname/database |
引數簡述:
- username:登入資料庫的使用者名稱。
- password:登入資料庫的密碼。
- hostname:SQL服務所在的主句,可以是本地也可以是遠端。
- database:使用的資料庫。
0x2 連線資料庫
連線MySQL資料庫的程式碼示例如下:
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://jay:zpj12345@localhost:3306/todo')
with engine.connect() as con:
rs = con.execute("SELECT 1")
print(rs.fetchone())
複製程式碼
輸出結果如下:
(1,)
複製程式碼
另外還可以通過下面這樣的方法來初始化SQLAlchemy,程式碼示例如下:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
app = Flask(__name__)
db.init_app(app)
複製程式碼
0x3 使用原生SQL
sqlalchemy支援直接執行原生的SQL語句,程式碼示例如下:
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://jay:zpj12345@localhost:3306/todo')
with engine.connect() as con:
con.execute("CREATE TABLE IF Not Exists note(_id INT AUTO_INCREMENT PRIMARY KEY, tran TEXT, status int)")
con.execute("INSERT INTO note(tran, status) VALUES ('吃飯', 1)")
con.execute("INSERT INTO note(tran, status) VALUES ('睡覺', 1)")
rs = con.execute("SELECT * FROM note")
for row in rs:
print(row)
複製程式碼
程式碼輸出結果如下:
(1, '吃飯', 1)
(2, '睡覺', 1)
複製程式碼
0x4 ORM模型與基本操作
演示一波Flask-SQLAlchemy的基本操作:
- 1.建表(create_all,對應的刪除表可以用drop_all)
# models.py
from manager import db
class Note(db.Model):
__tablename__ = 'note'
_id = db.Column(db.INTEGER, primary_key=True, autoincrement=True)
tran = db.Column(db.TEXT)
status = db.Column(db.INT,default=0)
db.create_all() # 建立表
複製程式碼
- 2.插入資料
from model import Note
from manager import db
def create_note():
# 建立一個新物件
note1 = Note()
note1.tran = "吃飯"
note1.status = "1"
note2 = Note()
note2.tran = "睡覺"
note2.status = "2"
# 將新建筆記新增到資料庫會話中
db.session.add(note1)
db.session.add(note2)
# 將資料庫會話中的變動提交到資料庫中,如果不commit,資料庫內容是不會變化的
db.session.commit()
create_note()
複製程式碼
- 3.刪除資料
def delete_note():
# 獲取筆記物件(這裡是獲取_id=1的記錄)
note = Note.query.filter_by(_id=1).first()
# 刪除筆記
db.session.delete(note)
# 提交資料庫會話
db.session.commit()
delete_note()
複製程式碼
- 4.修改資料
def update_note():
# 獲取筆記物件(這裡是獲取_id=2的記錄)
note = Note.query.filter_by(_id=2).first()
# 修改筆記內容
note.tran = "打豆豆"
# 提交資料庫會話
db.session.commit()
update_note()
複製程式碼
- 5.查詢資料
說到查詢,必然有查詢條件,SQLAlchemy提供瞭如下表所示的常用查詢函式:
函式 | 描述 | 使用示例 |
---|---|---|
filter_by | 精確查詢 | filter_by(xxx='xxx') |
filter | 模糊查詢 | filter(xxx.endWith('xxx')) |
get(主鍵) | 根據主鍵查詢,一般為id | get(1) |
not_() | 邏輯非,也可以直接把==換成!= | not_(xxx='xxx') |
and_() | 邏輯與 | and_(xxx='xxx') |
or_() | 邏輯或 | or_(xxx='xxx') |
in_() | 在某個範圍裡 | XXX.xxx.in_((1,2,3)) |
notin_() | 不在某個範圍內 | XXX.xxx.notin_((1,2,3)) |
first() | 返回查詢到的一個物件 | XXX.query.first() |
all() | 返回查詢到的所有物件 | XXX.query.all() |
order_by() | 排序 | XXX.order_by(xxx.xxx.desc()) |
limit() | 限制返回條數 | XXX.limit(3) |
offset() | 設定偏移量 | XXX.offset() |
count() | 返回記錄的總條數 | xxx.count() |
程式碼示例如下:
from sqlalchemy import not_, or_
def query_all():
notes = Note.query.all()
print("查詢全部資料:", notes)
note = Note.query.filter(not_(or_(Note.tran == '吃飯', Note.tran == '睡覺'))).first()
print(note._id, ":", note.tran, ":", note.status)
if __name__ == '__main__':
# 先插入幾條資料
create_note()
create_note()
query_all()
複製程式碼
輸出結果如下:
查詢全部資料: [<Note 2>, <Note 3>, <Note 4>, <Note 5>, <Note 6>]
2 : 打豆豆 : 2
複製程式碼
14.Web表單外掛——Flask-WTF
Flask中一般不會直接用原始表單,而是通過Flask-WTF擴充套件,它簡單繼承了WTForms,包括CSRF (跨域請求偽造),驗證表單資料的功能,檔案上傳以及Google內嵌的驗證碼。
0x1 WTForms支援的HTML標準欄位
欄位型別 | 說明 |
---|---|
StringField | 文字欄位 |
TextAreaField | 多行文字欄位 |
PasswordField | 密碼文字欄位 |
HiddenField | 隱藏文字欄位 |
DateField | 文字欄位,值為datetime.date格式 |
DateTimeField | 文字欄位,值為datetime.datetime格式 |
IntegerField | 文字欄位,值為整數 |
DecimalField | 文字欄位,值為decimal.Decimal |
FloatField | 文字欄位,值為浮點數 |
BooleanField | 核取方塊,值為True和False |
RadioField | 一組單選框 |
SelectField | 下拉選單 |
SelectMultipleField | 下拉選單,可選擇多個值 |
FileField | 檔案上傳欄位 |
SubmitField | 表單提交按鈕 |
FormField | 把表單作為欄位嵌入另一個表單 |
FieldList | 一組指定型別的欄位 |
0x2 WTForms驗證函式
驗證函式 | 說明 |
---|---|
驗證電子郵件地址 | |
EqualTo | 比較兩個欄位的值,常用於要求輸入兩次密碼進行確認的情況 |
IPAddress | 驗證IPv4網路地址 |
Length | 驗證輸入字串的長度 |
NumberRange | 驗證輸入的值在數字範圍內 |
Optional | 無輸入值時跳過其他驗證函式 |
Required | 確保欄位中有資料 |
Regexp | 使用正規表示式驗證輸入值 |
URL | 驗證URL |
AnyOf | 確保輸入值在可選值列表中 |
NoneOf | 確保輸入值不在可選列表中 |
0x3 寫個簡單的例子
接著我們來編寫一個登錄檔單的例子:
# coding=utf-8
from flask import Flask, request, render_template
from flask_wtf import FlaskForm
# 匯入自定義表單需要的欄位
from wtforms import SubmitField, StringField, PasswordField
# 匯入表單驗證
from wtforms.validators import DataRequired, EqualTo
app = Flask(__name__)
app.config['SECRET_KEY'] = '123456'
# 自定義表單類
# StringField和PasswordField用於區分文字框型別
# 第一個引數是label值,第二個引數validators是要驗證的內容
class RegisterForm(FlaskForm):
username = StringField('使用者名稱:', validators=[DataRequired()])
password = PasswordField('密碼:', validators=[DataRequired()])
password2 = PasswordField('確認密碼:', validators=[DataRequired(), EqualTo('password', '兩次輸入的密碼不一致!')])
submit = SubmitField('註冊')
@app.route("/register", methods=['GET', 'POST'])
def register():
# 例項化登錄檔單類
register_from = RegisterForm()
if request.method == 'POST':
# 獲取請求引數引數
username = request.form.get('username')
password = request.form.get('password')
password2 = request.form.get('password2')
# 呼叫validation_on_submit,一次性執行完所有驗證函式的邏輯
if register_from.validate_on_submit():
return '註冊成功!'
else:
return '前後密碼不一致!'
return render_template('register.html', form=register_from)
if __name__ == "__main__":
print(app.url_map)
app.run(debug=True)
複製程式碼
# templates/register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post">
{# 設定scrf_token #}
{{ form.csrf_token() }}
{{ form.username.label }} {{ form.username }}<br>
{{ form.password.label }} {{ form.password }}<br>
{{ form.password2.label }} {{ form.password2 }}<br>
<br>
{{ form.submit }}
</form>
</body>
</html>
複製程式碼
輸入一波一樣的密碼和不一樣的密碼,瀏覽器的輸出結果如下所示:
另外有一點要注意:
使用Flask-WTF需要配置引數SECRET_KEY,CSRF_ENABLED是為了**CSRF(跨站請求偽造)**保護。 SECRET_KEY用於生成加密令牌,當CSRF啟用的時候,該設定會根據設定的金鑰生成加密令牌。
15.一個簡單通用的Flask專案結構
Flask基礎學得差不多了,接著我們來規範下專案結構,一個比較簡單通用的專案結構如圖:
簡述下結構:
- app:整個專案的包目錄。
- models:資料模型。
- static:靜態檔案,css,JavaScript,圖示等。
- templates:模板檔案。
- views:檢視檔案。
- config.py:配置檔案。
- venv:虛擬環境。
- manage.py:專案啟動控制檔案。
- requirements.txt:專案啟動控制檔案。
建立流程:
在__init__.py
中初始化app例項,程式碼如下:
from flask import Flask
app = Flask(__name__)
複製程式碼
views.py中寫個簡單的index路由:
from app import app
from flask import render_template
@app.route('/')
def index():
return render_template("index.html")
複製程式碼
templates資料夾建立一個index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello Flask!</h1>
</body>
</html>
複製程式碼
接著鍵入:python manage.py runserver,瀏覽器開啟:http://127.0.0.1:5000/
專案結構基本就弄好了,接著我們把專案丟到遠端伺服器上。
16.把Flask專案部署到雲伺服器上
0x1 程式碼上傳至雲伺服器
有兩種可選的方法,最簡單的就是通過FTP/SFTP工具直接傳。另外一種是把專案託管到 類似於Github這類程式碼託管平臺,然後直接ssh連雲伺服器,通過git clone命令把專案 拷貝到伺服器上,這裡筆者直接用的第一種方法,把專案上傳到雲伺服器上。
0x2 安裝nginx
Nginx是一款輕量級、效能強、佔用資源少,能很好的處理高併發的反向代理軟體。 Flask自帶的Web Server只能開單執行緒,自己測試還行,放到線上就不行了,這裡 我們用到Nginx,直接通過apt-get命令進行安裝,命令如下:
apt-get install nginx
複製程式碼
安裝完後,外網訪問伺服器的公網ip,出現下述頁面說明安裝成功:
Nginx安裝完,會預設建立一個目錄:/var/www/,直接通過命令把我們的 專案移動到這個路徑下。
mv AutoPubNews /var/www/AutoPubNews
複製程式碼
0x3 安裝配置uwsgi
WSGI是一種WEB伺服器,或者叫閘道器介面,Web伺服器(如nginx)與應用伺服器 (如uWSGI)通訊的一種規範(協議)。而uWSGI實現了WSGI的所有介面,是一個 快速、自我修復、開發人員和系統管理員友好的伺服器。uWSGI程式碼完全用C編寫, 效率高、效能穩定。舉個例子:uWSGI把HTTP協議轉化成WSGI協議,讓Python可以 直接使用。直接鍵入pip命令安裝:
pip install uwsgi
複製程式碼
一般都是能直接安裝完成的,如果出錯了可以試試先安裝libpython3.x-dev, 比如筆者的版本是3.5:
apt-get install libpython3.5-dev
複製程式碼
接著配置一下uwsgi,在專案裡新建一個config.ini作為配置檔案:
vim config.ini
複製程式碼
新增下述內容:
[uwsgi]
# uwsgi 啟動時所使用的地址與埠
socket = 127.0.0.1:8001 # 可以使用其他埠
# 指向網站目錄
chdir = /var/www/AutoPubNews
# python 啟動程式檔案
wsgi-file = manage.py
# python 程式內用以啟動的 application 變數名
callable = app
# 處理器數
processes = 4
# 執行緒數
threads = 2
#狀態檢測地址
stats = 127.0.0.1:5000
複製程式碼
接著執行下述命令:
uwsgi config.ini
複製程式碼
出現: Stats server enabled on 127.0.0.1:5000,代表正常啟動。
0x4 配置Nginx
接著配置下Nginx,不要去動預設的nginx.conf,直接將:/etc/nginx/sites-available/default
覆蓋掉,新建default檔案,新增下述內容:
server {
listen 80;
server_name _;
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8001; # 指向uwsgi 所應用的內部地址,所有請求將轉發給uwsgi 處理
uwsgi_param UWSGI_PYHOME /home/www/AutoPubNews/venv; # 指向虛擬環境目錄
uwsgi_param UWSGI_CHDIR /home/www/AutoPubNews; # 指向網站根目錄
uwsgi_param UWSGI_SCRIPT manager:app; #
}
}
複製程式碼
接著鍵入下述命令重啟載入下nginx配置:
sudo service nginx restart
複製程式碼
接著啟動uwsgi,然後就可以通過伺服器的公網ip直接訪問我們的專案了:
0x5 域名解析
每次訪問專案都用ip,顯得有些繁瑣,我們可以買個域名做下對映耍耍。 域名直接買就好,需要備案,搞定後開啟域名管理頁,找到剛買的域名 點選解析
然後點選「新增記錄」會出現如圖所示的對話方塊,記錄值那裡填你的雲伺服器公網ip即可。
此時就可以直接通過域名來訪問我們的專案了。
行吧,關於Flask速成就這麼都,下節我們來利用Flask編寫API介面,動態生成頁面等。 有疑問的歡迎在評論區留言,謝謝~
Tips:公號目前只是堅持發早報,在慢慢完善,有點心虛,只敢貼個小圖,想看早報的可以關注下~