本文完成的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