赵走x博客
网站访问量:151887
首页
书籍
软件
工具
古诗词
搜索
登录
Flask 实战:41、电子邮件进阶实践
Flask 实战:40、使用事务邮件服务SendGird
Flask 实战:39、使用Flask-Mail发送电子邮件
Flask 实战:38、数据库进阶实践
Flask 实战:37、更新数据库表
Flask 实战:36、定义关系
Flask 实战:35、在视图函数里操作数据库
Flask 实战:34、数据库操作:CRUD
Flask 实战:33、使用Flask-SQLAlchemy管理数据库
Flask 实战:32、ORM魔法
Flask 实战:31、数据库的分类
Flask 实战:30、单个页面多个表单
Flask 实战:29、单个表单多个提交按钮
Flask 实战:28、使用Flask-CKEditor集成富文本编辑器
Flask 实战:27、多文件上传
Flask 实战:26、文件上传
Flask 实战:25、自定义验证器
Flask 实战:24、使用宏渲染表单
Flask 实战:23、设置错误消息语言
Flask 实战:22、处理表单数据
Flask 实战:21、使用Flask-WTF处理表单
Flask 实战:20、HTML表单
Flask 实战:19、模板进阶实践
Flask 实战:18、模板结构组织
Flask 实战:17、模板辅助工具
Flask 实战:16、模板基本用法
Flask 实战:15、HTTP进阶实践
Flask 实战:14、Flask上下文
Flask 实战:13、HTTP响应
Flask 实战:12、HTTP请求
Flask 实战:11、请求响应循环
Flask 实战:10、Flask与MVC架构
Flask 实战:9、模板与静态文件
Flask 实战:8、Flask命令
Flask 实战:7、URL与端点
Flask 实战:6、项目配置
Flask 实战:5、Flask扩展
Flask 实战:4、Python Shell
Flask 实战:3、启动开发服务器
Flask 实战:2、Hello,Flask!
Flask 实战:1、初识Flask
Flask 实战:26、文件上传
资源编号:75877
Python Web
Flask Web开发实战:入门、进阶与原理解析
热度:193
在HTML中,渲染一个文件上传字段只需要将`<input>`标签的type属性设为file,即`<input type="file">`。这会在浏览器中渲染成一个文件上传字段,单击文件选择按钮会打开文件选择窗口,选择对应的文件后,被选择的文件名会显示在文件选择按钮旁边。
在HTML中,渲染一个文件上传字段只需要将`
`标签的type属性设为file,即`
`。这会在浏览器中渲染成一个文件上传字段,单击文件选择按钮会打开文件选择窗口,选择对应的文件后,被选择的文件名会显示在文件选择按钮旁边。 在服务器端,可以和普通数据一样获取上传文件数据并保存。不过我们需要考虑安全问题,文件上传漏洞也是比较流行的攻击方式。除了常规的CSRF防范,我们还需要重点注意下面的问题: * 验证文件类型。 * 验证文件大小。 * 过滤文件名。 ### 1.定义上传表单 在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() ``` 为了便于测试,我们创建一个用来上传图片的photo字段。和其他字段类似,我们也需要对文件上传字段进行验证。Flask-WTF在flask_wtf.file模块下提供了两个文件相关的验证器,用法说明如表4-5所示。  表4-5 Flask-WTF提供的上传文件验证器 我们使用FileRequired确保提交的表单字段中包含文件数据。出于安全考虑,我们必须对上传的文件类型进行限制。如果用户可以上传HTML文件,而且我们同时提供了视图函数获取上传后的文件,那么很容易导致XSS攻击。我们使用FileAllowed设置允许的文件类型,传入一个包含允许文件类型的后缀名列表。 顺便说一下,Flask-WTF提供的FileAllowed是在服务器端验证上传文件,使用HTML5中的accept属性也可以在客户端实现简单的类型过滤。这个属性接收MIME类型字符串或文件格式后缀,多个值之间使用逗号分隔,比如: ```
``` 当用户单击文件选择按钮后,打开的文件选择窗口会默认将accept属性值之外的文件过滤掉。尽管如此,用户还是可以选择设定之外的文件,所以我们仍然需要进行服务器端验证。 扩展Flask-Uploads([https://github.com/maxcountryman/flask-uploads](https://github.com/maxcountryman/flask-uploads) )内置了在Flask中实现文件上传的便利功能。Flask-WTF提供的FileAllowed()也支持传入Flask-Uploads中的上传集对象(Upload Set)作为upload_set参数的值。另外,同类的扩展还有Flask-Transfer([https://github.com/justanr/Flask-Transfer](https://github.com/justanr/Flask-Transfer) )。 除了验证文件的类型,我们通常还需要对文件大小进行验证,你肯定不想让用户上传超大的文件来拖垮你的服务器。通过设置Flask内置的配置变量MAX_CONTENT_LENGTH,我们可以限制请求报文的最大长度,单位为字节(byte)。比如,下面将最大长度限制为3M: ``` app.config['MAX_CONTENT_LENGTH'] = 3 * 1024 * 1024 ``` 当请求数据(上传文件大小)超过这个限制后,会返回413错误响应(Request Entity Too Large),如图4-6所示。  图4-6 413错误响应 我们可以创建对应的错误处理函数来返回自定义的413错误响应。需要注意,Flask内置的开发服务器在抛出对应的异常时不会返回413响应,而是中断连接。不过我们不用担心这个问题,当使用生产环境下的服务器时,会正确返回413错误响应。 ### 2.渲染上传表单 在新创建的upload视图里,我们实例化表单类UploadForm,然后传入模板: ``` @app.route('/upload', methods=['GET', 'POST']) def upload(): form = UploadForm() ... return render_template('upload.html', form=form) ``` 下面代码清单在模板中渲染了这个表单,渲染方式和其他字段相同。 ```
{{ form.csrf_token }} {{ form_field(form.photo) }} {{ form.submit }}
``` 唯一需要注意的是,当表单中包含文件上传字段时(即type属性为file的input标签),需要将表单的enctype属性设为"multipart/form-data",这会告诉浏览器将上传数据发送到服务器,否则仅会把文件名作为表单数据提交。 ### 3.处理上传文件 和普通的表单数据不同,当包含上传文件字段的表单提交后,上传的文件需要在请求对象的files属性(request.files)中获取。我们在第2章介绍过,这个属性是Werkzeug提供的ImmutableMultiDict字典对象,存储字段的name键值和文件对象的映射,比如: ``` ImmutableMultiDict([('photo',
)]) ``` 上传的文件会被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('avatar!@#//#\\%$^&.jpg') 'avatar.jpg' >>> secure_filename('avatar头像.jpg') 'avatar.jpg' ``` #### (3)统一重命名 secure_filename()函数非常方便,它会过滤掉文件名中的非ASCII字符。但如果文件名完全由非ASCII字符组成,那么会得到一个空文件名: ``` >>> secure_filename('头像.jpg') 'jpg' ``` 为了避免出现这种情况,更好的做法是使用统一的处理方式对所有上传的文件重新命名。随机文件名有很多种方式可以生成,下面是一个使用Python内置的uuid模块生成随机文件名的random_filename()函数: ``` def random_filename(filename): ext = os.path.splitext(filename)[1] new_filename = uuid.uuid4().hex + ext return new_filename ``` 这个函数接收原文件名作为参数,使用内置的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__))。为了保存文件,你需要提前手动创建这个文件夹。 对FileStorage对象调用save()方法即可保存,传入包含目标文件夹绝对路径和文件名在内的完整保存路径: ``` f.save(os.path.join(app.config['UPLOAD_PATH'], filename)) ``` 文件保存后,我们希望能够显示上传后的图片。为了让上传后的文件能够通过URL获取,我们还需要创建一个视图函数来返回上传后的文件,如下所示: ``` @app.route('/uploads/
') 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,作为
标签的src属性值,如下所示: ```
``` 访问[http://localhost:5000/upload](http://localhost:5000/upload) 打开文件上传示例,选择文件并提交后即可看到上传后的图片。另外,你会在示例程序文件夹中的uploads目录下发现上传的文件。