使用 Flask 製作網站
關於 Flask
Flask 一個是使用 Python 所撰寫的網站開發框架,跟其他的 Python 網站開發框架相比算是個輕量級的框架,沒有強迫開發者只能用某種資料庫或樣板引擎,有經驗的開發者可依個人能力及喜好選擇合適的套件來組裝、使用。Flask 本體雖然功能不算多,但週邊的擴充套件不少,透過這些週邊套件開發者們可以更快的建立功能完整的網站。
要用 Flask 做網站,第一件事就是要先把 Flask 給安裝好。雖然 Flask 是一個網站框架,但它其實也是個套件,所以跟其他套件一樣只要一行 pip install flask
就能安裝完成。
不過為了不把環境搞得一團亂,在這個章節我會使用在「環境安裝」章節介紹過的 Poetry 來建立虛擬環境,如果忘了怎麼使用的可再去復習一下。覺得 Poetry 太複雜的話,venv 也是個不錯的選擇。是說如果不想這麼麻煩,只是想試一下 Flask 手感的話,直接用 pip install flask
也沒什麼問題。
建立虛擬環境
首先,我先建立一個全新的目錄 flask-demo
,待會虛擬環境以及專案的程式碼都會放在這裡:
$ mkdir flask-demo
$ cd flask-demo
$ poetry init -n
這裡我請 Poetry 幫我建立一個空的 pyproject.toml
檔案,接著我們就可以用 Poetry 啟動虛擬環境:
$ poetry shell
Creating virtualenv flask-demo in /tmp/flask-demo/.venv
Spawning shell within /tmp/flask-demo/.venv
$ (flask-demo-py3.12) %
因為使用了虛擬環境,所以就算之前曾經在電腦上安裝過其他版本的 Flask 也都沒關係,反正在這個全新的虛擬環境裡都一切重新來過。因為接下來都是在虛擬環境裡作業,所以 (flask-demo-py3.12)
的提示字元我就會適時的省略。搞定虛擬環境之後,接著就是要請 Poetry 來安裝 Flask:
$ poetry add flask
Using version ^3.0.3 for flask
Updating dependencies
Resolving dependencies... (1.7s)
Package operations: 7 installs, 0 updates, 0 removals
- Installing markupsafe (2.1.5)
- Installing blinker (1.8.2)
- Installing click (8.1.7)
- Installing itsdangerous (2.2.0)
- Installing jinja2 (3.1.4)
- Installing werkzeug (3.0.3)
- Installing flask (3.0.3)
Writing lock file
可以看的出來除了 Flask 之外,還多裝了好幾個相依套件。安裝完了 Flask 之後,然後 Flask 專案會長什麼樣子?其他的網站開發框架可能有些指令可以建立全新的專案,在 Flask 有指令可以做這件事嗎?沒有,但其實也不太需要,因為隨便寫一個 .py
檔案可以開始用 Flask 了。
Hello, Flask!
首先,我先建立一個名為 hellokitty.py
檔案,然後在裡面寫入以下程式碼:
from flask import Flask
cat = Flask(__name__)
@cat.route("/")
def home():
return "<h1>Hello Flask!</h1>"
說明:
- 從
flask
套件中匯入Flask
類別,然後使用它建立一個物件叫做cat
。 - 在
home()
函數前面掛上一個函數的裝飾器,.route("/")
方法裡的"/"
代表根目錄,也就是網站的首頁。當有人訪問網站的根目錄時,就會執行這個函數,而執行這個函數會回傳一串文字。
這個函數裝飾器我們在前面的函數章節有介紹過,這個可以額外接參數的裝飾器的細節其實滿複雜的,但大概就是會幫我們加上路徑比對的功能,我們就不用像前一章在手刻 WSGI 一樣從環境變數判斷路徑。如果有需要,.route()
方 法還可以掛好幾次,例如:
@cat.route("/")
@cat.route("/hi")
def home():
return "<h1>Hello Flask!</h1>"
這樣表示 /
跟 /hi
這兩個網址都會執行 home()
函數。
還記得在上一章講到 WSGI 的時候,最後要回傳一個可迭代物件,裡面的元素還必須是位元組,過程中還要設定 HTTP 狀態碼以及標頭,實在有點麻煩。Flask 幫我們搞定了這些雜事,像上面這樣簡單幾行就能搞定,我們只要把心力放在想要給使用者看到什麼內容就好。
另外,這裡大家會看到我檔名用 hellokitty.py
,然後變數用 cat
,好像都跟官網文件不太一樣,看起來好像在亂寫?的確是有點亂寫沒錯,這除了好玩之外,也是為了讓大家知道這些檔名以及變數名稱都是可以自己決定的,並沒有規定一定要叫什麼名字。
就這樣短短幾行,我們就完成了一個超陽春的 Flask 程式。接著我們可以用 Flask 提供的指令來啟動網站:
$ flask --app hellokitty run
* Serving Flask app 'hellokitty'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
因為一開始我就手賤故意把檔案取名為 hellokitty.py
,所以這裡我需要額外加上 --app
選項來指定要執行的檔案。如果你的檔案跟官網文件一樣叫做 app.py
的話,那就不用這麼麻煩,直接執行 flask run
就行了。從執行之後的訊息可以看的出來 Flask 在本機的 Port 5000
上運行,所以只要打開瀏覽器連上 http://127.0.0.1:5000
就可以看到我們的網站了。
如果你仔細看啟動時候的訊息,還會發現有個貼心小提醒(其實是警告),說我們伺服器這個是開發用的,不適合用在正式環境,請改用比較厲害一點的 WSGI 伺服器來運行。別擔心,在最後我們也會介紹使用厲害一點的伺服器來執行我們的網站。
頁面不會更新?
如果你在啟動伺服器之後再去修改程式碼的話,你會發現頁面的內容再怎麼重新整理都不會跟著更新,你得先按 Ctrl + C
把原本的伺服器停掉,重新啟動,才會看到更新之後的效果,這感覺有點沒效率。這是因為 Flask 預設不會監聽檔案的內容異動,我們可以在啟動的時候額外加上 --reload
選項,就可以有這個效果了:
$ flask --app hellokitty run --reload
* Serving Flask app 'hellokitty'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
你可以試著修改一下程式碼,然後再看看終端機的反應:
* Detected change in '/tmp/flask-demo/hellokitty.py', reloading
* Restarting with stat
一改完就馬上被 Flask 發現檔案有變動,然後就自動幫我們重新啟動,這樣就不用每次修改完都要重新啟動伺服器了。
偵錯模式
Flask 有提供一個偵錯小工具,可以讓我們在開發的過程中就能透過網頁介面來抓問題。要開始偵錯工具的話,在啟動的時候加上 --debug
選項:
$ flask --app hellokitty run --debug
* Serving Flask app 'hellokitty'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 279-046-439
可以在上面的啟動訊息裡看到 Debug mode: on
,這表示已經開啟偵錯模式,同時在最後一行還有提供 PIN 碼 279-046-439
,這組 PIN 碼在不同的專案或不同的電腦上的數字應該不會一樣,所以如果各位跟著我做到這裡,號碼應該不會跟我的一樣。如果想試玩看看,你可以故意寫錯一行程式碼,在瀏覽器上就會看到錯誤訊息的畫面,在錯誤訊息的右手邊有個小小的終端機按鈕,點下去就會跳出這個提示視窗:
貼上剛才產生的 PIN 碼,就能直接在網頁上抓問題。或是如果只是想看看偵錯模式長什麼樣子,也直接可以連上 127.0.0.1:5000/console
,貼上 PIN 碼,就可以進入偵錯模式了:
同時,如果開啟偵錯模式,就會自動帶有頁面重新載入的效果,所以可以不用另外加 --reload
選項。
然後,不要在正式環境使開啟偵錯模式!不要在正式環境使開啟偵錯模式!不要在正式環境使開啟偵錯模式!重要的事情要講三次。在自己本機或開發環境上測試還行,但不要在正式環境使用,不要以為有設定 PIN 碼鎖起來大家進不來,事實上就這幾個號碼的排列組合並不是多難試出來的。
指定 IP 位址和 Port 號
剛才介紹的是使用 Flask 提供的指令配合一些選項來啟動伺服器,如果想要改變伺服器啟動時候的 IP 位置或是改變 Port 號,只要在後面接上 --host
及 --port
選項就可以了,但我個人更偏好把這些選項直接寫在程式碼裡,這樣就不用每次在輸入指令的時候都得加上好幾個選項。我把原來的程式碼改成這樣:
from flask import Flask
cat = Flask(__name__)
@cat.route("/")
def home():
return "<h1>Hello Flask!!</h1>"
if __name__ == "__main__":
cat.run(port=9527, debug=True)
我把啟動的程式碼寫在 __name__ == "__main__"
裡面,所以只有在直接執行這個檔案的時候才會啟動,也就是說,要啟動的時候就不是透過 flask run
.而是跟一般的執行 Python 程式一樣:
$ python hellokitty.py
* Serving Flask app 'hellokitty'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:9527
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 279-046-439
可以看到 Port 號變了,而且偵錯模式也打開了。
新增頁面:關於我們
是說,做網站總不能只有一個首頁吧,好歹也要再來個關於我們之類的頁面。在上一章講到 WSGI 的時候有提到,如果要加入其他頁面可以透過判斷環境變數的路徑來顯示不同的內容,但在 Flask 中,我們可以透過函數裝飾器來做到這件事。假設我想新增一個路徑是 /about
的頁面,並且希望在畫面上顯示「關於我們」字樣:
from flask import Flask
cat = Flask(__name__)
@cat.route("/")
def home():
return "<h1>Hello Flask!</h1>"
@cat.route("/about")
def about_page():
return "<h1>關於我們</h1>"
if __name__ == "__main__":
cat.run(port=9527, debug=True)
這寫起來應該明顯比用傳統的 WSGI 要簡單很多(應該有吧?),不用在函數裡面判斷路徑,不用設定狀態跟表頭,也不用回傳什麼位元組串列,好寫多了。
但通常我們的頁面也不會只有一個簡單的 <h1>
標籤,如果說能把這些 HTML 寫在另外的檔案裡,應該會更方便好用...
使用樣板
好,那我們就來試試看在這裡隨便新增一個 pages
的目錄,然後在裡面放一個 about.html
檔案,接著把剛才寫在函數裡的 <h1>
標籤寫到這個檔案裡。現在看起來的目錄結構應該是這樣:
flask-demo
├── hellokitty.py
├── pages
│ └── about.html <- 檔案在這裡
├── poetry.lock
└── pyproject.toml
看到這裡,也許你會猜這樣是不是就能直接連上這個頁面了?就是照著目錄跟檔案的名字連到 http://127.0.0.1:9527/pages/about.html
就行了?很可惜 Flask 不是這樣運作的。在 Flask 的應用程式,除了稍後會介紹的靜態檔案之外,所有頁面或連結的網址都是透過 Flask 來決定的,就是我們剛剛看到掛在 about_page()
前面的函數裝飾器,並不是放在某個目錄就 會被看到。不過有了這個檔案之後,我們就不用直接在主程式裡寫 HTML 了,而是透過 Flask 提供的函數讀取 HTML 檔案的內容,然後回傳給瀏覽器。
Flask 提供一個 render_template()
函數方便我們做這件事:
from flask import Flask, render_template
# ...略...
@cat.route("/about")
def about_page():
return render_template("pages/about.html")
透過這個函數檔案案讀進來,讓我們回到瀏覽器看看結果,結果發現...
咦?不是這樣寫嗎?為什麼找不到頁面?這是因為 render_template()
函數預設會到專案的 templates
目錄去找相對應的檔案,所以我在專案裡新增 templates
目錄,然後把剛才的整個 pages
目錄搬進去,現在的目錄結構如下:
flask-demo
├── hellokitty.py
├── poetry.lock
├── pyproject.toml
└── templates
└── pages
└── about.html
回到瀏覽器重新整理頁面,你應該就能看到「關於我們」的字樣了。
templates
目錄是 Flask 預設用來放樣版檔案的目錄,如果你看這個目錄不順眼想 幫它換個名字,可以在一開始的 Flask 類別的時候加上額外的 template_folder
參數來指定:
cat = Flask(__name__, template_folder="html")
這樣就可以把預設的樣板讀取目錄改成 html
了。
靜態檔案
剛才有提到,在我們這個小小專案裡,所有的連結應該都會由 Flask 來決定能不能連的上,如果有設定路徑就連的上,沒有就會找不到頁面。如果我想要放一張貓咪的可愛照片,檔名叫 cute_cat.jpg
,這樣是不是也得寫一個專門的函數或為這張圖片設定路徑,就只是為了顯示這張圖片?這樣也太麻煩了。
跟剛才的 templates
目錄一樣,Flask 有另外一個專門可以用來放檔案的目錄,預設的目錄名稱是 static
,所有放在這個目錄下的檔案都可以直接被瀏覽器讀取,不需要經過 Flask 的路徑比對。所以我們可以把 cute_cat.jpg
放在這裡,現在的目錄結構如下:
flask-demo
├── hellokitty.py
├── poetry.lock
├── pyproject.toml
├── static
│ └── images
│ └── cute_cat.jpg
└── templates
└── pages
└── about.htmll
這樣就能在 HTML 裡直接拿來用了:
<h1>關於我們</h1>
<h3>有看過這麼可愛的小貓嗎?沒有的話現在就給你看看!</h3>
<img src="/static/images/cute_cat.jpg" />
這樣就能在網頁上看到一張可愛的小貓了:
同樣的,如果你不喜歡 static
這個目錄名稱,也可以透過 Flask 類別的 static_folder
參數來指定:
cat = Flask(__name__, static_folder="public")
這樣就可以把預設的靜態檔案讀取目錄改成 public
了。
是說,如果這時候你打開瀏覽器檢視網頁原始碼,應該會發現頁面上的 HTML 標籤不太完整,那是因為我這裡的確只寫了部份的標籤而已,一個完整的 HTML 頁面應該會有像是 <head>
和 <body>
之類的標籤才是。為了寫出完整的 HTML 標籤,我們可以直接寫在 about.html
裡,但這樣其他每個頁面也要再寫一次,感覺有點笨,而且大部份的頁面的內容可能只有部份不同,頁首、頁尾或側邊欄的設計都是一樣的。
剛才在安裝 Flask 的時候,不知道有沒發現有跟著安裝了一個名為 jinja2
的套件,這是一個 Python 的樣版引擎,Flask 預設就是用它來處理樣版的。
Jinja 有一個比較聰明、省力又環保的做法叫做「樣版繼承(Template Inheritance)」。簡單來說,就是可以先來做一個公用的樣版,然後在裡面先挖好幾個洞,其他頁面在使用這個樣版的時候只要負責填空就行了。
公用樣版
這個公用樣版要叫什麼名字都可以,我就把它取名為 layout.html
,並且放在 templates/
目錄底下,檔案內容如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block page_title %}Hello Flask{% endblock %}</title>
</head>
<body>
{% block page_content %}{% endblock %}
</body>
</html>
上面看到的 {% .. %}
是樣版引擎 Jinja 的語法,這是用來告訴 Jinja 這不是一般的 HTML 內容,可能有一些特別的指令在裡面。{% block NAME %}{% endblock %}
的寫法你可以想像有點像是在 HTML 裡挖了一個洞,NAME
就是洞的名字,其他頁面有需要的話可以在這個洞裡填入自己頁面的專屬內容。
這裡我用 {% block NAME %}{% endblock %}
挖了兩個區塊,一個在 <title>
標籤裡,一個在 <body>
標籤裡。這兩個區塊的名字分別叫做 page_title
和 page_content
,名字你可以自己決定,沒有特別規定。
比較不一樣的是我在 {% block page_title %} {% endblock %}
區塊裡多放了 Hello Flask
字樣,表示如果這個區塊到時候沒人填的話,這就是它預設的輸出結果,但如果有人填,就會以填入的內容為主。
我們來試試怎麼用這東西,回到 about.html
頁面,我把原本的 HTML 改成這樣:
{% extends "layout.html" %}
{% block page_content %}
<h1>關於我們</h1>
<h3>有看過這麼可愛的小貓嗎?沒有的話現在就給你看看!</h3>
<img src="/static/images/cute_cat.jpg" />
{% endblock %}
{% extends %}
語法表示要使用 layout.html
樣版,接著自己把 page_content
區塊裡的東西填一填就行了。這樣一來,我們就可以把共用的部份寫在 layout.html
裡,每個頁面只要負責填入自己的內容就行了。
Flask 的基本介紹差不多就到這裡,最後我們就來做個簡單的樂透號碼產生器來結束這個章節。
《練習》樂透號碼產生器
想中樂透嗎?別想太多,我沒這種本事讓你中大獎,有的話我也不用做教學或寫書了。雖然我沒辦法讓你中大獎,不過要做個簡單的樂透號碼產生器倒不會太難,接著就來用 Flask 做一個過一下乾癮。
首先,我想讓這個功能的路徑是 /lottery
,所以第一件事就是在原本的程式加上一個新的函數,並且掛上路徑的函數裝飾器:
@cat.route("/lottery")
def lottery():
# 這裡等等要來抽號碼
return render_template("lottery.html")
lottery()
函數很簡單,中間抽號碼的邏輯待會再寫,這裡就只是先把 lottery.html
檔案讀出來而已,所以別忘了也在 templates
目錄下新增 lottery.html
檔案,這個檔案就是我們待會要顯示樂透號碼的地方。
問題是,號碼要怎麼產生?如果你會寫一點 JavaScript 程式,大概會猜這個只要在頁面上寫一些簡單的 JavaScript 就能隨機產生了。是沒錯,但前端其實是沒有秘密的,寫在前端的話,產生號碼的邏輯只要檢視原始碼就會被看光光,完全沒有做手腳的機會(咦?)。如果說我們想要控制號碼產生的邏輯,例如讓某些號碼開出來的機會比較高,這種小動作寫在後端來寫會比較安全、合理一些,這裡所謂的後端,就是指我剛剛寫的 lottery()
函數。
接著我來寫一個可以簡單產生 N 個 1 到 49 之間的號碼的函數 number_generator()
,並且在 lottery()
裡呼叫它來產生 6 個號碼:
from random import sample
@cat.route("/lottery")
def lottery():
lottery_numbers = number_generator(6)
return render_template("lottery.html")
def number_generator(n):
numbers = range(1, 50)
return sorted(sample(numbers, n))
到目前應該還算單純,不過產生了這個號碼,要怎麼顯示在 HTML 頁面?並不是在 Flask 的函數裡的變數就可以在 HTML 樣版直接取用喔,我們得主動講清楚哪些東西可以給 HTML 使用:
@cat.route("/lottery")
def lottery():
lottery_numbers = number_generator(6)
return render_template("lottery.html", lottery_numbers=lottery_numbers)
render_template()
函數可以傳入 Key / Value 的組合,這到時候會被我們之前學過的 **
給解析成字典,在 HTML 就可以取用。在 HTML 如果要取用傳過來的值的話,要改成這樣寫:
<h1>樂透號碼</h1>
{{ lottery_numbers }}
使用兩層大括號 {{ }}
,裡面可以放剛才透過 render_template()
函數傳過來的字典,這樣就能在 HTML 裡看到這些號碼了。
這個 {{ }}
語法跟剛才介紹的 {% %}
一樣,也是 Flask 預設的樣版引擎 Jinja 提供的語法,並不是 HTML 內建的,這個語法可以讓我們在 HTML 裡安插 Flask 程式裡的值。
剛才產生的樂透號碼本身是串列,這樣直接印出來雖然也是看的懂,但如果想要把號碼一個一個顯示出來或是加些美化設計,而不是整串印出來,我們可以再次借用樣版引擎提供的功能,寫個 for
迴圈,把號碼逐個印出來:
<h1>樂透號碼</h1>
<h3>本期號碼</h3>
<ul>
{% for number in lottery_numbers %}
<li>{{ number }}</li>
{% endfor %}
</ul>
我在 <ul>
標籤裡放了 for
迴圈,然後在迴圈裡把號碼一個一個印出來。在 Jinja 裡,{% %}
是用來放程式邏輯的,像是 if...else...
或是 for
迴圈之類的,而 {{ }}
是用來顯示東西的。
不過,大家在這裡看到 for...in...
的寫法,這並不是 Python 的語法,這是樣版引擎提供的,只是它們長的有點像而已。透過樣版引擎的處理,使用者最後看到的是處理過後的結果,而不是看到 {{ }}
的字樣在畫面上(除非寫錯了)。
事實上,Jinja 背後的運作原理是把這些 HTML 跟變數或邏輯編譯成 Python 的程式碼,所以效能就跟 Python 本身差不多快(或差不多慢?),但這樣的寫法,讓我們可以把程式的邏輯跟顯示的邏輯分開,讓程式碼更容易維護。
假設你知道 CSS 怎麼寫,可以再加上一點點的 CSS 美化:
看起來就更有樣子了,不過 CSS 不是目前的重點,這裡就先不多做介紹。
補充一個不太重要的冷知識,Jinja 專案之所以叫這名字,是因為 Jinja 是日文「神社(じんじゃ)」的意思,而神社或寺廟的英文 temple 的發音跟「樣版(template)」唸起來有點像,所以這名字就是這樣來的。
控制機率
剛才寫的樂透號碼產生器,其實只是個隨機數字產生器,如果想要控制機率,例如說,你想要讓某些號碼出現的機率比較高,或是直接排除某些號碼,剛剛的抽號碼程式可以這樣改一下:
def number_generator(n):
all_numbers = set(range(1, 50))
excluded_numbers = set([9, 5, 2, 7])
numbers = list(all_numbers - excluded_numbers)
return sorted(sample(numbers, n))
我利用了我們在「元組與集合」章節學過的集合,利用差集運算把要排除的號碼從所有號碼裡扣掉,就可以達到排除某些號碼的效果。這樣一來 9、5、2、7 這幾個數字不管你抽幾次都不會出現。或是想要讓偶數號碼出現的機率比較高,可以這樣改:
from random import random, choice
def number_generator(n):
odd_nums = list(range(1, 50, 2))
even_nums = list(range(2, 50, 2))
all_nums = []
for _ in range(n):
if random() > 0.7: # 等於約 30% 的機率抽到奇數
all_nums.append(choice(odd_nums))
odd_nums.remove(all_nums[-1])
else:
all_nums.append(choice(even_nums))
even_nums.remove(all_nums[-1])
return sorted(all_nums)
這樣奇數號碼出現的機率就會比較低,只要比例不要調整的太明顯,玩家的感受就不會有太明顯有差異。我不是在教各位做什麼奇怪的事,只是想讓大家知道這些東西都可以用程式調整,正因為都是在後端做計算,所以打開瀏覽器的原始碼也看不出來有什麼問題,除非取樣數夠多,不然就只能感覺很玄,好像就是有些號碼就是特別容易出。
如果需要的話,上面這些檔案或程式碼都可以在我的 GitHub 上找到。再次提醒,我會故意用一些不太正統的方式以及命名,目的是為了讓大家知道 Flask 是怎麼運作的,以及那些目錄、檔案或變數名稱都可以自己決定,只要你自己知道在做什麼就行了。實務上還是乖一點不要像我這樣亂搞,不然到時候要交接或維護的時候,會讓看的人一頭霧水。
GitHub Repo https://github.com/kaochenlong/hello-flask