在更新时显示从 Flask 视图流式传输的数据
Display data streamed from a Flask view as it updates
我有一个生成数据并实时流式传输的视图。我不知道如何将此数据发送到我可以在我的 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
向将流式传输数据的端点发出请求。然后定期从流中读取直到完成。
这会带来复杂性,但允许直接更新页面并完全控制输出的外观。以下示例通过显示当前值和所有值的日志来演示。
此示例采用非常简单的消息格式:单行数据,后跟换行符。这可以根据需要变得非常复杂,只要有一种方法可以识别每条消息。例如,每个循环可以 return 客户端解码的 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.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 无关,因为他没有流式传输用户提供的数据,并且与广大用户无关大多数人都看到了这一点 post,因为流式传输用户提供的数据是一种极端情况,很少有人会不得不这样做。
最初我遇到了与 here 发布的问题类似的问题,其中正在训练模型并且更新应该是固定的并在 Html 中格式化。以下答案供日后参考或正在尝试解决相同问题并需要启发的人参考。
实现此目的的一个很好的解决方案是使用 Javascript 中的 EventSource,如 here 所述。可以使用上下文变量启动此侦听器,例如从表单或其他来源。通过发送停止命令停止侦听器。在这个例子中,睡眠命令用于可视化,没有做任何实际工作。最后,Html 格式化可以使用 Javascript DOM-Manipulation.
来实现
烧瓶应用程序
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 %}
我有一个生成数据并实时流式传输的视图。我不知道如何将此数据发送到我可以在我的 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
向将流式传输数据的端点发出请求。然后定期从流中读取直到完成。
这会带来复杂性,但允许直接更新页面并完全控制输出的外观。以下示例通过显示当前值和所有值的日志来演示。
此示例采用非常简单的消息格式:单行数据,后跟换行符。这可以根据需要变得非常复杂,只要有一种方法可以识别每条消息。例如,每个循环可以 return 客户端解码的 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.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 无关,因为他没有流式传输用户提供的数据,并且与广大用户无关大多数人都看到了这一点 post,因为流式传输用户提供的数据是一种极端情况,很少有人会不得不这样做。
最初我遇到了与 here 发布的问题类似的问题,其中正在训练模型并且更新应该是固定的并在 Html 中格式化。以下答案供日后参考或正在尝试解决相同问题并需要启发的人参考。
实现此目的的一个很好的解决方案是使用 Javascript 中的 EventSource,如 here 所述。可以使用上下文变量启动此侦听器,例如从表单或其他来源。通过发送停止命令停止侦听器。在这个例子中,睡眠命令用于可视化,没有做任何实际工作。最后,Html 格式化可以使用 Javascript DOM-Manipulation.
来实现烧瓶应用程序
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 %}