本文完成的Flask上傳檔案服務的運作畫面如下,成功接收上傳檔之後,app.py裡的首頁路由將回應相同的上傳表單頁面,在其中插入訊息文字並顯示剛剛上傳的影像。

如果選擇上傳不容許的檔案格式,首頁將顯示「僅允許上傳png, jpg, jpeg和gif影像檔」快閃訊息,檔案不會被保存下來。

包含上傳檔案表單的HTML網頁樣版
底下是本文採用的HTML表單樣版原始碼,相較於上一篇文章的HTML碼,這個版本新增顯示flash message(快閃訊息)以及上傳影像。同樣請將此網頁樣版命名成index.html,存入templates資料夾備用。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<title>上傳影像檔</title>
</head>
<body>
<h1>上傳影像檔</h1>
{% with msg = get_flashed_messages() %}
{% if msg %} <!-- 如果msg變數值不是空的… -->
{% for m in msg %}
<p>{{ m }}</p> <!-- 顯示快閃訊息 -->
{% endfor %}
{% endif %}
{% endwith %}
{% if filename %} <!-- 如果filename變數值不是空的,則顯示影像。 -->
<div>
<img src="{{ url_for('display_image', filename=filename) }}">
</div>
{% endif %}
<form method="POST" enctype="multipart/form-data" action="{{ url_for('upload_file') }}">
<input type="file" name="filename" accept= ".png, .jpg, .jpeg .gif" required>
<input type="submit" value="上傳">
</form>
</body>
</html>
嵌入影像的<img>標籤的src(檔案來源)屬性設成Python Flask的display_image()路由,並傳入filename(檔名)參數。這個路由將傳回我們自訂的 “/img/上傳檔名” 格式字串,例如:”img/cookies.jpg”,避免使用者得知真實的檔案存放路徑,如:”static/uploads/cookies.jpg”。
透過secure_filename()函式轉換不安全的上傳檔名
網路安全的基本守則:不要信任使用者輸入的資料。
有些使用者會在網頁表單輸入程式碼,令網站伺服器在接收表單資料時觸發執行,藉以駭入網站竊取資料;使用者上傳的檔案也可能包含惡意程式碼。例如,Linux系統的使用者家目錄有個設置終端環境的.bashrc檔,會在使用者登入時自動執行,駭客可以透過上傳包含相對路徑的檔名,讓伺服器把檔案寫入使用者的家目錄:
filename = “../../../../../.bashrc”
為了避免這種情況,可透過werkzeug程式庫的secure_filename()函式(直譯為「安全檔名」)檢查並轉換上傳檔案的名稱。

用Python程式確認上傳檔的副檔名
雖然HTML表單已經設定僅限上傳影像檔,但仍可被輕易繞過限制,而且存取網站資源也不一定要透過瀏覽器,所以伺服器端程式有必要檢查表單資料。
假設Python程式的filename變數儲存了上傳檔名”bug.EXE.JPG”,擷取其中的副檔名的方法有很多種。底下敘述採用字串物件的endswith()方法(代表ends with,「以…結尾」之意)檢查變數內容是否以“.jpg”結尾。

endswith()方法的參數可以是字串或者元組型態,像這樣檢查一組副檔名:

此外,os.path模組的splitext()函式,也能擷取檔案路徑中的副檔名,例如:

Flask官方網站的處理上傳檔案說明頁裡的範例程式,採用另一種方式來判斷上傳檔類型,程式片段如下。首先定義一個儲存「允許上傳的副檔名(不含‘.’點)」的ALLOWED_EXTENSIONS列表變數,判斷邏輯寫在allowed_file()函式裡面,它將傳回True(代表允許)或者False(不允許)。
ALLOWED_EXTENSIONS = {‘png’, ‘jpg’, ‘jpeg’, ‘gif’} # 允許上傳的副檔名列表
def allowed_file(filename): # 查看上傳檔的副檔名是否在允許之列
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
allowed_file()函式的判斷邏輯,由and(且)運算子組成兩個條件,若前面的條件「檔名是否有‘.’(點)」不成立,則傳回False,否則接續執行後面的條件敘述。

字串物件的rsplit()方法(原意為right split,由右分割),代表從字串的最右邊開始,依給定的字串參數分割(預設用空格分割,此例指定用‘.’分割),並傳回列表型態的分割結果。

rsplit()接受兩個參數,第2個參數是「最大分割數」,其預設值為-1,代表沒有限制,設成1代表只切一次,所以副檔名字串一定是索引編號1的列表元素:

把副檔名轉成小寫,再透過in比對是否在允許的副檔名列表之中。
具備查驗上傳檔名以及傳遞快閃訊息的Python Flask程式
綜合以上說明,處理使用者上傳檔的Python Flask程式碼如下(app.py檔):
import os
import pathlib
from flask import Flask, flash, request, redirect, url_for, render_template
from werkzeug.utils import secure_filename
# 取得目前檔案所在的資料夾
SRC_PATH = pathlib.Path(__file__).parent.absolute()
UPLOAD_FOLDER = os.path.join(SRC_PATH, 'static', 'uploads')
app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' # 設置密鑰
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # 設置儲存上傳檔的資料夾
app.config['MAX_CONTENT_LENGTH'] = 3 * 1024 * 1024 # 上傳檔最大3MB
其中的SECRET_KEY(密鑰)設置說明,請參閱《超圖解Python程式設計入門》第12章「建立資料庫檔案」單元。
Flask預設允許上傳任何大小的檔案,從0.6版本之後加入‘MAX_CONTENT_LENGTH’(最大內容長度)設置,單位是位元組,上面程式裡的3 * 1024 * 1024代表上傳檔的大小上限3MB。若上傳檔超過限制,Flask將拋出RequestEntityTooLarge(直譯為「請求本體太大」)例外錯誤。
此app.py檔的其餘程式碼:
# 檢查上傳檔的副檔名
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
return render_template('index.html')
@app.route('/', methods=['POST'])
def upload_file():
if 'filename' not in request.files: # 如果表單的「檔案」欄位沒有'filename'
flash('沒有上傳檔案')
return redirect(url_for('index'))
file = request.files['filename'] # 取得上傳的檔案
if file.filename == '': # 若上傳的檔名是空白的…
flash('請選擇要上傳的影像') # 發出快閃訊息
return redirect(url_for('index')) # 令瀏覽器跳回首頁
if file and allowed_file(file.filename): # 確認有檔案且副檔名在允許之列
filename = secure_filename(file.filename) # 轉成「安全的檔名」
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
flash('影像上傳完畢!')
# 顯示頁面並傳入上傳的檔名
return render_template('index.html', filename=filename)
else:
flash('僅允許上傳png, jpg, jpeg和gif影像檔')
return redirect(url_for('index')) # 令瀏覽器跳回首頁
@app.route('/img/<filename>') # 處理請求“/img/圖檔”的路由;顯示影像。
def display_image(filename):
return redirect(url_for('static', filename='uploads/' + filename))
if __name__ == "__main__":
app.run()
《超圖解Python程式設計入門》第9章「解析表單資料」單元提到,Flask程式透過request.form取得表單欄位資料。讀取表單的「檔案」欄位,則是透過request.files。
本文的HTML表單的「檔案」欄位命名成”filename”,所以request.files[‘filename’]敘述將取得上傳檔;如果request.files裡面沒有’filename’欄位,代表使用者沒有上傳檔案。
啟動此Python虛擬環境,執行python app.py即可測試上傳檔案服務。
副檔名不代表真實的檔案類型
除了擷取上傳檔的副檔名,程式也能透過「檔案」物件的content_type屬性讀取MIME類型字串來辨別檔案。假設上傳檔是“cookies.jpg”影像檔,底下的敘述將顯示 “檔案類型:image/jpeg”。
file = request.files['filename'] # 取得上傳檔
mimetype = file.content_type # 讀取檔案的MIME類型
print('檔案類型:' + mimetype)
然而,MIME類型也是藉由副檔名來辨別檔案,如果把某個.exe檔的副檔名改成.jpg,也能蒙混過關。所以,程式不能依賴副檔名來檢測使用者上傳的檔案類型,下一篇文章會介紹解決方式。

您好!感謝您的教學,剛好是我想要做的東西
按照您的教學做好以後,發現不能上傳中文檔名?不知道是不是我哪裡漏了?
因為secure_filename()不支援中文,最簡單的方式就是把這一行:
filename = secure_filename(file.filename) # 轉成「安全的檔名」
改成:
filename = file.filename