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库在客户端进行预验证,并添加进度条来优化用户体验。

相关文章
相关标签/搜索