cover

Flask 中的文件上传和下载

本文主要介绍了在 flask 中如何实现文件的上传和下载, 同时稍微深入的探寻了一下文件上传和下载的底层原理.

很久以前

heading

概述

在 flask 中实现文件上传, 需要使用的是

request.files
对象, 基本流程是:

  1. 在模板的定义
    enctype=multipart/form-data
    form
  2. form
    中定义
    input[type=file, name=filename]
  3. 使用
    request.files[filename]
    来获取上传的文件
    FileStorage
    对象
  4. FileStorage
    对象中,
    filename
    为文件名, 使用
    read
    方法可以读取文件内容

在 flask 中实现文件下载, 需要使用

send_file
方法, 基本流程是:

  1. 构造/获取 需要被下载文件的
    file pointer
  2. send_file(fp, as_attachment=True, attachment_filename=filename)
    作为返回响应
import tempfilefrom flask import requests, send_file def handler(content_raw): # handle raw content in file return content_raw def read_and_download(): file = request.files.values()[0] filename, file_content = file.filename, file.read() content_handled = handler(file_content) # handler use to handle file content tmp = tempfile.TemporaryFile() # construct a tmp file tmp.write(content_handled) # save handled content to tmp file tmp.seek(0) # reset file stream position return send_file(tmp, as_attachment=True, attachment_filename="download.ext")
heading

文件上传的原理

文件上传, 其本质是通过客户端(仅以浏览器为例)将一种数据传输到服务器(即通过 form 提交数据), 其特殊之处在于, 传输的数据类型是二进制类型数据

一般的情况下, 我们通过 form 提交数据, 要做的事情有两件:

  1. 定义提交的目的地, 通过指定 form 的 action 实现
  2. 定义提交的方法, 通过指定 form 的 method 实现

当服务器接收到来自浏览器的请求之后, 会根据请求头的内容对数据进行处理.

对于一般的 form 数据, 浏览器会默认为请求头添加

Content-Type: application/x-www-form-urlencoded
(其含义为传输的数据类型是经过 URL 编码的数据).

但是对于文件类型的数据我们必须要手动设置

Content-Type
(因为文件的类型是二进制数据), 因此, 对于文件上传, 我们需要针对提交 form 数据添加额外的参数:

  • 设置
    form
    enctype
    multipart/form-data

设置之后, 请求头中会出现

Content-Type: multipart/form-data
,
multipart/form-data
这一类型的含义是被分割为多份的表单数据.

通过这样的处理, 文件就能被服务器接收到并且进行正确的处理, 下面是一个基于 flask 的文件上传的请求头实例

header

在 flask 中, 对于 上传的文件数据 的获取主要是在

werkzeug
中实现的, 想要阅读源码(flask==1.0.2)可以参考以下路径:

  1. flask/wrappers.py[L:168] -
    Request._load_form_data
  2. werkzeug/wrappers.py[L:379 ~ L:385] -
    parser.parse
  3. werkzeug/formparser.py[L:213] -
    _parse_multipart
heading

文件下载的原理

这里所说的文件下载并不是只将 flask 作为 client 进行下载, 而是指访问者可以从 flask server 下载文件

其实, 文件下载的本质是客户端(以浏览器为例)针对服务器响应的特殊处理.

我们看到的网页内容本质上都是服务器返回给浏览器的

数据
, 这些数据之所以会以不同的形式出现, 是因为浏览器会根据响应头中的
Content-Type
决定采用何种方式处理这些
数据
.

例如, 如果在响应头中存在

Content-Type: text/html
, 那么浏览器就会将返回数据以 HTML 的形式进行渲染.

那么一个可以下载文件的响应的请求头回事什么样的呢?

response

上图是一个简单的基于 flask 实现的文件下载的响应头, 图中红框标注的是要重点关注的地方.

Content-Type: application/octet-stream
表示响应数据的类型是
通用的二进制
类型数据, 浏览器针对
通用二进制
类型数据的默认处理方法就是触发
下载
操作.

Content-Disposition: attachment; filename="filename.ext"
的含义是, 以附件形式下载, 并且下载文件名为 filename.ext.

设置

Content-Disposition
头的原因有二:

  1. 某些类型的二进制数据浏览器是可用进行渲染的(比如 json 或者 pdf), 因此浏览器的默认行为就不再是下载而是渲染(此时
    Content-Disposition
    的值为
    inline
    , 因此对于如果希望总是触发下载操作, 就需要手动设置
    Content-Disposition
    attachment
    (即下载).
  2. 默认的情况下, 浏览器触发的下载操作并不会对下载的文件进行命名, 因此需要手动进行命名, 即为
    Content-Disposition
    添加
    filename="filename"
    .
heading

Flask 中实现文件上传和下载的方法

在 flask 中实现文件的上传与下载并不复杂, 因为许多针对底层的操作 flask 已经封装好了.

heading

实现文件上传

flask 中实现文件上传的步骤主要有四:

  1. 在 template 中定义
    enctype
    multipart/form-data
    ,
    method
    POST
    的 form 元素
  2. 在 form 添加
    input[type=file]
    元素
  3. 在 views 中定义处理 form action 的方法
  4. 在 view function 中 使用
    request.files
    获取文件并进行处理

下面是一个简单的例子(摘自 flask 官方文档并进行了一定的修改, 直接保存为 app.py 并运行即可)

from flask import request, Flask, redirect def upload_file(): if request.method == 'POST': # check if the post request has the file part if 'file' not in request.files: print('No file part') return redirect(request.url) file = request.files['file'] # if user does not select file, browser also # submit an empty part without filename if file.filename == '': print('No selected file') return redirect(request.url) return file.read().decode() # convert to str return ''' <!doctype html>人 <title>Upload new File</title> <h1>Upload new File</h1> <form action="{{ url_for('upload') }}" method="POST" enctype=multipart/form-data> <input type="file" name="file"> <input type="submit" value="Upload"> </form> </html> ''' app = Flask(__file__) app.add_url_rule('', 'index', upload_file, methods=('GET',)) app.add_url_rule('', 'download', upload_file, methods=('POST',)) app.run(debug=True)

flask.requests
是一个
MultiDict
(即一个 key 可以对应多个 value), 通过在定义
input[type=file, name=name]
即可以通过
flask.requests[name]
的方式获取到上传的文件.

上传的文件是 FileStorage 的实例,

FileStorage
werkzeug
针对
request stream
的封装, 所以可以直接使用
request stream
的方法进行处理.

比如, 获取文件名:

FileStorage().filename
, 获取文件大小:
FileStorage().content_length
, 获取文件内容:
FileStorage().read()

注意

  • FileStorage
    stream
    的封装, 因此在
    read
    之后 pointer 就指向了
    stream
    的末尾, 此时再次
    read
    无法得到任何内容, 如果需要再次使用
    read
    方法获取内容, 需要先 file.seek(0) 将 pointer 重新指向
    stream
    头部
heading

实现文件下载

实现文件下载主要是利用 flask 提供的

send_file
, 一个简单的例子如下(直接保存为 app.py 并运行即可):

import json import tempfile from flask import Flask, send_file def download_file(): file = tempfile.TemporaryFile() json.dump({'name': 'demo', 'age': 22}, file) return send_file(file, as_attachment=True, attachment_filename='demo.json') app = Flask(__file__) app.add_url_rule('', 'download', download_file, methods=('GET,')) app.run(debug=True)

如过希望下载文件, 则必须设置

as_attachment=True
以及
attachment_filename=filename
.

需要注意的是,

send_file
的第一个参数要求必须是
file pointer
(后续简称 fp). 一般地, 在 python 中得到一个 fp 的方式是使用
open
方法打开一个文件, 但这就要求文件必须存储于服务器上, 在某些情况下这样子是可行的, 但是在一般情况下, 服务器并不需要保存生成的文件, 在文件被下载之后就可以移除, 这样子能减少服务器在存储上的负担.

为此, 可以使用临时文件解决这个问题, 使用 python 标准库中的

tempfile
可以很方便地生成临时文件, 如果使用
(Named)TemporaryFile
甚至可以不考虑清除临时文件的问题(
tempfile
会在文件被关闭后自行清理).

注意:

  1. 不要以

    with
    context(如 with tempfile.Temporary() as wf: …) 的形式使用
    tempfile
    , 因为在这种情况下执行
    file.write
    (或类似操作)之后会自动关闭 fp 导致
    send_file
    时遇到 的错误(有一点我无法确认, 在 flask
    send_file
    完成之后是否会自动 close file)

    send closed file

  2. 当执行完

    file.write
    (或类似操作)之后, 需要使用
    file.seek(0)
    重置
    stream
    位置(原因在上一节最后有说明), 否则下载的文件将会是空文件

heading

实例

Online File Converter 是一个基于 flask 实现的站点, 这个站点的作用是将用户上传的

csv
文件合并后以
excel
的形式下载, 这是一个关于 flask 文件上传和下载的完整例子.

heading

参考资料

  1. 发送表单数据
  2. HTTP MIME types
  3. HTTP Header: Content-Disposition
  4. Flask: FileStorage
  5. Flask: demo of file upload