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

 

文件上傳

在HTML中,渲染一個文件上傳字段只須要將<input>標籤的type屬性設爲file,即<input type=」file」>。html

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

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

 

定義上傳表單

在python表單類中建立文件上傳字段時,咱們使用擴展Flask-WTF提供的FileField類,它集成WTForms提供的上傳字段FileField,添加了對Flask的集成。例如: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模塊下提供了兩個文件相關的驗證器,用法以下:bash

 

咱們使用FileRequired確保提交的表單字段中包含文件數據。處於安全考慮,必須對上傳的文件類型進行限制。若是用戶能夠上傳HTML文件,並且咱們同時提供了視圖函數獲取上傳後的文件,那麼很容易致使XSS攻擊。使用FileAllowed設置容許的文件類型,傳入一個包含容許文件類型的後綴名列表。服務器

 

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

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

 

當用戶單擊文件選擇按鈕後,打開的文件選擇窗口會默認將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庫在客戶端進行預驗證,並添加進度條來優化用戶體驗。

相關文章
相關標籤/搜索