最近在使用Flask搭建個(gè)人博客系統(tǒng)時(shí),內(nèi)容編輯頁上傳文件時(shí)遇到"缺少圖像源文件地址"問題,經(jīng)過查詢資料打算把Flask文件上傳系統(tǒng)學(xué)習(xí)過程記錄下來。
文件上傳是個(gè)躲不掉的問題,用戶頭像,文章圖片,文件分享等等都需要上傳功能。但這里涉及很多內(nèi)容,上傳文件,過濾文件類型,限制大小,上傳前的編輯篩選,拖拽上傳,進(jìn)度條,文件命名,文件目錄管理,訪問速度……
一、使用Flask原生實(shí)現(xiàn)文件上傳
這是一個(gè)圖片上傳完整的實(shí)現(xiàn)Demo,可以復(fù)制體驗(yàn)一下,后面會(huì)有詳解。
# -*- coding: utf-8 -*-
import os
from flask import Flask, request, url_for, send_from_directory
from werkzeug import secure_filename
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.getcwd()
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
html = '''
<!DOCTYPE html>
<title>Upload File</title>
<h1>圖片上傳</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=上傳>
</form>
'''
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'],
filename)
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files['file']
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
file_url = url_for('uploaded_file', filename=filename)
return html + '<br><img src=' + file_url + '>'
return html
if __name__ == '__main__':
app.run()
簡單來說,只有三個(gè)步驟:
1、創(chuàng)建一個(gè)上傳表單:
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
2、當(dāng)按下提交鍵后,通過request對象上的files獲取文件。和以前用request獲取表單值一樣,使用input字段的name值獲?。?/p>
file = request.files['file']
3、使用save()方法保存文件,指定保存的地址及文件名:
file.save(path + filename)
當(dāng)然,除了這些,還有很多東西要考慮。
上傳配置
在這里我們設(shè)置上傳文件夾地址、允許的文件擴(kuò)展名、限制文件大?。?/p>
UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
你也可以使用:
app.config['UPLOAD_FOLDER'] = '/path/to/the/uploads'
安全問題
1、導(dǎo)入Werkzeug提供的secure_filename()函數(shù)來檢查文件名,它會(huì)過濾掉危險(xiǎn)字符(比如../../../home/username):
filename = secure_filename(file.filename)
要注意的是,secure_filename僅返回ASCII字符。所以, 非ASCII(比如漢字)會(huì)被過濾掉,空格會(huì)被替換為下劃線。你也可以自己處理文件名,或是在使用這個(gè)函數(shù)前將中文替換為拼音或是英文。具體見后續(xù)文章。
2、使用我們在上面配置的擴(kuò)展名來檢查文件類型。
創(chuàng)建一個(gè)檢查函數(shù):
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
判斷上傳文件名:
...
if file and allowed_file(file.filename):
...
3、使用上面配置的文件最大長度來檢查文件大?。▋H需要配置),如果超過限制,會(huì)拋出RequestEntityTooLarge異常,進(jìn)而返回413錯(cuò)誤(在開發(fā)服務(wù)器可能會(huì)直接斷開連接,屬正?,F(xiàn)象)。
獲取上傳后的文件
配置一個(gè)函數(shù)來獲取上傳文件的url:
from flask import send_from_directory
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'],
filename)
獲取url:
file_url = url_for('uploaded_file', filename=filename)
二、用擴(kuò)展Flask-Uploads實(shí)現(xiàn)上傳功能
Flask-Uploads簡化了大部分操作。比如在上面的原生上傳中,我們需要自己設(shè)置一個(gè)集合來設(shè)置允許哪些類型的文件(ALLOWED_EXTENSIONS),而Flask-Uploads已經(jīng)把常用的文件類型分好類,你只需要導(dǎo)入相應(yīng)的集合名稱,比如IMAGES、TEXT、AUDIO等等(默認(rèn)配置為DEFAULTS,包括TEXT + DOCUMENTS + IMAGES + DATA)。除此之外,你還可以配置全部允許(All)、除某些文件類型外全允許(AllExcept())。這些集合也可以組合使用(比如IMAGES+TEXT)。
同樣是一個(gè)圖片上傳的完整實(shí)現(xiàn)demo,這次要簡單的多。
# -*- coding: utf-8 -*-
import os
from flask import Flask, request
from flask_uploads import UploadSet, configure_uploads, IMAGES,\
patch_request_class
app = Flask(__name__)
app.config['UPLOADED_PHOTOS_DEST'] = os.getcwd() # 文件儲(chǔ)存地址
photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)
patch_request_class(app) # 文件大小限制,默認(rèn)為16MB
html = '''
<!DOCTYPE html>
<title>Upload File</title>
<h1>圖片上傳</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=photo>
<input type=submit value=上傳>
</form>
'''
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST' and 'photo' in request.files:
filename = photos.save(request.files['photo'])
file_url = photos.url(filename)
return html + '<br><img src=' + file_url + '>'
return html
if __name__ == '__main__':
app.run()
注:視圖函數(shù)里的兩個(gè)‘photo’均指的是input字段的name值。
上傳配置
因?yàn)镕lask-Uploads允許你創(chuàng)建不同的set(代表你上傳的文件的集合),所以配置時(shí)要把下面的FILES替換成你的set名。
比如文件儲(chǔ)存地址設(shè)置:
UPLOADED_FILES_DEST = '/path/to/the/uploads'
如果你的set叫photos,那么這條設(shè)置就改成:
UPLOADED_PHOTOS_DEST = '/path/to/the/uploads'
你也可以設(shè)置一個(gè)默認(rèn)的配置:
UPLOADS_DEFAULT_DEST = '/path/to/the/uploads'
安全問題
1、文件類型過濾
創(chuàng)建一個(gè)set(通過實(shí)例化UploadSet()類實(shí)現(xiàn)),然后使用configure_uploads()方法注冊并完成相應(yīng)的配置(類似大多數(shù)擴(kuò)展提供的初始化類),傳入當(dāng)前應(yīng)用實(shí)例和set:
photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)
這里的photos是set的名字,它很重要。因?yàn)榻酉聛硭痛砟阋呀?jīng)保存的文件,對它調(diào)用save()方法保存文件,對它調(diào)用url()獲取文件url,對它調(diào)用path()獲取文件的絕對地址……(你可以把它類比成代表數(shù)據(jù)庫的db)
2、文件名過濾
不再需要secure_filename()函數(shù)來處理文件名,使用set的save()方法直接保存從request對象獲取的文件,將返回處理后的文件名:
filename = photos.save(request.files['photo'])
可能還需要再提醒一下,這里的['photo']是input字段的name值。
3、限制文件大小
導(dǎo)入patch_request_class()函數(shù),傳入應(yīng)用實(shí)例和大?。J(rèn)為16MB),比如:
patch_request_class(app)
或是
patch_request_class(app, 32 * 1024 * 1024) # 或是從配置里讀取大小
獲取文件
你不必再創(chuàng)建新的視圖函數(shù)來獲取文件,F(xiàn)lask-Uploads自帶了一個(gè)視圖函數(shù),當(dāng)你對一個(gè)set(比如我們上面創(chuàng)建的photos)使用url()方法(傳入文件名作為參數(shù)),比如:
photos.url('demo.jpg')
它會(huì)返回一個(gè)文件的url:
http://example.com/_uploads/photos/demo.jpg # /<setname>/<path:filename>
三、使用Flask-WTF
完整實(shí)現(xiàn)demo,這次把HTML代碼放在了單獨(dú)的文件,效果仍然和之前兩個(gè)版本相同。
upload.py
# -*- coding: utf-8 -*-
import os
from flask import Flask, render_template
from flask_uploads import UploadSet, configure_uploads, IMAGES, patch_request_class
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField
app = Flask(__name__)
app.config['SECRET_KEY'] = 'I have a dream'
app.config['UPLOADED_PHOTOS_DEST'] = os.getcwd()
photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)
patch_request_class(app) # set maximum file size, default is 16MB
class UploadForm(FlaskForm):
photo = FileField(validators=[
FileAllowed(photos, u'只能上傳圖片!'),
FileRequired(u'文件未選擇!')])
submit = SubmitField(u'上傳')
@app.route('/', methods=['GET', 'POST'])
def upload_file():
form = UploadForm()
if form.validate_on_submit():
filename = photos.save(form.photo.data)
file_url = photos.url(filename)
else:
file_url = None
return render_template('index.html', form=form, file_url=file_url)
if __name__ == '__main__':
app.run()
模板文件:index.html
<!DOCTYPE html>
<title>Upload File</title>
<h1>圖片上傳</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ form.photo }}
{% for error in form.photo.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{{ form.submit }}
</form>
{% if file_url %}
<br><img src="{{ file_url }}">
{% endif %}
1、初始化
為CSRF保護(hù)創(chuàng)建SECRET_KEY設(shè)置:
app.config['SECRET_KEY'] = 'a random string'
2、創(chuàng)建表單
使用Flask-WTF提供的FileField字段以及FileRequired、FileAllowed驗(yàn)證函數(shù)
from flask_wtf.file import FileField, FileRequired, FileAllowed
可以傳入錯(cuò)誤信息:
photo = FileField(u'圖片上傳', validators=[
FileAllowed(photos, u'只能上傳圖片!'),
FileRequired(u'文件未選擇!')
])
這里FileAllowed()的第一個(gè)參數(shù)是使用Flask-Uploads設(shè)置的set名稱,如果不使用Flask-Uploads,可以替換成文件后綴組成的列表,比如['jpg', 'png']。
3、在模板中渲染錯(cuò)誤信息
{% for error in form.field.errors %}
{{ error }}
{% endfor %}
這里面的field是字段名稱,比如上面的photo。
4、獲取文件
文件數(shù)據(jù)不再從request對象獲取,而是使用data方法獲?。?/p>
filename = photos.save(form.photo.data)