使用flask将python脚本的stdout获取到flask网站

问题描述 投票:0回答:1

我想捕获长时间运行但冗长的脚本(目前是 pytest)的输出,并在网页上打印进度。我在这里面临两个问题

首先:我需要异步运行脚本并在脚本仍在运行时捕获输出。我发现 pytest 有

--capture=tee-sys
标志,我想这是为了这个目的,但我不知道它是如何工作的。

更通用的方法是使用

subprocess.Popen
,它似乎已经处理了异步部分并且可以通过管道传输输出。但是,如果我使用
Popen.communicat()
捕获,我要么阻塞线程,直到进程完成并且无法实时更新,要么我必须不断捕获
TimeoutExpired
异常并重新启动通信,这感觉像是一种解决方法,但是可能会起作用。但有没有更简单的方法来实现这一目标?

第二:我需要在没有收到请求的情况下更新前端。起初,我什至认为拥有终端样式输出会很酷,并发现了像 ttyd 这样的东西,但这似乎有点过分了,如果我没有记错的话,将允许用户输入到终端,这是我不想要的。现在我还发现了这个答案,建议使用 iframe 和

flask 文档中的 
stream_template_context(),但是如果我在这两种情况下都没有弄错的话,似乎我需要在使用从子流程收集的数据运行,感觉很容易出错。我还发现了 flask-socketio,这是我想避免的,因为 falsk 服务器应该在自定义 Linux 上运行,我需要首先添加这个模块。这最终应该是可行的,但如果有更简单的方法,我会更喜欢。我考虑过从前端 JavaScript 进行轮询的一个选项,但这看起来又是一个解决方法。这里最好的做法是什么?

对于这两部分,我很高兴有更多资源来阅读最佳实践等。

经过更多研究,我得到了一个像这样的最小工作示例:

from flask import Flask, Response, render_template from subprocess import Popen, PIPE app = Flask(__name__) @app.route('/content') def content(): # start subprocess def inner(): # proc = Popen(['/usr/local/bin/pytest', 'test1.py'], stdout=PIPE) with Popen(['/usr/local/bin/pytest', 'test1.py'], stdout=PIPE) as proc: while proc.poll() is None: line = proc.stdout.readline() # yield line.decode() + '<br/>\n' yield str(line) + '<br/>\n' return Response(inner(), mimetype='text/html') @app.route('/') def index(): return render_template('test.html') if __name__ == '__main__': app.run(debug=True)
其中

test.py

只是定义了一系列测试函数,每个函数都休眠
一秒钟和 
assert True
 只是为了获得所需的样本输出,
和一个
test.html

<!doctype html> <head> <title>Title</title> </head> <body> <div> <iframe frameborder="1" width="52%" height="500px" style='background: transparent;' src="{{ url_for('content')}}"> </iframe> </div> </body>
但是,

子进程文档指出,不鼓励像这样使用proc.stdout.readline()

,因为它可能会导致子进程死锁。
此外,在子进程终止后,我有时会得到一些,有时是很多空行。我使用 
str(line)
 使它们可见,因为字节字符串中的 
b''
 就在那里。
关于从这里去哪里有什么想法吗?

html python-3.x flask subprocess
1个回答
0
投票
我找到了两种解决方案,其中一种是我非常喜欢的,所以我要分享。

但首先:我想我误解了

子流程文档

中关于不使用
Popen.stdout.read()的警告。我只使用 readline
 我应该没问题,因为无论如何呼叫都应该返回。 (如果我在这里错了,请纠正我,因为整个答案是基于这样的假设,即 
readline
 不会在 
read
 的情况下陷入僵局。)

第一个解决方案:修改

这个答案。 实际上并不需要 <iframe>

,Flask 可以将数据流式传输到 
<div>
。 html 看起来像这样:

<p>Test Output</p> <div id="output"> {% if data %} {% for item in data %} {{ item }}<br /> {% endfor %} {% endif %} </div>

{% if data %}

 是可选的。我用它来加载带有空 
<div>
 的页面,然后按按钮重新加载网站并提供 
data
 来填充 
<div>

Python部分如下所示:

@app.route('/') def stream() -> Response: """stream data to template""" def update(task: str | list[str]): """update the stream data""" with Popen(task, stdout=PIPE, stderr=PIPE) as proc: while proc.poll() is None: line = proc.stdout.readline() log.info(line.decode()) yield line.decode() while (line := proc.stdout.readline()) != b'': log.info(line.decode()) yield line.decode() err = proc.stderr.read() # risk of deadlock # could be replaced by while readline as above if err != b'': log.warning("additionional stderr produced while running tests: %s", err.decode()) return Response( stream_template('index.html.jinja', data=update(['/usr/local/bin/pytest', 'dummytest.py'])))
我必须在 

proc.poll()

 返回后添加第二个 while 循环,因为我一直缺少最后一行数据。但我宁愿不单独使用第二个 while 循环,因为如果一行为空,这将有中断的风险(不应该发生,因为换行符应该始终存在)。

第二个解决方案基本上是

这个答案利用EventStream

。我很犹豫要不要尝试这个,因为我对 javascript 不太流利,但最终这就像一个魅力。我在按钮上添加了一个事件处理程序,阻止表单 
post
 发送到服务器,但手动打开 EventStream。 html 看起来像这样:

<div id="output"></div> <p><form action="/action" method="post" role="form" id="testsForm"> <button class="btn btn-primary" type="submit">Run Test</button> </form></p> <script type="text/javascript"> (() => { 'use strict' const form = document.getElementById("testsForm") form.addEventListener('submit', event => { event.preventDefault() var target = document.getElementById("output") var update = new EventSource("/action/stream") update.onmessage = function(e) { if (e.data == "close") { update.close(); } else { target.innerHTML += (e.data + '<br/>'); } }; }, false) })() </script>
表单中的 

action

method
 可能是完全可选的,因为表单永远不会传播到后端。 (这是使用 
bootstrap 类,如果有人想重新创建它)

这个Python看起来像这样:

@app.route("/action/stream") def stream() -> Response: """open event stream to client""" def update(task: str | list[str]): """update the event stream data""" with Popen(task, stdout=PIPE, stderr=PIPE) as proc: log.info("opened subprocess for async task communication") while proc.poll() is None: line = proc.stdout.readline() log.info(line.decode().rstrip()) # the '\n\n' is needed for termination in th frontend # the stream handling (especially closing) will break without it yield 'data: ' + line.decode() + "\n\n" while (line := proc.stdout.readline()) != b'': log.info(line.decode()) yield 'data: ' + line.decode() + "\n\n" err = proc.stderr.read() # risk of deadlock if err != b'': log.warning("additionional stderr produced while running script: %s", err.decode()) yield "data: close\n\n" return Response(update(['/usr/local/bin/pytest', 'dummytest.py']), mimetype="text/event-stream")
此解决方案的一大优点是,网站无需重新加载即可传输数据。我有一个空框,按下按钮时,脚本的输出将显示在屏幕上。 (

我什至找到了一些样式,让它看起来像一个老式学校显示器

在更长的通信方面:我在 pytest 脚本中的一次测试中以 10 分钟超时测试了这两个版本,并且都没有问题。

甚至可能有

另一种解决方案,但我从未尝试过这个,因为我不太明白。如果第二个解决方案由于某种原因还不够,我可能会在未来。

© www.soinside.com 2019 - 2024. All rights reserved.