Mostrar datos transmitidos desde una vista de Flask a medida que se actualiza
Tengo una vista que genera datos y los transmite en tiempo real. No sé cómo enviar estos datos a una variable que pueda usar en mi plantilla HTML. Mi solución actual simplemente envía los datos a una página en blanco a medida que llegan, lo cual funciona, pero quiero incluirlos en una página más grande con formato. ¿Cómo actualizo, formateo y visualizo los datos a medida que se transmiten a la página?
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)
Puede transmitir datos en una respuesta, pero no puede actualizar dinámicamente una plantilla de la forma que describe. La plantilla se representa una vez en el lado del servidor y luego se envía al cliente.
Una solución es utilizar JavaScript para leer la respuesta transmitida y generar los datos en el lado del cliente. Úselo XMLHttpRequest
para realizar una solicitud al punto final que transmitirá los datos. Luego lea periódicamente desde la transmisión hasta que esté listo.
Esto introduce complejidad, pero permite actualizar la página directamente y brinda control total sobre el aspecto del resultado. El siguiente ejemplo lo demuestra mostrando tanto el valor actual como el registro de todos los valores.
Este ejemplo asume un formato de mensaje muy simple: una sola línea de datos, seguida de una nueva línea. Esto puede ser tan complejo como sea necesario, siempre que haya una manera de identificar cada mensaje. Por ejemplo, cada bucle podría devolver un objeto JSON que el cliente decodifica.
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>
Se <iframe>
puede utilizar un para mostrar la salida HTML transmitida, pero tiene algunas desventajas. El marco es un documento separado, lo que aumenta el uso de recursos. Dado que solo muestra los datos transmitidos, puede que no sea fácil diseñarlo como el resto de la página. Solo puede agregar datos, por lo que la salida larga se mostrará debajo del área de desplazamiento visible. No puede modificar otras partes de la página en respuesta a cada evento.
index.html
representa la página con un marco apuntando al stream
punto final. El marco tiene dimensiones predeterminadas bastante pequeñas, por lo que es posible que desees darle más estilo. Utilice render_template_string
, que sabe escapar de las variables, para representar el HTML de cada elemento (o utilícelo render_template
con un archivo de plantilla más complejo). Se puede generar una línea inicial para cargar CSS en el marco primero.
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 años de retraso, pero esto en realidad se puede hacer de la forma en que intentaste hacerlo inicialmente, javascript es totalmente innecesario (Editar: el autor de la respuesta aceptada agregó la sección iframe después de que escribí esto). Solo tienes que incluir incrustar la salida como <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)
Luego, el archivo 'index.html.jinja' incluirá una <iframe>
URL con el contenido como src, que sería algo como:
<!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()
Al renderizar , se deben utilizar datos proporcionados por el usuario para renderizar el contenido para evitar ataques de inyección. Sin embargo, dejé esto fuera del ejemplo porque agrega complejidad adicional, está fuera del alcance de la pregunta, no es relevante para el OP ya que no está transmitiendo datos proporcionados por el usuario y no será relevante para la gran mayoría. La mayoría de las personas ven esta publicación, ya que la transmisión de datos proporcionados por los usuarios es un caso extremo que pocas personas, si es que alguna, tendrán que hacer.
Originalmente tuve un problema similar al publicado aquí donde se está entrenando un modelo y la actualización debería ser estacionaria y formateada en HTML. La siguiente respuesta es para referencia futura o para personas que intentan resolver el mismo problema y necesitan inspiración.
Una buena solución para lograr esto es utilizar un EventSource en Javascript, como se describe aquí . Este oyente se puede iniciar utilizando una variable de contexto, como desde un formulario u otra fuente. El oyente se detiene enviando un comando de parada. En este ejemplo, se utiliza un comando de suspensión para visualización sin realizar ningún trabajo real. Por último, el formato HTML se puede lograr mediante la manipulación DOM de Javascript.
Aplicación de matraz
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)
Plantilla 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 %}