77.Flask基礎知識
簡介
Flask是Python的一個輕量微框架。微框架中的“微”,意味著Flask核心簡單,但這並不意味著Flask功能薄弱,相反,Flask有許多外掛支援連線資料庫、表單驗證、檔案上傳等各種功能,配合這些外掛可以實現非常強大的功能。Flask是Python在Web開發領域的一件利器。
安裝
Flask支援Python>=3.4、Python 2.7或PyPy,安裝命令:
pip install Flask
自動安裝如下依賴:
- Werkzeug:Python的一種WSGI實現。
- Jinja:一種模板渲染語言。
- MarkupSafe:配合Jinja,轉義客戶端的輸入,規避跨站指令碼攻擊(Cross Site Script,CSS)。
- ItsDangerous:利用數字簽名技術保證資料一致性,用來保護Flask的session加密。
- Click:編寫命令列程式的框架,Flask用它支援
flask
命令並允許新增自定義命令。
如下依賴不會自動安裝,如果環境上存在的話,Flask會自動載入使用:
- Blinker:支援訊號量(Signal)。
- SimpleJSON:相容Python的
json
模組,速度更快。 - python-dotenv:執行
flask
命令時,提供從dotenv
提取環境變數的支援。 - Watchdog:開發模式下,提供更快、更高效的裝載器。
小試牛刀
# hello.py,應用程式檔名不能是flask.py,這會和Flask本身衝突
from flask import Flask #一個Flask類的例項表示一個Web應用程式
app = Flask(__name__) #第一個引數是應用程式的模組或包名,Flask根據此引數查詢模板、靜態檔案等內容
@app.route('/') #route()裝飾器,定義URL和檢視函式的繫結關係
def hello_world():
return "Hello, World!"
啟動應用程式可以使用flask
命令:
$ export FLASK_APP=hello.py
$ flask run
也可以使用Python的-m
開關:
$ export FLASK_APP= hello.py
$ python -m flask run
Flask會啟動一個非常簡單的內建伺服器( 僅供除錯,不建議用於生產環境),瀏覽器訪問http://127.0.0.1:5000即可看到Hello, World
的響應:
預設情況下,Flask應用程式只監聽本機loopback網路,flask run --host=0.0.0.0
可以令程式監聽所有網路埠。一定要確保關閉了除錯功能,再開放監聽所有網路埠。
開發模式與除錯功能
Flask的開發模式,在程式出錯時提供除錯功能,並且自動裝載最新的程式碼,不必每次更新程式碼都重啟應用程式。flask run
之前通過如下方式開啟開發模式:
export FLASK_ENV=development
開發模式的Flask開啟瞭如下功能:
- 開啟偵錯程式
- 開啟自動裝載器
- 在Flask應用程式上應用偵錯程式
如果想只在Flask應用程式上應用偵錯程式,使用如下命令:
export FLASK_DEBUG=1
生產環境無論如何都不要開啟開發模式,開發模式下允許應用程式的使用者在伺服器上執行任意的Python程式碼。比如如下異常場景: 滑鼠懸浮在報錯的那一層上,點選最後側的命令列圖示,即可基於該層進行除錯:
路由
Flask通過route()
裝飾器繫結URL和檢視函式,例如:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World'
URL的可變部分用<variable_name>
代替,<variable_name>
的值作為關鍵字引數傳遞給繫結的函式,也可以使用<converter:variable_name>
指定變數的型別。
from flask import Flask
app = Flask(__name__)
@app.route('/user/<username>')
def show_user_profile(username):
return 'User %s' % username
@app.route('/post/<int:post_id>')
def show_post(post_id):
return 'Post %d' % post_id
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
return 'Subpath %s' % subpath
converter
支援如下型別:
- string:除斜線(/)外任何字串。
- int:十進位制正整數。
- float:正浮點數。
- path:類似
string
型別,但是接受斜線(/)。 - uuid:接受uuid字串。 對於特殊字元(如#),在瀏覽器中輸入時需要URL轉碼後訪問(如%23)。
URL唯一性與URL重定向
有如下兩種格式定義的URL:
rom flask import Flask
app = Flask(__name__)
@app.route('/projects/')
def projects():
return 'The project page'
@app.route('/about')
def about():
return 'The about page'
訪問http://127.0.0.1:5000/projects
,Flask會將請求重定向到http://127.0.0.1:5000/projects/
。訪問http://127.0.0.1:5000/about/
,Flask響應404 Not Found。建議使用不帶字尾斜線的URL,為資源確定唯一的URL,防止搜尋引擎為同樣的頁面建立兩次索引。
生成URL
使用url_for()
函式生成某個方法的URL,url_for()
的第一個引數是方法名(字串),然後是關鍵字引數,這些關鍵字引數相當於URL的可變部分<variable_name>
,如果提供的關鍵字引數不存在,作為查詢引數生成URL(?key=value
)。
不將URL硬編碼到模板檔案中的理由:
- 生成URL比硬編碼更具可讀性。
- 修改URL格式時,只需修改
route()
裝飾器定義處,無需修改所有使用了此URL的地方。 - 生成URL自動處理字元轉義、Unicode編碼轉換等場景。
- 生成URL得到的是絕對路徑,避免了瀏覽器中使用相對路徑可能出現的問題。
- 如果應用程式不在URL根目錄下,比如在
/myapplication
目錄而不是/
目錄,url_for()
也能正確處理。
from flask import Flask
from flask import url_for
app = Flask(__name__)
@app.route('/')
def index():
return 'index'
@app.route('/login')
def login():
return 'login'
@app.route('/user/<username>')
def profile(username):
return '{}\'s profile'.format(username)
with app.test_request_context(): #假裝Flask收到了一個請求,即使是通過Python命令列啟動的指令碼
print url_for('index')
print url_for('login')
print url_for('login', next = '/')
print url_for('profile', username = 'mars loo')
執行結果:
/
/login
/login?next=%2F
/user/mars%20loo
HTTP方法
Flask路由預設支援HTTP GET方法,如果要支援其他HTTP方法,在定義route
時給出methods
引數:
from flask import Flask
from flask import request
app = Flask(__name__)
@app.route('/login', methods = ['GET', 'POST'])
def login():
if request.method == 'GET':
return 'Show the login form'
else:
return 'Valid user'
Flask預設實現了OPTIONS方法,如果URL支援GET方法,Flask也自動支援了HEAD方法。
靜態檔案
動態Web網站也需要靜態檔案,如CSS、JavaScript檔案等。使用靜態檔案,需要在包中建立一個名為static
的目錄(如果使用模組,建立與模組同級的static
目錄),然後就可以通過/static
的方式訪問了。獲取靜態檔案的URL路徑,可以置url_for()
方法,置第一個引數名為static
,第二個引數名為filename
:
from flask import Flask
from flask import url_for
app = Flask(__name__)
@app.route('/')
def index():
return url_for('static', filename = 'style.css')
獲取style.css
檔案的URL連結時,這個檔案不一定真的存在。
渲染模板
用Python生成HTML非常麻煩,需要考慮字元轉義、應用程式安全等問題。Flask引入了Jinja2
模板引擎支援自動渲染HTML檔案。
渲染模板可以render_template()
函式,函式的第一個引數是模板檔名,剩餘的關鍵字引數是要傳入模板的值,例如:
# application.py
from flask import Flask
from flask import render_template
app = Flask(__name__)
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name = None):
return render_template('hello.html', name = name)
建立與application.py同級的templates
目錄,放入hello.html
模板檔案:
<!-- hello.html -->
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
如果name
變數包含HTML程式碼,render_template()
會自動將其轉義(僅.html
、.htm
、.xml
、.xhtml
支援自動轉義,如果模板引數是字串,預設不支援自動轉義)。如果Flask應用程式是一個包,則templates
目錄包含在包的目錄下即可:
/application
/__init__.py
/templates
/hello.html
在模板檔案(本例是hello.html
)中,也可以使用request
、session
、g
等物件及get_flashed_messages()
方法。
模板配合繼承功能尤其有用,比如可以讓某些元素在所有頁面都存在(頁首、導航、頁尾等)。
獲取請求資料
Web應用程式的基本場景:服務端接收客戶端請求,服務端處理請求體後,傳送響應給客戶端。Flask通過全域性的request
物件訪問請求,該變數是執行緒安全的(thread local)。
使用request
物件要先import:
from flask import request
請求的HTTP方法通過request
物件的method
屬性獲得。通過form
屬性,可以獲得POST
及PUT
方法提交的資料。
from flask import Flask
from flask import request
from flask import render_template
app = Flask(__name__)
def valid_login(username, password):
return False
@app.route('/login', methods = ['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
return render_template('login.html', error = error)
login.html檔案內容:
<!doctype html>
<title>Login page</title>
{% if error %}
<p>{{ error }}</p>
{% endif %}
<form action="/login" enctype="multipart/form-data" method="post">
<label>username</label>
<input type="text" name="username"/>
<label>password</label>
<input type="password" name="password" />
<input type="submit" />
</form>
若form
屬性中的key不存在,會丟擲KeyError
異常,可以捕獲處理該異常,如果不處理,會返回HTTP 400 Bad Request,大多數情況無需自己處理該異常。
獲取URL中的引數(?key=value
),使用args
屬性(dict型別):
searchword = request.args.get('key', '')
檔案上傳
HTML網頁通過form
表格上傳檔案到Web服務端:
<!-- index.html -->
<!doctype html>
<title>upload file</title>
<body>
<form method="post" enctype="multipart/form-data" action="/upload">
<label>your file:</label>
<input type="file" name="certi_file" />
<input type="submit">
</form>
</body>
上傳檔案儲存在臨時記憶體,通過request.files
屬性獲取所有上傳檔案,例如:
from flask import Flask
from flask import request
from flask import render_template
app = Flask(__name__)
@app.route('/upload', methods = ['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['certi_file']
f.save('./certi_file')
return 'ok'
@app.route('/')
def index():
return render_template('index.html')
上傳檔案的原始檔名儲存在filename
屬性,直接用這個檔名有CSS攻擊風險,可以使用Werkzeug的secure_filename()
規避:
from flask import Flask
from flask import request
from flask import render_template
from werkzeug.utils import secure_filename
app = Flask(__name__)
@app.route('/upload', methods = ['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['certi_file']
f.save(secure_filename(f.filename))
return 'ok'
@app.route('/')
def index():
return render_template('index.html')
Cookie
request.cookie
屬性儲存客戶端上傳的所有cookie。讀取cookie:
from flask import Flask
from flask import request
app = Flask(__name__)
@app.route('/')
def index():
username = request.cookies.get('username', '')
return username
寫入cookie:
from flask import Flask
from flask import request
from flask import make_response
app = Flask(__name__)
@app.route('/')
def index():
resp = make_response('')
resp.set_cookie('username', 'loo')
return resp
@app.route('/get_cookie')
def get_cookie():
username = request.cookies.get('username', '')
return username
有響應物件才能寫入cookie,呼叫make_response()
函式可以顯示獲取一個響應物件。
重定向和報錯
redirect()
函式重定向到另一個URL節點。abort()
函式異常終止處理、返回客戶端一個錯誤碼。
from flask import abort
from flask import redirect
from flask import url_for
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return redirect(url_for('login')) #預設錯誤碼302
@app.route('/login')
def login():
abort(401)
print "never executed"
使用errorhandler()
裝飾器定製錯誤頁面的樣式:
from flask import abort
from flask import redirect
from flask import url_for
from flask import Flask
from flask import render_template
app = Flask(__name__)
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login')
def login():
abort(401)
print "never executed"
@app.errorhandler(401)
def not_authorized(error):
return render_template('not_authorized.html'), 401
<!-- not_authorized.html -->
<!doctype html>
<title>Not authorized</title>
<body>
<p>You are not authorized</p>
</body>
關於響應
URL繫結的檢視函式,自動將return的值轉換為響應物件,轉換規則如下:
- 如果是響應物件,直接返回。
- 如果是字串,將字串作為響應物件的body,狀態碼為200,MIME型別為
text/html
。 - 如果return的是元組,必須按照
(response, status, headers)
或(response, headers)
的格式,headers
可以是列表,或者頭資訊的字典。 - 如果上述規則均未命中,認為返回值是一個合法的WSGI應用,並將其轉換為響應物件。
例如如果在404頁面響應中新增頭資訊,可以通過make_reponse()
函式顯示獲得響應物件後新增:
from flask import Flask
from flask import render_template
from flask import make_response
app = Flask(__name__)
@app.errorhandler(404)
def page_not_found(error):
resp = make_response(render_template('page_not_found.html'), 404)
resp.headers['key'] = 'value'
return resp
<!-- page_not_found.html -->
<!doctype html>
<title>page not found</title>
<body>
<p>Page not found</p>
</body>
Session
session
物件可以用於多次請求間儲存資料。Flask內建的sesison基於cookie實現,並對cookie做了簽名加密處理,使用者能夠看到cookie的值但是無法修改,除非他知道金鑰的值。Flask內建的session比cookie更安全。
from flask import Flask
from flask import request
from flask import session
from flask import redirect
from flask import url_for
from flask import escape
import os
app = Flask(__name__)
secret_key = os.urandom(16)
print "secret_key:", secret_key
app.secret_key = secret_key
@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username']) #escape可以轉義特殊字元,防止CSS攻擊
return 'You are not logged in'
@app.route('/login', methods = ['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<label>username</label>
<input type="text" name="username" />
<input type="submit" value="login" />
</form>
'''
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))
預設情況下,Flask使用了這種基於客戶端cookie的session技術.如果某些session沒有在請求間傳遞,檢查瀏覽器是否開啟了cookie支援,或者cookie的大小是否超過了瀏覽器的最大限制。
Flash訊息
Flash訊息技術,在返回響應前儲存一個訊息,在下次請求(且僅下次請求)時訪問到這個訊息,該技術通常配合一個浮層模板顯示提示訊息。使用flash()
函式釋出一個flash訊息,使用get_flashed_messages()
獲取上一次請求中釋出的訊息(在Jinja模板中也可以使用此函式)。
日誌
Flask內建了一個logger
支援記錄日誌,例如:
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
具體用法參考官方文件。
使用Flask擴充套件
Flask擴充套件為Flask應用提供了豐富的功能支援,例如傳送郵件、連結資料庫、開發ReST API應用等。Flask擴充套件的名字通常是Flask-Foo
或Foo-Flask
,Flask開發團隊會review這些擴充套件,並批准通過與未來發布的Flask相容的擴充套件(approved extension)。