(一)Flask 實(shí)現(xiàn)單個(gè)文件上傳

最近在使用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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評論 2 372