文章目錄
- 2.1 Flask介紹及其安裝
- 2.2 Virtualenv
- 3.1 一個最小的應用
- 3.2 外部課件服務器
- 3.3 調試模式
- 4.1 路由介紹
- 4.2 變量規則
- 4.3 構建URL
- 4.4 HTTP 方法
- 4 總結
- 5.1 靜態文件
- 5.2 渲染模板
- 5.3 練習
- 6
- 6.1 接收請求數據
- 6.2 請求對象
- 6.3 文件上傳
- 6.4 Cookies
- 6 總結
- 7
- 7.1 重定向和錯誤
- 7.2 關于響應
- 7.3 會話
- 7.4 消息閃爍
- 7.5 日志和整合 WSGI 中間件
- 7 總結
- 7 練習
- 參考 https://www.shiyanlou.com/courses/29/learning/?id=263
2.1 Flask介紹及其安裝
Flask 是一個輕量級的 Web 應用框架, 使用 Python 編寫。
基于 WerkzeugWSGI 工具箱和 Jinja2 模板引擎。使用 BSD 授權。
Flask 也被稱為 microframework ,因為它使用簡單的核心,用 extension 增加其它功能。
Flask 沒有默認使用的數據庫、窗體驗證工具。
然而,Flask 保留了擴增的彈性,可以用 Flask-extension 加入這些功能:ORM、窗體驗證工具、文件上傳、各種開放式身份驗證技術。
當安裝 Flask 時,以下組件也會自動安裝:
- Werkzeug: WSGI(web 服務器網關接口)工具,是介于應用和服務器之間標準的接口工具。
- Jinja: web 前端頁面中使用的模板語言。
- MarkupSafe: 與 Jinja 配合使用,當表單頁面跳轉時會進行驗證從而避免遭遇不信任的輸入帶來的攻擊。
- ItsDangerous: 安全地注入數據以確保數據的完整性,通常用于保護 Flask 的 session cookie。
- Click: 一個解析命令行的應用,它支持在 Flask 中自定義管理命令。
2.2 Virtualenv
也許 Virtualenv 是你在開發中最愿意使用的,如果你在生產機器上有 shell 權限的時候,也會愿意用上 Virtualenv。
virtualenv 解決了什么問題?如果你像我一樣喜歡 Python 的話,有很多機會在基于 Flask 的 web 應用外的其它項目上使用 Python。 然而項目越多,越有可能在不同版本的 python,或者至少在不同 python 庫的版本上工作。我們需要面對這樣的事實:庫破壞向后兼容性的情況相當常見,而且零依賴的正式應用也不大可能存在。如此,當你的項目中的兩個或更多出現依賴性沖突,你會怎么做?
Virtualenv 的出現解決這一切!Virtualenv 能夠允許多個不同版本的 Python 安裝,每一個服務于各自的項目。它實際上并沒有安裝獨立的 Python 副本,只是提供了一種方式使得環境保持獨立。讓我們見識下 virtualenv 怎么工作的。
如果你在 Mac OS X 或 Windows 下,下面兩條命令可能會適用:
sudo easy_install virtualenv# 或者是:
sudo pip3 install virtualenv
上述的命令會在你的系統中安裝 virtualenv。它甚至可能會出現在包管理器中。
如果是在 Windows 下并且沒有安裝 easy_install 命令,你首先必須安裝 easy_install 。要想獲取更多的安裝信息,請查看 Windows 下的 pip 和 distribute 。一旦安裝好 easy_install ,運行上述的命令,但是要去掉 sudo 前綴。
如果你使用 Ubuntu ,請嘗試:
sudo apt-get install python-virtualenv
一旦成功安裝 virtualenv,運行 shell 創建自己的環境。通常會創建一個項目文件夾 myproject,其下創建 venv 文件夾,該文件夾就是一個虛擬的 Python 環境,同樣的,我們可以使用 -p 參數來改變 python 的版本,默認情況下,virtualenv 會優先選取系統默認的 python 環境。本實驗中我們使用 python3 。
$ cd /home/shiyanlou/Code
$ mkdir myproject && cd myproject
$ virtualenv -p /usr/bin/python3 venv
New python executable in venv/bin/python
Installing distribute............done.
現在,只要你想要在某個項目上(在本課程中,我們建議你在新建的 myproject 目錄下,這樣 python 運行環境之間不存在沖突)工作,只要激活相應的環境。在 Mac OS X 和 Linux 下,執行如下命令:
. venv/bin/activate
如果你是 Windows 用戶,下面的命令行是為你而準備:
venv\scripts\activate
無論哪種方式,現在都能夠使用你的 virtualenv (注意你的 shell 提示符顯示的是活動的環境)。
接下來只需要輸入以下的命令在 virtualenv 中安裝 flask,在本課程中統一使用 Flask 1.0.2 版本:
pip3 install flask==1.0.2
幾秒后,一切就為你準備就緒。
檢查是否成功安裝 Flask:
$ python3
>>> import flask
>>>
2.2.2 全局安裝
以下命令安裝 Flask 也是可行的,這樣就是把 Flask 全局安裝在操作環境中。只需要以 root 權限運行 pip:
sudo apt-get update
sudo pip3 install flask==1.0.2
(在 Windows 系統上,在管理員權限的命令提示符中運行這條命令,不需要 sudo。)
2.2.3 體驗最新的 Flask (Living on the Edge)
如果你想要使用最新版的 Flask ,可以直接在終端執行如下命令:
sudo pip3 install -U https://github.com/pallets/flask/archive/master.tar.gz
3.1 一個最小的應用
一個最小的應用看起來像這樣,在 /home/shiyanlou/Code 目錄下新建 hello.py 文件,并向其中寫入如下代碼:
from flask import Flask
app = Flask(__name__)@app.route('/')
def hello_world():return 'Hello, World!'
那么這段代碼做了什么?
首先我們導入了類 Flask。這個類的實例化將會是我們的 WSGI 應用。
接著,我們創建一個該類的實例。第一個參數是應用模塊或包的名稱,這樣 Flask 才會知道去哪里尋找模板、靜態文件等等。如果你使用的是單一的模塊(就如本例),第一個參數應該使用 name。
我們使用裝飾器route()告訴 Flask 哪個URL才能觸發我們的函數。
定義一個函數,該函數名也是用來給特定函數生成 URLs,并且返回我們想要顯示在用戶瀏覽器上的信息。
使用 Python 解釋器運行這個文件,注意這個文件不能取名為flask.py,因為這會與 Flask 本身沖突。
運行這個應用既可以使用 flask 命令行也可以使用 Python 的 -m 調用 flask,在運行之前你需要設置 FLASK_APP 的環境變量來告訴終端需要運行哪個應用,在終端執行如下命令: (大家請注意,記得要切回 Code 目錄,才能調用,在后續課程實例代碼中,將不會再提醒)
$ cd /home/shiyanlou/Code
$ export FLASK_APP=hello.py
$ flask run* Serving Flask app "hello.py"* Environment: productionWARNING: Do not use the development server in a production environment.Use a production WSGI server instead.* Debug mode: off* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
也可以使用如下命令啟動應用:
$ export FLASK_APP=hello.py$ python3 -m flask run* Serving Flask app "hello.py"* Environment: productionWARNING: Do not use the development server in a production environment.Use a production WSGI server instead.* Debug mode: off* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
以上的命令啟動了一個非常簡單的 flask 內置服務器,用于測試已經足夠了但可能你并不想用于生產環境。更多配置可以參考開發者選項。
現在使用瀏覽器瀏覽http://127.0.0.1:5000/,將會看到頁面上的 Hello, World!。
請按Ctrl+c來停止服務器。
3.2 外部課件服務器
一個最小的應用看起來像這樣,在 /home/shiyanlou/Code 目錄下新建 hello.py 文件,并向其中寫入如下代碼:
from flask import Flask
app = Flask(name)
@app.route(’/’)
def hello_world():
return ‘Hello, World!’
那么這段代碼做了什么?
首先我們導入了類 Flask。這個類的實例化將會是我們的 WSGI 應用。
接著,我們創建一個該類的實例。第一個參數是應用模塊或包的名稱,這樣 Flask 才會知道去哪里尋找模板、靜態文件等等。如果你使用的是單一的模塊(就如本例),第一個參數應該使用 name。
我們使用裝飾器route()告訴 Flask 哪個URL才能觸發我們的函數。
定義一個函數,該函數名也是用來給特定函數生成 URLs,并且返回我們想要顯示在用戶瀏覽器上的信息。
使用 Python 解釋器運行這個文件,注意這個文件不能取名為flask.py,因為這會與 Flask 本身沖突。
運行這個應用既可以使用 flask 命令行也可以使用 Python 的 -m 調用 flask,在運行之前你需要設置 FLASK_APP 的環境變量來告訴終端需要運行哪個應用,在終端執行如下命令: (大家請注意,記得要切回 Code 目錄,才能調用,在后續課程實例代碼中,將不會再提醒)
$ cd /home/shiyanlou/Code
$ export FLASK_APP=hello.py
$ flask run* Serving Flask app "hello.py"* Environment: productionWARNING: Do not use the development server in a production environment.Use a production WSGI server instead.* Debug mode: off* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
也可以使用如下命令啟動應用:
$ export FLASK_APP=hello.py$ python3 -m flask run* Serving Flask app "hello.py"* Environment: productionWARNING: Do not use the development server in a production environment.Use a production WSGI server instead.* Debug mode: off* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
以上的命令啟動了一個非常簡單的 flask 內置服務器,用于測試已經足夠了但可能你并不想用于生產環境。更多配置可以參考開發者選項。
現在使用瀏覽器瀏覽http://127.0.0.1:5000/,將會看到頁面上的 Hello, World!。
請按Ctrl+c來停止服務器。
3.3 調試模式
使用 flask 命令行可以非常方便的啟動一個本地開發服務器,但是每次修改代碼后你都需要手動重啟服務器。通過前面的啟動后輸出顯示可以發現 Environment 為 production,同時調試模式未開啟 Debug mode: off。
這樣做并不好,Flask 能做得更好。如果啟用了調試支持,在代碼修改后服務器能夠自動重載,并且如果發生錯誤,它會提供一個有用的調試器。
為了讓所有的開發者特征可用(包括調試模式),在運行服務器之前可以設置 FLASK_ENV 環境變量為 development:
$ export FLASK_ENV=development
$ export FLASK_DEBUG=1
$ flask run
- Serving Flask app “hello.py” (lazy loading)
- Environment: development
- Debug mode: on
- Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
- Restarting with stat
- Debugger is active!
- Debugger PIN: 219-973-102
上述命令做了以下幾件事:
使調試器(debugger)可用
啟動了代碼改變自動的熱加載
在 flask 應用中開啟了 debug 模式
注意
盡管交互式調試器(debugger)不能在分叉(forking)環境下工作(這使得它幾乎不可能在生產服務器上使用),它依然允許執行任意代碼。這使它成為一個巨大的安全風險,因此它絕對不能用于生產環境。
運行中的調試器的截圖,從截圖可以看出在頁面上有終端可以執行交互式命令:
本節講解了一個 Flask 的小例子,以及在命令行中通過設置環境變量來開啟調試模式。
參考鏈接
WSGI 簡介(https://blog.csdn.net/on_1y/article/details/18803563)
4.1 路由介紹
現代 Web 應用程序使用有意義的 URLs 去幫助用戶。如果一個網站使用有意義的 URL 能夠讓用戶記住并且直接訪問這個頁面,那么用戶會更有可能再一次訪問該網站。
正如上面所說,route 裝飾器是用于把一個函數綁定到一個 URL 上。修改 /home/shiyanlou/Code/hello.py 文件的代碼如下所示:
from flask import Flask
app = Flask(__name__)# 如果訪問 /,返回 Index Page
@app.route('/')
def index():return 'Index Page'# 如果訪問 /hello,返回 Hello, World!
@app.route('/hello')
def hello():return 'Hello, World!'
然后在終端執行如下命令啟動服務:
export FLASK_APP=hello.py
export FLASK_ENV=development
flask run
訪問地址 http://127.0.0.1:5000,瀏覽器頁面會顯示 Index Page;如果訪問地址 http://127.0.0.1:5000/hello,瀏覽器頁面會顯示 Hello, World!。這樣就實現了通過訪問不同的 URL 地址從而響應不同的頁面。
不僅如此!你可以動態地構造 URL 的特定部分,也可以在一個函數上綁定多個不同的規則。
4.2 變量規則
為了給 URL 增加變量的部分,你需要把一些特定的字段標記成<variable_name>。這些特定的字段將作為參數傳入到你的函數中。當然也可以指定一個可選的轉換器通過規則converter:variable_name將變量值轉換為特定的數據類型。 在 /home/shiyanlou/Code/hello.py 文件中添加如下的代碼:
@app.route('/user/<username>')
def show_user_profile(username):# 顯示用戶名return 'User {}'.format(username)@app.route('/post/<int:post_id>')
def show_post(post_id):# 顯示提交整型的用戶"id"的結果,注意"int"是將輸入的字符串形式轉換為整型數據return 'Post {}'.format(post_id)@app.route('/path/<path:subpath>')
def show_subpath(subpath):# 顯示 /path/ 之后的路徑名return 'Subpath {}'.format(subpath)
按照前面的方式啟動應用,逐個訪問地址:
當訪問 http://127.0.0.1:5000/user/shiyanlou 時,頁面顯示為 User shiyanlou。
當訪問 http://127.0.0.1:5000/post/3 時,頁面顯示為 Post 3。用戶在瀏覽器地址欄上輸入的都是字符串,但是在傳遞給 show_post 函數處理時已經被轉換為了整型。
當訪問 http://127.0.0.1:5000/path/file/A/a.txt 時,頁面顯示為 Subpath file/A/a.txt。
轉換器的主要類型如下:
類型 含義
string 默認的數據類型,接受沒有任何斜杠“/”的字符串
int 接受整型
float 接受浮點類型
path 和 string 類似,但是接受斜杠“/”
uuid 只接受 uuid 字符串
唯一 URLs / 重定向行為
Flask 的 URL 規則是基于 Werkzeug 的 routing 模塊。該模塊背后的思路是基于 Apache 和早期的 HTTP 服務器定下先例確保優雅和唯一的 URL。
以這兩個規則為例,在 /home/shiyanlou/Code/hello.py 文件中添加如下的代碼:
@app.route('/projects/')
def projects():return 'The project page'@app.route('/about')
def about():return 'The about page'
雖然它們看起來確實相似,但它們結尾斜線的使用在 URL 定義中不同。
第一種情況中,規范的 URL 指向 projects 尾端有一個斜線/。這種感覺很像在文件系統中的文件夾。訪問一個結尾不帶斜線的 URL 會被 Flask 重定向到帶斜線的規范 URL 去。當訪問 http://127.0.0.1:5000/projects/ 時,頁面會顯示 The project page。
然而,第二種情況的 URL 結尾不帶斜線,類似 UNIX-like 系統下的文件的路徑名。此時如果訪問結尾帶斜線的 URL 會產生一個404 “Not Found”錯誤。當訪問 http://127.0.0.1:5000/about 時,頁面會顯示 The about page;但是當訪問 http://127.0.0.1:5000/about/ 時,頁面就會報錯 Not Found。
當用戶訪問頁面忘記結尾斜線時,這個行為允許關聯的 URL 繼續工作,并且與 Apache 和其它的服務器的行為一致,反之則不行,因此在代碼的 URL 設置時斜線只可多寫不可少寫;另外,URL 會保持唯一,有助于避免搜索引擎索引同一個頁面兩次。
4.3 構建URL
去構建一個 URL 來匹配一個特定的函數可以使用 url_for() 方法。它接受函數名作為第一個參數,以及一些關鍵字參數,每一個關鍵字參數對應于 URL 規則的變量部分。未知變量部分被插入到 URL 中作為查詢參數。
為什么你要構建 URLs 而不是在模版中硬編碼呢?這里有幾個理由:
反向構建通常比硬編碼更具備描述性。
它允許你一次性修改 URL,而不是到處找 URL 修改。
構建 URL 能夠顯式地處理特殊字符和Unicode轉義,因此你不必去處理這些。
如果你的應用不在 URL 根目錄下(比如,在 /myapplication 而不在 /),url_for()將會適當地替你處理好。
在 Python shell 交互式命令行下運行如下代碼:
(記得切回myproject對應目錄)
$ python3
>>> from flask import Flask, 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():
... print(url_for('index'))
... print(url_for('login'))
... print(url_for('login', next='/'))
... print(url_for('profile', username='John Doe'))
...
/
/login
/login?next=%2F # 對字符串進行了轉義,未知變量部分被當做查詢參數
/user/John%20Doe
test_request_context() 方法告訴 Flask 表現得像是在處理一個請求,即使我們正在通過 Python shell 交互。大家可以仔細分析一下該函數的打印結果。
4.4 HTTP 方法
HTTP (也就是 Web 應用協議) 有不同的方法來訪問 URLs 。默認情況下,路由只會響應 GET 請求,但是能夠通過給 route() 裝飾器提供 methods 參數來改變。這里是一個例子:
@app.route('/login', methods=['GET', 'POST'])
def login():if request.method == 'POST':do_the_login() # 如果是 POST 方法就執行登錄操作else:show_the_login_form() # 如果是 GET 方法就展示登錄表單
如果使用 GET 方法,HEAD 方法將會自動添加進來。你不必處理它們。也能確保 HEAD 請求會按照 HTTP RFC (文檔在 HTTP 協議里面描述) 要求來處理,因此你完全可以忽略這部分 HTTP 規范。同樣地,自從 Flask 0.6 后,OPTIONS 方法也能自動為你處理。
也許你并不清楚 HTTP 方法是什么?別擔心,這里有一個 HTTP 方法的快速入門以及為什么它們重要:
HTTP方法(通常也稱為“謂詞”)告訴服務器客戶端想要對請求的頁面做什么。
下面這些方法是比較常見的:
GET:瀏覽器通知服務器只獲取頁面上的信息并且發送回來。這可能是最常用的方法。
HEAD:瀏覽器告訴服務器獲取信息,但是只對頭信息感興趣,不需要整個頁面的內容。應用應該處理起來像接收到一個 GET 請求但是不傳遞實際內容。在 Flask 中你完全不需要處理它,底層的 Werkzeug 庫會為你處理的。
POST:瀏覽器通知服務器它要在 URL 上提交一些信息,服務器必須保證數據被存儲且只存儲一次。這是 HTML 表單通常發送數據到服務器的方法。
PUT:同 POST 類似,但是服務器可能觸發了多次存儲過程,多次覆蓋掉舊值。現在你就會問這有什么用,有許多理由需要如此去做。考慮下在傳輸過程中連接丟失:在這種情況下瀏覽器和服務器之間的系統可能安全地第二次接收請求,而不破壞其它東西。該過程操作 POST 方法是不可能實現的,因為它只會被觸發一次。
DELETE:移除給定位置的信息。
OPTIONS:給客戶端提供一個快速的途徑來指出這個 URL 支持哪些 HTTP 方法。從 Flask 0.6 開始,自動實現了該功能。
現在在 HTML4 和 XHTML1 中,表單只能以 GET 和 POST 方法來提交到服務器。在 JavaScript 和以后的 HTML 標準中也能使用其它的方法。同時,HTTP 最近變得十分流行,瀏覽器不再是唯一使用 HTTP 的客戶端。比如許多版本控制系統使用 HTTP。
4 總結
本節講解了 Flask 的路由,我們可以給 URL 添加規則,也可以動態地構建 URL 。
5.1 靜態文件
動態的 web 應用同樣需要靜態文件。CSS 和 JavaScript 文件通常來源于此。理想情況下,你的 web 服務器已經配置好為它們服務,然而在開發過程中 Flask 就能夠做到。只要在你的包中或模塊旁邊創建一個名為static 的文件夾,在應用中使用 /static 即可訪問。
給靜態文件生成 URL ,使用特殊的 static 端點名:
url_for('static', filename='style.css')
這個文件是應該存儲在文件系統上的static/style.css。
5.2 渲染模板
在 Python 中生成 HTML 并不好玩,實際上是相當繁瑣的,因為你必須自行做好 HTML 轉義以保持應用程序的安全。由于這個原因,Flask 自動為你配置好 Jinja2 模板。
你可以使用方法 render_template() 來渲染模板。所有你需要做的就是提供模板的名稱以及你想要作為關鍵字參數傳入模板的變量。
這里有個渲染模板的簡單例子,在 /home/shiyanlou/Code 目錄下新建 hello.py 文件,并向其中添加如下代碼:
from flask import Flask, render_template
app = Flask(__name__)@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None): # 默認 name 為 Nonereturn render_template('hello.html', name=name) # 將 name 參數傳遞到模板變量中
Flask 將會在 templates 文件夾中尋找模板。因此如果你的應用是個模塊,這個文件夾在模塊的旁邊,如果它是一個包,那么這個文件夾在你的包里面:
比如,應用是模塊(本系列實驗的應用結構都是模塊型):
/application.py
/templates/hello.html
比如,應用是包:
/application/__init__.py/templates/hello.html
對于模板,你可以使用 Jinja2 模板的全部能力。詳細信息查看官方的 Jinja2 Template Documentation 。
在 /home/shiyanlou/Code 目錄下新建 templates 文件夾并在其中新建 hello.html 文件:
cd /home/shiyanlou/Code
mkdir templates && cd templates
touch hello.html
然后向 hello.html 模板文件中添加如下代碼:
<!doctype html>
<title>Hello from Flask</title>
{% if name %} <!-- 如果 name 不為空則將 name 渲染出來 --><h1>Hello {{ name }}!</h1>
{% else %} <!-- 如果 name 為空則打印 Hello World! --><h1>Hello World!</h1>
{% endif %}
按照前面的方法運行應用程序,當訪問 http://127.0.0.1:5000/hello/ 時頁面顯示 Hello World!;當訪問 http://127.0.0.1:5000/hello/shiyanlou 時頁面顯示 Hello shiyanlou!。
在模板中你也可以使用request,session和g對象,也能使用函數get_flashed_messages() 。
模板繼承是十分有用的。如果想要知道模板繼承如何工作的話,請閱讀文檔模板繼承。基本的模板繼承使得某些特定元素(如標題、導航和頁腳)在每一頁成為可能。
自動轉義默認是開啟的,因此如name包含 HTML,它將會自動轉義。如果你信任一個變量,并且你知道它是安全的(例如一個模塊把 wiki 標記轉換到 HTML ),你可以用Markup類或|safe過濾器在模板中標記它是安全的。 在 Jinja 2 文檔中,你會見到更多例子。
下面有一個Markup類如何工作的基本介紹,在 Python3 交互式命令行中執行如下命令:
$ python3
>>> from flask import Markup
>>> Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
Markup('<strong>Hello <blink>hacker</blink>!</strong>')
>>> Markup.escape('<blink>hacker</blink>')
Markup('<blink>hacker</blink>')
>>> Markup('<em>Marked up</em> » HTML').striptags()
'Marked up ? HTML'
>>>
注意:在后面的0.5版本以上:
自動轉義不再在所有模板中啟用。模板中下列后綴的文件會觸發自動轉義:.html, .htm, .xml,.xhtml。從字符串加載的模板會禁用自動轉義。
5.3 練習
請創建一個模板和CSS文件,并在模板引入CSS文件,當訪問網站首頁時顯示一個綠色的Hello ShiYanLou字樣。
6
對于 Web 應用來說,對客戶端發送給服務器的數據做出反應至關重要,本實驗將介紹 Flask 是怎樣提供這些信息的。
6.1 接收請求數據
在 Flask 中由全局對象 request 來提供這些信息。如果你有一定的 Python 經驗,你會好奇這個對象怎么可能是全局的,并且 Flask 是怎么還能保證線程安全。答案是上下文作用域。
局部上下文
注意:如果你想要了解上下文作用域是如何工作的以及如何使用它進行測試,就可以讀這一部分,如果暫時不需要的話,可以直接跳過這部分。
Flask 中的某些對象是全局對象,但不是通常的類型。這些對象實際上是給定上下文的局部對象的代理。雖然很拗口,但實際上很容易理解。
想象下線程處理的上下文。一個請求傳入,web 服務器決定產生一個新線程(或者其它東西,底層對象比線程更有能力處理并發系統)。當 Flask 開始它內部請求處理時,它認定當前線程是活動的上下文并綁定當前的應用和 WSGI 環境到那個上下文(線程)。它以一種智能的方法來實現,以致一個應用可以調用另一個應用而不會中斷。
所以這對你意味著什么呢?除非你是在做一些類似單元測試的事情,否則基本上你可以完全忽略這種情況。你會發現依賴于請求對象的代碼會突然中斷,因為沒有請求對象。解決方案就是自己創建一個請求并把它跟上下文綁定。
針對單元測試最早的解決方案是使用test_request_context()上下文管理器。結合with聲明,它將綁定一個測試請求來進行交互。這里是一個例子:
from flask import requestwith app.test_request_context('/hello', method='POST'):# 現在你可以做出請求,比如基本的斷言assert request.path == '/hello'assert request.method == 'POST'
另一個可能性就是傳入整個 WSGI 環境到request_context()方法:
from flask import requestwith app.request_context(environ):assert request.method == 'POST'
參考鏈接:
Flask 上下文理解 (https://jin-yang.github.io/post/flask-context.html)
Flask 的 Context 機制(https://blog.tonyseek.com/post/the-context-mechanism-of-flask/)
6.2 請求對象
首先你需要從 flask 模塊中導入request:
from flask import request
當前請求的方法可以用method屬性來訪問。你可以用form屬性來訪問表單數據 (數據在 POST 或者PUT中傳輸)。這里是上面提及到的兩種屬性的完整的例子:
@app.route('/login', methods=['POST', 'GET'])
def login():error = Noneif 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'# 當請求形式為“GET”或者認證失敗則執行以下代碼return render_template('login.html', error=error)
如果在form屬性中不存在上述鍵值會發生些什么?在這種情況下會觸發一個特別的 KeyError。你可以像捕獲標準的KeyError一樣來捕獲它,如果你不這樣去做,會顯示一個HTTP 400 Bad Request錯誤頁面。所以很多情況下你不需要處理這個問題。
你可以用args屬性來接收在URL ( ?key=value )中提交的參數:
searchword = request.args.get('key', '')
我們推薦使用get來訪問 URL 參數或捕獲KeyError,因為用戶可能會修改 URL,向他們顯示一個400 bad request頁面不是用戶友好的。
6.3 文件上傳
你能夠很容易地用 Flask 處理文件上傳。只要確保在你的 HTML 表單中不要忘記設置屬性enctype=“multipart/form-data”,否則瀏覽器將不會傳送文件。
上傳的文件是存儲在內存或者文件系統上一個臨時位置。你可以通過請求對象中files屬性訪問這些文件。每個上傳的文件都會存儲在這個屬性字典里。它表現得像一個標準的 Python file對象,但是它同樣具有save()方法,該方法允許你存儲文件在服務器的文件系統上。
下面是一個簡單的例子用來演示提交文件到服務器上:
from flask import request@app.route('/upload', methods=['GET', 'POST'])
def upload_file():if request.method == 'POST':f = request.files['the_file']f.save('/var/www/uploads/uploaded_file.txt')...
如果你想要知道在上傳到你的應用之前在客戶端的文件名稱,你可以訪問filename屬性。但請記住永遠不要信任這個值,因為這個值可以偽造。如果你想要使用客戶端的文件名來在服務器上存儲文件,把它傳遞到Werkzeug提供給你的secure_filename()函數:
from flask import request
from werkzeug import secure_filename@app.route('/upload', methods=['GET', 'POST'])
def upload_file():if request.method == 'POST':f = request.files['the_file']f.save('/var/www/uploads/' + secure_filename(f.filename))...
6.4 Cookies
你可以用 cookies 屬性來訪問 Cookies 。你能夠用響應對象的 set_cookie 來設置 cookies。請求對象中的 cookies 屬性是一個客戶端發送所有的 cookies 的字典。
如果你要使用會話(sessions),請不要直接使用 cookies,相反,請用 Flask 中的會話,Flask 已經在cookies 上增加了一些安全細節;關于更多 seesions 和 cookies 的區別與聯系,請參見施楊出品的博客。
讀取 cookies:
from flask import request@app.route('/')
def index():username = request.cookies.get('username')# 注意這里引用cookies字典的鍵值對是使用cookies.get(key)# 而不是cookies[key],這是防止該字典不存在時報錯"keyerror"
存儲 cookies:from flask import make_response@app.route('/')
def index():resp = make_response(render_template(...))resp.set_cookie('username', 'the username')return resp
注意cookies是在響應對象中被設置。由于通常只是從視圖函數返回字符串,Flask 會將其轉換為響應對象。如果你要顯式地這么做,可以使用 make_response() 函數接著修改它。
有時候你可能要在響應對象不存在的地方設置cookie。利用延遲請求回調模式使得這種情況成為可能。
6 總結
本節講解了 flask 的請求,如果想在沒有請求的情況下獲取上下文,可以使用test_request_context()或者request_context(),從request對象的form中可以獲取表單的數據,args中可以獲取 URL 中的參數,files可以獲取上傳的文件,cookies可以操作cookie。
7
7.1 重定向和錯誤
你能夠用redirect()函數重定向用戶到其它地方。能夠用abort()函數提前中斷一個請求并帶有一個錯誤代碼。
下面是一個演示它們如何工作的例子,在 /home/shiyanlou/Code/ 目錄下新建 hello.py 文件并向其中寫入如下代碼:
from flask import Flask
from flask import abort, redirect, url_forapp = Flask(__name__)@app.route('/')
def index():return redirect(url_for('login'))@app.route('/login')
def login():abort(401)this_is_never_executed()
按照之前的方式運行應用,這是一個相當無意義的例子因為用戶會從主頁/重定向到一個不能訪問的頁面/login( 401 意味著禁止訪問),但是它說明了重定向如何工作。
默認情況下,每個錯誤代碼會顯示一個黑白錯誤頁面。比如上面的頁面會顯示 401 Unauthorized。如果你想定制錯誤頁面,可以使用errorhandler()裝飾器,向 /home/shiyanlou/Code/hello.py 文件中添加如下代碼:
from flask import render_template@app.errorhandler(401)
def page_not_found(error):return render_template('page_not_found.html'), 404
注意到 404 是在render_template()調用之后。告訴 Flask 該頁的錯誤代碼應是 404 ,即沒有找到。默認的 200 被假定為:一切正常。
在 /home/shiyanlou/Code 目錄下新建 templates 文件夾,并在其中新建 page_not_found.html 文件。
cd /home/shiyanlou/Code
mkdir templates && cd templates
touch page_not_found.html
向 page_not_found.html 文件中添加如下代碼:
<h1>page not found, this is an error page.</h1>
等代碼重新熱加載后,訪問首頁就可以看到 page not found, this is an error page.。
7.2 關于響應
一個視圖函數的返回值會被自動轉換為一個響應對象。如果返回值是一個字符串,它被轉換成一個響應主體是該字符串,錯誤代碼為 200 OK ,媒體類型為text/html的響應對象。Flask 把返回值轉換成響應對象的邏輯如下:
如果返回的是一個合法的響應對象,它會直接從視圖返回。
如果返回的是一個字符串,響應對象會用字符串數據和默認參數創建。
如果返回的是一個元組而且元組中元素能夠提供額外的信息。這樣的元組必須是(response, status, headers) 形式且至少含有其中的一個元素。status值將會覆蓋狀態代碼,headers可以是一個列表或額外的消息頭值字典。
如果上述條件均不滿足,Flask 會假設返回值是一個合法的 WSGI 應用程序,并轉換為一個請求對象。
如果你想要獲取在視圖中得到的響應對象,你可以用函數make_response()。
想象你有這樣一個視圖:
@app.errorhandler(404)
def not_found(error):return render_template('error.html'), 404
你只需要用make_response()封裝返回表達式,獲取結果對象并修改,然后返回它:@app.errorhandler(404)
def not_found(error):resp = make_response(render_template('error.html'), 404)resp.headers['X-Something'] = 'A value'return resp
7.3 會話
除了請求對象,還有第二個稱為session對象允許你在不同請求間存儲特定用戶的信息。這是在 cookies 的基礎上實現的,并且在 cookies 中使用加密的簽名。這意味著用戶可以查看 cookie 的內容,但是不能修改它,除非知道簽名的密鑰。
要使用會話,你需要設置一個密鑰。這里介紹會話如何工作,在 /home/shiyanlou/Code 目錄下新建 test.py 文件并寫入如下代碼:
from flask import Flask, session, redirect, url_for, escape, requestapp = Flask(__name__)# 設置密鑰,保證會話安全
app.secret_key = '_5#y2L"F4Q8z\n\xec]/'@app.route('/')
def index():if 'username' in session:return 'Logged in as %s' % escape(session['username'])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"><p><input type=text name=username><p><input type=submit value=Login></form>'''@app.route('/logout')
def logout():# 如果用戶名存在,則從會話中移除該用戶名session.pop('username', None)return redirect(url_for('index'))
這里提到的escape()可以在你不使用模板引擎的時候做轉義(如同本例)。其中,login函數中返回的網頁源代碼可以單獨存儲在templates文件夾中作為模板文件html,然后使用return render_template()更方便。
按照前面的方式運行程序:
當訪問首頁 http://127.0.0.1:5000/ 時會顯示 You are not logged in;
當訪問登錄頁面 http://127.0.0.1:5000/login 時會出現一個輸入框,在輸入框中輸入用戶名 shiyanlou,然后點擊 Login 按鈕,這時 URL 會重定向到首頁上,首頁顯示 Logged in as shiyanlou;
最后再訪問登出頁面 http://127.0.0.1:5000/logout,這時從 session 中移除了用戶名,URL 重定向到首頁顯示 You are not logged in;
怎樣產生一個好的密鑰:
隨機的問題在于很難判斷什么是真隨機。一個密鑰應該足夠隨機。你的操作系統可以基于一個密碼隨機生成器來生成漂亮的隨機值,這個值可以用來做密鑰:
$ python3 -c 'import os; print(os.urandom(16))'
b'm \xf8>]?\x86\xcf/y\x0e\xc5\xc7j\xc5/'
把這個值復制粘貼到你的代碼,你就搞定了密鑰。
使用基于 cookie 的會話需注意: Flask 會將你放進會話(session)對象的值序列化到 cookie 。如果你試圖尋找一個跨請求不能存留的值,cookies 確實是啟用的,并且你不會獲得明確的錯誤信息,檢查你頁面請求中 cookie 的大小,并與 web 瀏覽器所支持的大小對比。
7.4 消息閃爍
好的應用和用戶界面全部是關于反饋。如果用戶得不到足夠的反饋,他們可能會變得討厭這個應用。Flask 提供了一個真正的簡單的方式來通過消息閃現系統給用戶反饋。消息閃現系統基本上使得在請求結束時記錄信息并在下一個 (且僅在下一個)請求中訪問。通常結合模板布局來顯示消息。
使用flash()方法來閃現一個消息,使用get_flashed_messages()能夠獲取消息,get_flashed_messages()也能用于模板中。
下面來看一個簡單的例子,在 /home/shiyanlou/Code 目錄下新建 flashTest.py 文件,并向其中寫入如下代碼:
from flask import Flask, flash, redirect, render_template, \request, url_forapp = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'@app.route('/')
def index():return render_template('index.html')@app.route('/login', methods=['GET', 'POST'])
def login():error = Noneif request.method == 'POST':if request.form['username'] != 'admin' or \request.form['password'] != 'secret':error = 'Invalid credentials'else:flash('You were successfully logged in')return redirect(url_for('index'))return render_template('login.html', error=error)
然后在 /home/shiyanlou/Code/templates 目錄下新建 base.html 頁面,其中寫入基本的模板代碼,代碼主要是從后端獲取 flash 消息以及錯誤信息。
<!doctype html>
<title>My Application</title>
{% with messages = get_flashed_messages() %}{% if messages %}<ul class=flashes>{% for message in messages %}<li>{{ message }}</li>{% endfor %}</ul>{% endif %}
{% endwith %}
{% block body %}{% endblock %}
在該目錄下新建 index.html 頁面,這個頁面繼承于 base.html 頁面:{% extends "base.html" %}
{% block body %}<h1>Overview</h1><p>Do you want to <a href="{{ url_for('login') }}">log in?</a>
{% endblock %}
在該目錄下新建 login.html 頁面,這個頁面也繼承于 base.html 頁面:
{% extends "base.html" %}
{% block body %}<h1>Login</h1>{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}<form method=post><dl><dt>Username:<dd><input type=text name=username value="{{request.form.username }}"><dt>Password:<dd><input type=password name=password></dl><p><input type=submit value=Login></form>
{% endblock %}
按照前面的方式運行程序:
當訪問首頁 http://127.0.0.1:5000,會提示 Do you want to log in?,點擊鏈接跳轉到登錄頁面。
在登錄頁面 http://127.0.0.1:5000/login,輸入用戶名和密碼,如果輸入錯誤的信息比如兩個都為 shiyanlou,點擊 Login,就會出現錯誤提示 Error: Invalid credentials。如果用戶名輸入 admin、密碼輸入 secret,點擊 Login,就會跳轉到首頁,同時在首頁會顯示 flash 消息 You were successfully logged in。
7.5 日志和整合 WSGI 中間件
日志
有時候你會遇到一種情況:理論上來說你處理的數據應該是正確的,然而實際上并不正確的狀況。比如你可能有一些客戶端代碼,代碼向服務器發送一個 HTTP 請求但是顯然它是錯誤的。這可能是由于用戶篡改數據,或客戶端代碼失敗。大部分時候針對這一情況返回400 Bad Request就可以了,但是有時候不能這樣做,代碼必須繼續工作。
你也有可能想要記錄一些發生的不正常事情。這時候日志就派上用處。從 Flask 0.3 開始日志記錄是預先配置好的。
這里有一些日志調用的例子:
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
附帶的 logger 是一個標準的日志類 Logger ,因此更多的信息請查閱官方文檔 logging documentation。
整合 WSGI 中間件
如果你想給你的應用添加 WSGI 中間件,你可以封裝內部 WSGI 應用。例如如果你想使用 Werkzeug 包中的某個中間件來應付 lighttpd 中的 bugs,你可以這樣做:
from werkzeug.contrib.fixers import LighttpdCGIRootFix
app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app)
7 總結
本節講了 flask 的重定向、響應、會話和擴展,重定向可以使用redirect(),錯誤處理可以使用errorhander裝飾器,session對象保存會話信息,要使用會話需要設置secret_key,可以用make_response函數生成響應,flash可以用于消息閃現,flask 也能夠整合 WSGI 中間件。
7 練習
請實現一個完整的用戶登錄功能:
當訪問地址 http://127.0.0.1:5000/login ,出現登錄頁面,可以使用用戶名和密碼填寫登錄表單。
如果用戶名和密碼都為shiyanlou,那么就把用戶名數據放到session中,把地址重定向到首頁顯示Hello shiyanlou,同時閃現消息 you were logged in。
如果用戶名和密碼不對,依然把地址重定向到首頁顯示hello world,同時閃現消息 username or password invalid。