flask 文件上傳(單文件上傳、多文件上傳)--


 

文件上傳

在HTML中,渲染一個文件上傳字段只需要將<input>標簽的type屬性設為file,即<input type=”file”>。

這會在瀏覽器中渲染成一個文件上傳字段,單擊文件選擇按鈕會打開文件選擇窗口,選擇對應的文件后,被選擇的文件名會顯示在文件選擇按鈕旁邊。

在服務器端,可以和普通數據一樣獲取上傳文件數據並保存。不過需要考慮安全問題,文件上傳的漏洞也是比較流行的攻擊方式。除了常規的CSRF防范,我們還需要重點關注這幾個問題:驗證文件類型、驗證文件大小、過濾文件名

 

定義上傳表單

在python表單類中創建文件上傳字段時,我們使用擴展Flask-WTF提供的FileField類,它集成WTForms提供的上傳字段FileField,添加了對Flask的集成。例如:

創建上傳表單:

 

from flask_wtf.file import FileField, FileRequired, FileAllowed

class UploadForm(FlaskForm):
    photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])])
    submit = SubmitField()

 

在表單類UploadForm()中創建了一個FileField類的photo字段,用來上傳圖片。

和其他字段類似,需要對文件上傳字段進行驗證。Flask-WTF在flask_wtf.file模塊下提供了兩個文件相關的驗證器,用法如下:

 

我們使用FileRequired確保提交的表單字段中包含文件數據。處於安全考慮,必須對上傳的文件類型進行限制。如果用戶可以上傳HTML文件,而且我們同時提供了視圖函數獲取上傳后的文件,那么很容易導致XSS攻擊。使用FileAllowed設置允許的文件類型,傳入一個包含允許文件類型的后綴名列表。

 

Flask-WTF提供的FileAllowed是在服務器端驗證上傳文件,使用HTML5中的accept屬性也可以在客戶端實現簡單的類型過濾。這個屬性接收MIME類型字符串或文件格式后綴,多個值之間使用逗號分隔,比如:

<input type=”file” id=”profile_pic” name=”profile_pic” accept=”.jpg, .jpeg, .png, .gif”>

 

當用戶單擊文件選擇按鈕后,打開的文件選擇窗口會默認將accept屬性之外的文件過濾掉(其實沒有過濾掉)。

盡管如此,用戶還是可以選擇設定之外的文件,所以仍然需要在服務器端驗證。

 

驗證文件大小,通過設置Flask內置的配置變量MAX_CONTENT_LENGTH,可以顯示請求報文的最大長度,單位是字節,比如:

app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024

 

當上傳文件的大小超過這個限制后,flask內置的開服務器會中斷連接,在生產環境的服務器上會返回413錯誤響應。

 

渲染上傳表單

在新創建的upload視圖里,我們實例化表單類UploadForm,然后傳入模板:

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    return render_template('upload.html',form = form)

 

在upload.html中渲染上傳表單

{% from 'macros.html' import form_field %}

{% extends 'base.html' %}
{% block content %}
    <form method="post" enctype="multipart/form-data">
        {{ form.csrf_token }}
        {{ form_field(form.photo) }}<br>
        {{ form.submit }}<br>
    </form>
{% endblock %}

 

 

需要注意的是,當表單中包含文件上傳字段時(即type屬性為file的input標簽)需要將表單的enctype屬性設為”multipart/form-data”,這會告訴瀏覽器將上傳數據發送到服務器,否則僅會把文件名作為表單數據提交。

 

處理上傳文件

和普通的表單數據不同,當包含上傳文件字段的表單提價后,上傳的文件需要在請求對象的files屬性(request.files)中獲取。這個屬性(request.files)是Werkzeug提供的ImmutableMultiDict字典對象,存儲字段name鍵值和文件對象的映射,比如:

ImmutableMultiDict([('photo', <FileStorage: u'xiaxiaoxu.JPG' (image/jpeg)>)])

 

上傳的文件會被Flask解析為Werkzeug中的FileStorage對象(werkzeug.datastructures.FileStorage)。當手動處理時,需要使用文件上傳字段的name屬性值作為鍵獲取對應的文件對象。比如:

request.files.get(‘photo’)

 

當使用Flask-WTF時,它會自動幫我們獲取對應的文件對象,這里我們仍然使用表單類屬性的data屬性獲取上傳文件。處理上傳表單提交請求的upload視圖函數如下:

import os
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        f = form.photo.data
        filename =random_filename(f.filename)
        f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
        flash('Upload success.')
        session['filenames'] = [filename]
        return redirect(url_for('show_images'))
    return render_template('upload.html', form = form)
 
        

里面的函數在后面說明

當表單通過驗證后,我們通過form.photo.data獲取存儲上傳文件的FileStorage對象。接下來,我們需要處理文件名,通常有三種處理方

處理文件名的方式

1)使用原文件名

如果能夠確定文件的來源安全,可以直接使用原文件名,通過FileStorage對象的filename屬性獲取:

filename = f.filename

 

2)使用過濾后的文件名

如果要支持用戶上傳文件,我們必須對文件名進行處理,因為攻擊者可能會在文件名中加入惡意路徑。比如,如果惡意用戶在文件名中加入表示上級目錄的..(比如../../../home/username/.bashrc或../../etc/passwd),那么當我們保存文件時,如果這里表示上級目錄的../數量正確,就會導致服務器上的系統該文件被覆蓋或篡改,還有可能執行惡意腳本。我們可以使用Werkzeug提供的secure_filename()函數對文件名進行過濾,傳遞文件名作為參數,它會過濾掉所有危險字符,返回“安全的文件名”,如下所示:

 

>>> from werkzeug import secure_filename

>>> secure_filename('sam!@$%^&.jpg')

'sam.jpg'

>>> secure_filename('sam圖片.jpg')

'sam.jpg'

>>> 

 

3)統一重命名

secure_filename()函數非常方便,它會過濾掉文件名中的非ASCII字符。但如果文件名完全由非ASCII字符組成,那么會得到一個空文件名:

>>> secure_filename('圖像.jpg')

'jpg'

 

為了避免出現這種情況,更好的做法是使用統一的處理方式對所有上傳的文件重新命名。隨機文件名有很多種方式生成,下面是一個是python內置的uuid模塊生成隨機文件名的random_filename()函數:

import uuid

def random_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return new_filename
 

其中os.path.splitext()和uuid.uuid4()的用法如下:

>>> import os

>>> os.path.splitext('d://sam/sam.jpg')

('d://sam/sam', '.jpg')

 

>>> import uuid

>>> uuid.uuid4()

UUID('b35f485e-5a79-4d98-8cac-af62be1f0a36')

>>> uuid.uuid4().hex

'62f65743d16e4b388f9f6eabe3f8e5b4'

 

這個函數接收原文件名作為參數,使用內置的uuid模塊中的uuid4()方法生成新的文件名,並使用hex屬性獲取十六進制字符串,最后返回包含后綴的新文件名。

 

UUID(Universally Unique Identifier,通用唯一識別碼)是用來表示信息的128位數字,比如用作數據庫表的主鍵。使用標准方法生成的UUID出現重復的可能性接近0。在UUID的標准中,UUID分為5個版本,每個版本使用不同的生產方法並且適用於不同的場景。我們使用的uuid4()方法對應的第4個版本:不接受參數而生成的隨機UUID。

 

在upload視圖中,我們調用這個函數獲取隨機文件名,傳入原文件名作為參數:

filename = random_filename(f.filename)

 

處理完文件名后,是時候將文件保存到文件系統中了。在form目錄下創建一個uploads文件夾,用於保存上傳后的文件。指向這個文件夾的絕對路徑存儲在自定義配置變量UPLOAD_PATH中:

app.config[‘UPLOAD_PATH’] = os.path.join(app.root_path, ‘uploads’)

這里的路徑通過app.root_path屬性構造,它存儲了程序實例所在腳本的絕對路徑,相當於:

os.path.abspath(os.path.dirname(__file__))。為了保存文件,需要提前手動創建這個文件夾。

調試:

print "__file__:",__file__
print "app.root_path:",app.root_path
 

結果:

__file__: D:/flask/FLASK_PRACTICE/form/app.py

app.root_path: D:\flask\FLASK_PRACTICE\form

 

對FileStorage對象調用save()方法即可保存,傳入包含目標文件夾絕對路徑和文件名在內的完整保存路徑:

f.save(os.path.join(app.config[‘upload_path’], filename))

文件保存后,我們希望能夠顯示長傳后的圖片,為了讓上傳后的文件能夠通過URL獲取,我們需要創建一個視圖函數來返回上傳后的文件,如下所示:

@app.route('/uploads/<path:filename>')
def get_file(filename):
    return send_from_directory(app.config['UPLOAD_PATH', filename])

 

 

這個視圖的作用與Flask內置的static視圖類似,通過傳入的文件路徑返回對應的靜態文件。在這個uploads視圖中,使用Flask提供的send_from_directory()函數來獲取文件,傳入文件的路徑和文件名作為參數。

 

在get_file視圖的URL規則中,filename變量使用了path轉換器以支持傳入包含斜線的路徑字符串。

upload視圖里保存文件后,使用flash()發送一個提示,將文件名保存到session中,最后重定向到show_images視圖。show_images視圖返回的uploaded.html模板中將從session獲取文件名,渲染出上傳后的圖片。

flash('Upload success.')
session['filenames'] = [filename]
return redirect(url_for('show_images'))

 

這里將filename作為列表傳入session只是為了兼容下面的多文件上傳示例,這兩個視圖使用同一個模板,使用session可以在模板中統一從session獲取文件名列表。

 

在uploaded.html模板里,我們將傳入的文件名作為URL變量,通過上面的get_file視圖獲取文件URL,作為<img>標簽的src屬性值,如下所示:

<img src="{{ url_for('get_file', filename=filename) }}">

訪問127.0.0.1:5000/upload,打開文件上傳示例,選擇文件並提交后即可看到上傳后的圖片。另外,在uploads文件夾中可以看到上傳的文件。

 

 

 

提交后,看到圖片

 

uploads目錄下保存的文件:

 

下面列一下涉及的文件:

app.py:

from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask import send_from_directory

class UploadForm(FlaskForm):
    photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])])
    submit = SubmitField()

import os
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')

import uuid

def random_filename(filename):
    ext = os.path.splitext(filename)[1]
    new_filename = uuid.uuid4().hex + ext
    return new_filename

@app.route('/uploaded-images')
def show_images():
    return render_template('uploaded.html')

@app.route('/uploads/<path:filename>')
def get_file(filename):
    return send_from_directory(app.config['UPLOAD_PATH'], filename)


@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        f = form.photo.data
        filename =random_filename(f.filename)
        f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
        flash('Upload success.')
        session['filenames'] = [filename]
        return redirect(url_for('show_images'))
    return render_template('upload.html', form = form)
 
        

uploaded.html:

 

{% extends 'base.html' %}
{% from 'macros.html' import form_field %}

{% block title %}Home{% endblock %}

{% block content %}
{% if session.filenames %}
{% for filename in session.filenames %}
<a href="{{ url_for('get_file', filename=filename) }}" target="_blank">
    <img src="{{ url_for('get_file', filename=filename) }}">
</a>
{% endfor %}
{% endif %}
{% endblock %}

 

upload.html:

 

{% from 'macros.html' import form_field %}

{% extends 'base.html' %}
{% block content %}
    <form method="post" enctype="multipart/form-data">
        {{ form.csrf_token }}
        {{ form_field(form.photo) }}<br>
        {{ form.submit }}<br>
    </form>
{% endblock %}

 

多文件上傳

因為Flask-WTF當前版本中並未添加多多文件上傳到額渲染和驗證支持,因此需要在視圖函數中手動獲取文件並進行驗證。

 

在客戶端,通過在文件上傳字段(type=file)加入multiple屬性,就可以開啟多選:

<input type=”file” id=”file” multiple>

創建表單類時,可以直接使用WTForms提供的MultipleFileField字段實現,添加一個DataRequired驗證器來確保包含文件:

 

from wtforms import MultipleFileField
class MultiUploadForm(FlaskForm):
    photo = MultipleFileField('Upload Image', validators={DataRequired()})
    submit = SubmitField()

 

表單提交時,在服務器端的程序中,對request.files屬性調用getlist()方法並傳入字段的name屬性值會返回包含所有上傳文件對象的列表。在multi_upload視圖中,我們遍歷這個列表,然后逐一對文件進行處理:

from flask import url_for, request, session, flash, redirect
from flask_wtf.csrf import validate_csrf
from wtforms import ValidationError

@app.route('/multi-upload', methods=['GET', 'POST'])
def multi_upload():
    form = MultiUploadForm()
    if request.method == 'POST':
        filenames = []
        #驗證CSRF令牌
        try:
            validate_csrf(form.csrf_token.data)
        except ValidationError:
            flash('CSRF token error.')
            return redirect(url_for('multi_upload'))
        #檢查文件是否存在
        if 'photo' not in request.files:
            flash('This field is required.')
            return redirect(url_for('multi_upload'))
        for f in request.files.getlist('photo'):
            #檢查文件類型
            if f and allowed_file(f.filename):
                filename = random_filename(f.filename)
                f.save(os.path.join(app.config['UPLOAD_PATH'], filename ))
                filenames.append(filename)
            else:
                flash('Invalid file type:')
                return redirect(url_for('multi_upload'))
        flash('Upload success.')
        session['filenames'] = filenames
        return redirect(url_for('show_images'))
    return render_template('upload.html', form=form)
 

在請求方法為POST時,我們對上傳數據進行手動驗證,包含下面幾步:

1)  手動調用flask_wtf.csrf.validate_csrf驗證CSRF令牌,傳入表單中csrf_token隱藏字段的值。如果拋出wtforms.ValidationError異常則表明驗證未通過。

2)  其中if ‘photo’ not in request.files用來確保字段中包含文件數據(相當於FileRequired驗證器),如果用戶沒有選擇文件就提交表單則request_files將是空(實際上,不選擇文件,點擊提交,會觸發瀏覽器內置提示)。

3)  if f用來確保文件對象存在,這里也可以檢查f是否是FileStorage實例。

4)  allowed_file(f.filename)調用了allowed_file()函數,傳入文件名。這個函數相當於FileAllowed驗證器,用來驗證文件類型,返回布爾值。

 

allowed_file()函數定義:

app.config['ALLOWED_EXTENSIONS'] = ['png', 'jpg', 'jpeg', 'gif']

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

 

 

在上面的一個驗證語句里,如果沒有通過驗證,我們使用flash()函數顯示錯誤消息,然后重定向到multi_uplaod視圖。

 

filesnames[]列表是為了方便測試,保存上傳后的文件名到session中。

 

訪問127.0.0.1:5000/multi-upload,單擊按鈕選擇多個文件,當上傳的文件通過驗證時,程序會重定向到show_images視圖,這個視圖返回的uploaded.html模板中將從session獲取所有文件名,渲染出所有上傳后的圖片。

 

 

在新版本的Flask-WTF發布后,可以和上傳單個文件相同的方式處理表單。比如可以使用Flask-WTF提供的的MultipleFileField來創建多文件上傳的字段,使用相應的驗證器對文件進行驗證。在視圖函數中,可以繼續使用form.validate_on_submit()來驗證表單,並通過form.photot.data來獲取字段的數據:包含所有上傳文件對象(werkzeug.datastructures.FileStorage)的列表。

 

多文件上傳處理通常會使用JavaScript庫在客戶端進行預驗證,並添加進度條來優化用戶體驗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM