zoukankan      html  css  js  c++  java
  • flask 文件上传(单文件上传、多文件上传)--

     

    文件上传

    在HTML中,渲染一个文件上传字段只需要将<input>标签的type属性设为file,即<input type=”file”>。

    这会在浏览器中渲染成一个文件上传字段,单击文件选择按钮会打开文件选择窗口,选择对应的文件后,被选择的文件名会显示在文件选择按钮旁边。

    在服务器端,可以和普通数据一样获取上传文件数据并保存。不过需要考虑安全问题,文件上传的漏洞也是比较流行的攻击方式。除了常规的CSRF防范,我们还需要重点关注这几个问题:验证文件类型、验证文件大小、过滤文件名

    定义上传表单

    在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()

    在表单类UploadForm()中创建了一个FileField类的photo字段,用来上传图片。

    和其他字段类似,需要对文件上传字段进行验证。Flask-WTF在flask_wtf.file模块下提供了两个文件相关的验证器,用法如下:

    我们使用FileRequired确保提交的表单字段中包含文件数据。处于安全考虑,必须对上传的文件类型进行限制。如果用户可以上传HTML文件,而且我们同时提供了视图函数获取上传后的文件,那么很容易导致XSS攻击。使用FileAllowed设置允许的文件类型,传入一个包含允许文件类型的后缀名列表。

    Flask-WTF提供的FileAllowed是在服务器端验证上传文件,使用HTML5中的accept属性也可以在客户端实现简单的类型过滤。这个属性接收MIME类型字符串或文件格式后缀,多个值之间使用逗号分隔,比如:

    <input type=”file” id=”profile_pic” name=”profile_pic” accept=”.jpg, .jpeg, .png, .gif”>

    当用户单击文件选择按钮后,打开的文件选择窗口会默认将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:flaskFLASK_PRACTICEform

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

  • 相关阅读:
    URAL 2067 Friends and Berries (推理,数学)
    URAL 2070 Interesting Numbers (找规律)
    URAL 2073 Log Files (模拟)
    URAL 2069 Hard Rock (最短路)
    URAL 2068 Game of Nuts (博弈)
    URAL 2066 Simple Expression (水题,暴力)
    URAL 2065 Different Sums (找规律)
    UVa 1640 The Counting Problem (数学,区间计数)
    UVa 1630 Folding (区间DP)
    UVa 1629 Cake slicing (记忆化搜索)
  • 原文地址:https://www.cnblogs.com/xiaxiaoxu/p/10549485.html
Copyright © 2011-2022 走看看