Flask 在流式传输数据后加载新页面

Flask Load New Page After Streaming Data

提问人:sushi 提问时间:7/23/2022 更新时间:11/25/2022 访问量:974

问:

我有一个简单的 Flask 应用程序,它接受 CSV 上传,进行一些更改,并将结果作为 CSV 流式传输回用户的下载文件夹。

HTML 表单

<form action = {{uploader_page}} method = "POST" enctype = "multipart/form-data">
    <label>CSV file</label><br>
    <input type = "file" name = "input_file" required></input><br><br>
    <!-- some other inputs -->
    <div id = "submit_btn_container">
        <input id="submit_btn" onclick = "this.form.submit(); this.disabled = true; this.value = 'Processing';" type = "submit"></input>
    </div>
</form>

from flask import Flask, request, Response, redirect, flash, render_template
from io import BytesIO
import pandas as pd

@app.route('/uploader', methods = ['POST'])
def uploadFile():
    uploaded_file = request.files['input_file']
    data_df = pd.read_csv(BytesIO(uploaded_file.read()))
    # do stuff
    
    # stream the pandas df as a csv to the user download folder
    return Response(data_df.to_csv(index = False),
                            mimetype = "text/csv",
                            headers = {"Content-Disposition": "attachment; filename=result.csv"})

这很好用,我在下载文件夹中看到了该文件。

但是,我想在完成后显示“下载完成”页面。

我该怎么做?通常我习惯于更改页面。return redirect("some_url")

python html flask Web 应用程序 httpresponse

评论


答:

0赞 jak bin 7/28/2022 #1

以下是一些更改。

在输入 onclick 事件中设置。window.open('')

HTML 表单

<form action ="/uploader" method = "POST" enctype = "multipart/form-data">
        <label>CSV file</label><br>
        <input type = "file" name = "input_file" required></input><br><br>
        <!-- some other inputs -->
        <div id = "submit_btn_container">
            <input id="submit_btn" onclick = "this.form.submit(); this.disabled = true; this.value = 'Processing'; window.open('your_url');" type = "submit"></input>
        </div>
</form>

评论

0赞 sushi 7/29/2022
当我尝试时,这会打开一个带有页面的新选项卡。是否可以重定向当前页面?
0赞 jak bin 7/29/2022
您正在将文件返回给用户。如果在同一页面上重定向用户,则用户未获得文件。
4赞 Jonathan Ciapetti 7/29/2022 #2

请考虑使用 send_file() 或 send_from_directory() 发送文件。

从 1 个请求中获得 2 个响应是不可能的,但您可以借助一些 JS 将问题拆分为块,遵循这个简单的图表(不是很精确的 UML,但仅此而已):

此图引用了代码的先前和更简单的版本,该版本后来在 OP 要求调用 flash() 后进行了更新

enter image description here

  1. POST 通过从表单调用的函数 ,这样除了保存文件之外,您还可以在那里有一些逻辑,例如检查响应状态/uploaderonsubmit

  2. 处理文件(我通过以下方式对您的处理进行了模拟upper())

  3. 如果服务器响应 (“Created”),那么您可以保存文件并打印 “Download Complete”(我使用了它,因为它只有一个标签,我们可以替换所有以前的 DOM;它不应该用于更改复杂的 HTML)201window.document.body.innerHTML

  4. 否则,如果服务器使用其他状态代码(如 )进行响应,则 POST 以获取要呈现的新 - 可能闪烁的 - HTML。图中未显示 POST 步骤。500/something-went-wrong

要测试错误页面,请在内部处理中犯一些语法错误,例如 aread()upload_file()data_df = pd.read_csv(BytesIO(uploaded_file.))

在响应中,我添加了一个 CSP 标头来缓解可能的恶意攻击,因为我们无法足够信任用户。something-went-wrong

代码如下:

main.py

from flask import (Flask,
                   request,
                   redirect,
                   render_template,
                   send_file,
                   url_for,
                   Response, jsonify, flash, make_response)
from flask_wtf.csrf import CSRFProtect

from io import BytesIO
import pandas as pd

app = Flask(__name__)
app.secret_key = "your_secret"

csrf = CSRFProtect(app)


@app.route('/')
def index():
    return render_template("index.html")


@app.route("/uploader", methods=['POST'])
def upload_file():
    try:
        uploaded_file = request.files['input_file']
        data_df = pd.read_csv(BytesIO(uploaded_file.read()))

        # do stuff
        data_df["foo"] = data_df["foo"].str.upper()

        # Stream buffer:
        io_buffer = BytesIO()
        data_df.to_csv(io_buffer)
        io_buffer.seek(0)

    except Exception as ex:
        print(ex)  # and log if needed
        # Here I return the exception string just as an example! Not good in real usage.
        return jsonify({"message": str(ex)}), 500
    else:
        return send_file(io_buffer,
                         download_name="result.csv",
                         as_attachment=True), 201


@app.route("/something-went-wrong", methods=["POST"])
def something_went_wrong():
    flash(request.get_json()["message"])
    response = make_response(render_template("something-went-wrong.html"), 200)
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response

带有 JS 处理程序的表单

<form id="myForm" enctype="multipart/form-data" onsubmit="return submitHandler()">
    <input type="hidden" name="csrfToken" value="{{ csrf_token() }}"/>
    <label>CSV file</label><br>
    <input type="file" id="inputFile" name="input_file" required/><br><br>
    <!-- some other inputs -->
    <div id="submitBtnContainer">
        <input id="submitBtn" type="submit"/>
    </div>
</form>

<script>
    function submitHandler() {
        const csrf_token = "{{ csrf_token() }}";

        let formData = new FormData();
        const file = document.getElementById('inputFile').files[0];
        formData.append("input_file", file);

        fetch("/uploader", {
            method: "POST",
            body: formData,
            headers: {
                "X-CSRFToken": csrf_token,
            },
        })
        .then(response => {
            if (response.status != 201) {
                response.json().then(data => {
                    fetch("/something-went-wrong", {
                        method: "POST",
                        body: JSON.stringify({"message": data["message"]}),
                        headers: {
                            "Content-Type": "application/json",
                            "X-CSRFToken": csrf_token,
                        },
                    })
                    .then(response => response.text())
                    .then(text => {
                        window.document.body.innerHTML = text;
                    })
                });
            }
            else {
                return response.blob().then(blob => {
                    const file = new Blob([blob], { type: 'text/csv' });
                    const fileURL = URL.createObjectURL(file);
                    let fileLink = document.createElement('a');
                    fileLink.href = fileURL;
                    fileLink.download = "result.csv";
                    fileLink.click();
                    window.document.body.innerHTML = "<h1>Download Complete</h1>";
                });
            }
        })
        return false;
    }
</script>

为了完整起见,我的虚拟 csv :"file.csv"

酒吧

评论

1赞 sushi 8/3/2022
效果很好!我最终使用“var formData = new FormData(document.getElementById(”blur_form“)))将所有表单数据传入,但除此之外,这按预期工作。
0赞 sushi 8/4/2022
我想知道如何使用这种方法传递错误信息?以前我使用“flash(some_error) return redirect(same_page)”,但现在由于 xhr.onreadystatechange 函数正在处理请求和重定向,我的 flash 消息没有显示出来。
0赞 Jonathan Ciapetti 8/5/2022
您可以通过多种方式进行操作。在一些后续 POST 的帮助下,我编辑了我的答案,向您展示了一种方法:我使用它们来携带一条消息(请参阅从 中的 except 块返回的值),该消息将被永久闪烁。我还更喜欢从 AJAX 切换到 API 来完成所有这些工作,并且我直接使用 JS 呈现“下载完成”,而不使用 Flask 路由。upload_file()fetch
0赞 Olney1 8/2/2022 #3

您需要两个函数,一个用于处理 uploadFile() 等处理,另一个用于在同一应用路由中返回渲染模板。

当 uploadFile() 函数完成时:completed = True

然后,编写另一个函数来测试全局变量以返回渲染模板。if completed:

请参见:如何在 Flask 中对多个函数使用相同的路由

最后,使用 Jinja2 将一个变量返回到页面,并使用 Javascript 确定该变量是否存在,以通过 Javascript 加载“下载完成”页面。

蟒:

    from flask import Flask, request, Response, redirect, flash, render_template
    from io import BytesIO
    import pandas as pd

    completed = False
    
    @app.route('/uploader', methods = ['POST'])
    def uploadFile():
        uploaded_file = request.files['input_file']
        data_df = pd.read_csv(BytesIO(uploaded_file.read()))
        # do stuff
        # When stuff is done
        global completed
        completed = True
        # stream the pandas df as a csv to the user download folder
        return Response(data_df.to_csv(index = False),
                                mimetype = "text/csv",
                                headers = {"Content-Disposition": "attachment; filename=result.csv"})
                                

如何加载新页面:https://www.geeksforgeeks.org/how-can-a-page-be-forced-to-load-another-page-in-javascript/

Javascript 条件:https://www.w3docs.com/learn-javascript/conditional-operators-if.html

使用 Jinja2 渲染变量:https://jinja.palletsprojects.com/en/3.0.x/templates/

此外,您实际上应该用 try 和 except 包装您的 uploadFile() 函数以捕获上传错误。