在Flask视图更新时显示数据流

Display data streamed from a Flask view as it updates

本文关键字:显示 数据流 更新 Flask 视图      更新时间:2023-09-26

我有一个生成数据并实时流式传输的视图。我不知道如何将这些数据发送到我可以在HTML模板中使用的变量。我目前的解决方案只是在数据到达时将其输出到空白页面,这是可行的,但是我想将其包含在一个更大的具有格式的页面中。当数据流到页面时,如何更新、格式化和显示数据?

import flask
import time, math
app = flask.Flask(__name__)
@app.route('/')
def index():
    def inner():
        # simulate a long process to watch
        for i in range(500):
            j = math.sqrt(i)
            time.sleep(1)
            # this value should be inserted into an HTML template
            yield str(i) + '<br/>'n'
    return flask.Response(inner(), mimetype='text/html')
app.run(debug=True)

你可以在响应中流式传输数据,但是你不能按照你描述的方式动态更新模板。模板在服务器端呈现一次,然后发送到客户端。

一种解决方案是使用JavaScript读取流响应并在客户端输出数据。使用XMLHttpRequest向将流式传输数据的端点发出请求。然后定期从流中读取,直到读取完成。

这引入了复杂性,但允许直接更新页面,并完全控制输出的样子。下面的示例通过同时显示当前值和所有值的日志来演示这一点。

这个例子假设了一个非常简单的消息格式:一行数据,后跟一个换行符。只要有一种方法来识别每个消息,这可以根据需要变得尽可能复杂。例如,每个循环可以返回一个客户端解码的JSON对象。

from math import sqrt
from time import sleep
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index():
    return render_template("index.html")
@app.route("/stream")
def stream():
    def generate():
        for i in range(500):
            yield "{}'n".format(sqrt(i))
            sleep(1)
    return app.response_class(generate(), mimetype="text/plain")
<p>This is the latest output: <span id="latest"></span></p>
<p>This is all the output:</p>
<ul id="output"></ul>
<script>
    var latest = document.getElementById('latest');
    var output = document.getElementById('output');
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '{{ url_for('stream') }}');
    xhr.send();
    var position = 0;
    function handleNewData() {
        // the response text include the entire response so far
        // split the messages, then take the messages that haven't been handled yet
        // position tracks how many messages have been handled
        // messages end with a newline, so split will always show one extra empty message at the end
        var messages = xhr.responseText.split(''n');
        messages.slice(position, -1).forEach(function(value) {
            latest.textContent = value;  // update the latest value in place
            // build and append a new item to a list to log all output
            var item = document.createElement('li');
            item.textContent = value;
            output.appendChild(item);
        });
        position = messages.length - 1;
    }
    var timer;
    timer = setInterval(function() {
        // check the response for new data
        handleNewData();
        // stop checking once the response has ended
        if (xhr.readyState == XMLHttpRequest.DONE) {
            clearInterval(timer);
            latest.textContent = 'Done';
        }
    }, 1000);
</script>

一个<iframe>可以用来显示流HTML输出,但它有一些缺点。框架是一个单独的文档,这增加了资源的使用。由于它只显示流数据,因此可能不容易像页面的其他部分那样设置样式。它只能附加数据,因此长输出将呈现在可见滚动区域下方。它不能修改页面的其他部分来响应每个事件。

index.html以指向stream端点的帧呈现页面。该框架具有相当小的默认尺寸,因此您可能希望进一步设置它的样式。使用render_template_string(它知道转义变量)来呈现每个条目的HTML(或者使用render_template和更复杂的模板文件)。一个初始行可以用来首先在框架中加载CSS。

from flask import render_template_string, stream_with_context
@app.route("/stream")
def stream():
    @stream_with_context
    def generate():
        yield render_template_string('<link rel=stylesheet href="{{ url_for("static", filename="stream.css") }}">')
        for i in range(500):
            yield render_template_string("<p>{{ i }}: {{ s }}</p>'n", i=i, s=sqrt(i))
            sleep(1)
    return app.response_class(generate())
<p>This is all the output:</p>
<iframe src="{{ url_for("stream") }}"></iframe>

5年晚了,但这实际上可以做到的方式,你最初试图这样做,javascript是完全不必要的(编辑:接受的答案的作者添加了iframe部分后,我写了这个)。您只需要将输出嵌入为<iframe>:

from flask import Flask, render_template, Response
import time, math
app = Flask(__name__)
@app.route('/content')
def content():
    """
    Render the content a url different from index
    """
    def inner():
        # simulate a long process to watch
        for i in range(500):
            j = math.sqrt(i)
            time.sleep(1)
            # this value should be inserted into an HTML template
            yield str(i) + '<br/>'n'
    return Response(inner(), mimetype='text/html')

@app.route('/')
def index():
    """
    Render a template at the index. The content will be embedded in this template
    """
    return render_template('index.html.jinja')
app.run(debug=True)

然后是'index.html. html。jinja'文件将包含一个<iframe>,其内容url作为src,类似于:

<!doctype html>
<head>
    <title>Title</title>
</head>
<body>
    <div>
        <iframe frameborder="0" 
                onresize="noresize"
                style='background: transparent; width: 100%; height:100%;' 
                src="{{ url_for('content')}}">
        </iframe>
    </div>
</body>

在呈现用户提供的数据时,应该使用render_template_string()来呈现内容,以避免注入攻击。然而,我把这一点排除在例子之外,因为它增加了额外的复杂性,超出了问题的范围,与OP无关,因为他不是流用户提供的数据,并且与绝大多数看到这篇文章的人无关,因为流用户提供的数据是一个非常边缘的情况,很少有人会这样做。

最初我有一个类似的问题张贴在这里,一个模型正在训练和更新应该是固定的和Html格式。下面的答案是为将来的参考或人们试图解决同样的问题,需要灵感。

实现这一点的一个很好的解决方案是在Javascript中使用EventSource,如下所述。可以使用上下文变量启动此侦听器,例如从表单或其他源启动。通过发送stop命令来停止侦听器。在本例中,sleep命令用于可视化,而不做任何实际工作。最后,Html格式化可以使用Javascript DOM-Manipulation来实现。

应用<<h3>瓶/h3>
import flask
import time
app = flask.Flask(__name__)
@app.route('/learn')
def learn():
    def update():
        yield 'data: Prepare for learning'n'n'
        # Preapre model
        time.sleep(1.0)
        for i in range(1, 101):
            # Perform update
            time.sleep(0.1)
            yield f'data: {i}%'n'n'
        yield 'data: close'n'n'
    return flask.Response(update(), mimetype='text/event-stream')

@app.route('/', methods=['GET', 'POST'])
def index():
    train_model = False
    if flask.request.method == 'POST':
        if 'train_model' in list(flask.request.form):
            train_model = True
    return flask.render_template('index.html', train_model=train_model)
app.run(threaded=True)

HTML模板

<form action="/" method="post">
    <input name="train_model" type="submit" value="Train Model" />
</form>
<p id="learn_output"></p>
{% if train_model %}
<script>
    var target_output = document.getElementById("learn_output");
    var learn_update = new EventSource("/learn");
    learn_update.onmessage = function (e) {
        if (e.data == "close") {
            learn_update.close();
        } else {
            target_output.innerHTML = "Status: " + e.data;
        }
    };
</script>
{% endif %}