使用Python Flask建置影像圖檔上傳網站服務(三)

本文完成的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()函式(直譯為「安全檔名」)檢查並轉換上傳檔案的名稱。

secure_filename()函式

用Python程式確認上傳檔的副檔名

雖然HTML表單已經設定僅限上傳影像檔,但仍可被輕易繞過限制,而且存取網站資源也不一定要透過瀏覽器,所以伺服器端程式有必要檢查表單資料。

假設Python程式的filename變數儲存了上傳檔名”bug.EXE.JPG”,擷取其中的副檔名的方法有很多種。底下敘述採用字串物件的endswith()方法(代表ends with,「以…結尾」之意)檢查變數內容是否以“.jpg”結尾。

字串物件的endswith()方法

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

字串物件的endswith()方法

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

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()方法

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

字串物件的rsplit()方法

把副檔名轉成小寫,再透過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,也能蒙混過關。所以,程式不能依賴副檔名來檢測使用者上傳的檔案類型,下一篇文章會介紹解決方式。

Posts created 483

2 thoughts on “使用Python Flask建置影像圖檔上傳網站服務(三)

  1. 您好!感謝您的教學,剛好是我想要做的東西
    按照您的教學做好以後,發現不能上傳中文檔名?不知道是不是我哪裡漏了?

    1. 因為secure_filename()不支援中文,最簡單的方式就是把這一行:

      filename = secure_filename(file.filename) # 轉成「安全的檔名」

      改成:

      filename = file.filename

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top