Python Flask 应用程序:在函数运行时更新进度条
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/24251898/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Flask App: Update progress bar while function runs
提问by FreshCrichard
I'm building a fairly simple WebApp in Flask that performs functions via a website's API. My users fill out a form with their account URL and API token; when they submit the form I have a python script that exports PDFs from their account via the API. This function can take a long time so I want to display a bootstrap progress bar on the form page indicating how far along in the process the script is. My question is how to I update the progress bar as the function is running? Here is a simplified version of what I'm talking about.
我正在 Flask 中构建一个相当简单的 WebApp,它通过网站的 API 执行功能。我的用户使用他们的帐户 URL 和 API 令牌填写表单;当他们提交表单时,我有一个 python 脚本,可以通过 API 从他们的帐户中导出 PDF。这个函数可能需要很长时间,所以我想在表单页面上显示一个引导程序进度条,指示脚本在进程中的进度。我的问题是如何在函数运行时更新进度条?这是我正在谈论的内容的简化版本。
views.py:
视图.py:
@app.route ('/export_pdf', methods = ['GET', 'POST'])
def export_pdf():
form = ExportPDF()
if form.validate_on_submit():
try:
export_pdfs.main_program(form.account_url.data,
form.api_token.data)
flash ('PDFs exported')
return redirect(url_for('export_pdf'))
except TransportException as e:
s = e.content
result = re.search('<error>(.*)</error>', s)
flash('There was an authentication error: ' + result.group(1))
except FailedRequest as e:
flash('There was an error: ' + e.error)
return render_template('export_pdf.html', title = 'Export PDFs', form = form)
export_pdf.html:
导出_pdf.html:
{% extends "base.html" %}
{% block content %}
{% include 'flash.html' %}
<div class="well well-sm">
<h3>Export PDFs</h3>
<form class="navbar-form navbar-left" action="" method ="post" name="receipt">
{{form.hidden_tag()}}
<br>
<div class="control-group{% if form.errors.account_url %} error{% endif %}">
<label class"control-label" for="account_url">Enter Account URL:</label>
<div class="controls">
{{ form.account_url(size = 50, class = "span4")}}
{% for error in form.errors.account_url %}
<span class="help-inline">[{{error}}]</span><br>
{% endfor %}
</div>
</div>
<br>
<div class="control-group{% if form.errors.api_token %} error{% endif %}">
<label class"control-label" for="api_token">Enter API Token:</label>
<div class="controls">
{{ form.api_token(size = 50, class = "span4")}}
{% for error in form.errors.api_token %}
<span class="help-inline">[{{error}}]</span><br>
{% endfor %}
</div>
</div>
<br>
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
<br>
<br>
<div class="progress progress-striped active">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<span class="sr-only"></span>
</div>
</form>
</div>
</div>
{% endblock %}
and export_pdfs.py:
和 export_pdfs.py:
def main_program(url, token):
api_caller = api.TokenClient(url, token)
path = os.path.expanduser('~/Desktop/'+url+'_pdfs/')
pdfs = list_all(api_caller.pdf.list, 'pdf')
total = 0
count = 1
for pdf in pdfs:
total = total + 1
for pdf in pdfs:
header, body = api_caller.getPDF(pdf_id=int(pdf.pdf_id))
with open('%s.pdf' % (pdf.number), 'wb') as f:
f.write(body)
count = count + 1
if count % 50 == 0:
time.sleep(1)
In that last function I have total the number of PDFs I will export, and have an ongoing count while it is processing. How can I send the current progress to my .html file to fit within the 'style=' tag of the progress bar? Preferably in a way that I can reuse the same tool for progress bars on other pages. Let me know if I haven't provided enough info.
在最后一个函数中,我有我将导出的 PDF 总数,并在处理过程中进行持续计数。如何将当前进度发送到我的 .html 文件以适应进度条的“style=”标签?最好是我可以在其他页面上为进度条重用相同的工具。如果我没有提供足够的信息,请告诉我。
回答by guneysus
I run this simple but educational Flask SSE implementation on localhost. To handle 3rd party (user uploaded) library in GAE:
我在本地主机上运行这个简单但具有教育意义的 Flask SSE 实现。在 GAE 中处理第 3 方(用户上传)库:
- Create a directory named
lib
in your root path. - copy
gevent
library directory tolib
directory. Add these lines to your
main.py
:import sys sys.path.insert(0,'lib')
Thats all. If you use
lib
directory from a child folder, use relative reference:sys.path.insert(0, ../../blablabla/lib')
- 创建一个以
lib
您的根路径命名的目录。 - 将
gevent
库目录复制到lib
目录。 将这些行添加到您的
main.py
:import sys sys.path.insert(0,'lib')
就这样。如果您使用
lib
子文件夹中的目录,请使用相对引用:sys.path.insert(0, ../../blablabla/lib')
From http://flask.pocoo.org/snippets/116/
来自http://flask.pocoo.org/snippets/116/
# author: [email protected]
#
# Make sure your gevent version is >= 1.0
import gevent
from gevent.wsgi import WSGIServer
from gevent.queue import Queue
from flask import Flask, Response
import time
# SSE "protocol" is described here: http://mzl.la/UPFyxY
class ServerSentEvent(object):
def __init__(self, data):
self.data = data
self.event = None
self.id = None
self.desc_map = {
self.data : "data",
self.event : "event",
self.id : "id"
}
def encode(self):
if not self.data:
return ""
lines = ["%s: %s" % (v, k)
for k, v in self.desc_map.iteritems() if k]
return "%s\n\n" % "\n".join(lines)
app = Flask(__name__)
subscriptions = []
# Client code consumes like this.
@app.route("/")
def index():
debug_template = """
<html>
<head>
</head>
<body>
<h1>Server sent events</h1>
<div id="event"></div>
<script type="text/javascript">
var eventOutputContainer = document.getElementById("event");
var evtSrc = new EventSource("/subscribe");
evtSrc.onmessage = function(e) {
console.log(e.data);
eventOutputContainer.innerHTML = e.data;
};
</script>
</body>
</html>
"""
return(debug_template)
@app.route("/debug")
def debug():
return "Currently %d subscriptions" % len(subscriptions)
@app.route("/publish")
def publish():
#Dummy data - pick up from request for real data
def notify():
msg = str(time.time())
for sub in subscriptions[:]:
sub.put(msg)
gevent.spawn(notify)
return "OK"
@app.route("/subscribe")
def subscribe():
def gen():
q = Queue()
subscriptions.append(q)
try:
while True:
result = q.get()
ev = ServerSentEvent(str(result))
yield ev.encode()
except GeneratorExit: # Or maybe use flask signals
subscriptions.remove(q)
return Response(gen(), mimetype="text/event-stream")
if __name__ == "__main__":
app.debug = True
server = WSGIServer(("", 5000), app)
server.serve_forever()
# Then visit http://localhost:5000 to subscribe
# and send messages by visiting http://localhost:5000/publish
回答by Alvae
As some others suggested in the comments, the simplest solution is to run your exporting function in another thread, and let your client pull progress information with another request. There are multiple approaches to handle this particular task. Depending on your needs, you might opt for a more or less sophisticated one.
正如评论中其他一些人所建议的那样,最简单的解决方案是在另一个线程中运行您的导出函数,并让您的客户端通过另一个请求提取进度信息。有多种方法可以处理此特定任务。根据您的需要,您可以选择或多或少复杂的。
Here's a very (very) minimal example on how to do it with threads:
这是一个关于如何使用线程执行此操作的非常(非常)最小的示例:
import random
import threading
import time
from flask import Flask
class ExportingThread(threading.Thread):
def __init__(self):
self.progress = 0
super().__init__()
def run(self):
# Your exporting stuff goes here ...
for _ in range(10):
time.sleep(1)
self.progress += 10
exporting_threads = {}
app = Flask(__name__)
app.debug = True
@app.route('/')
def index():
global exporting_threads
thread_id = random.randint(0, 10000)
exporting_threads[thread_id] = ExportingThread()
exporting_threads[thread_id].start()
return 'task id: #%s' % thread_id
@app.route('/progress/<int:thread_id>')
def progress(thread_id):
global exporting_threads
return str(exporting_threads[thread_id].progress)
if __name__ == '__main__':
app.run()
In the index route (/) we spawn a thread for each exporting task, and we return an ID to that task so that the client can retrieve it later with the progress route (/progress/[exporting_thread]). The exporting thread updates its progress value every time it thinks it is appropriate.
在索引路由 (/) 中,我们为每个导出任务生成一个线程,并为该任务返回一个 ID,以便客户端稍后可以使用进度路由 (/progress/[exporting_thread]) 检索它。导出线程每次认为合适时都会更新其进度值。
On the client side, you would get something like this (this example uses jQuery):
在客户端,你会得到这样的东西(这个例子使用 jQuery):
function check_progress(task_id, progress_bar) {
function worker() {
$.get('progress/' + task_id, function(data) {
if (progress < 100) {
progress_bar.set_progress(progress)
setTimeout(worker, 1000)
}
})
}
}
As said, this example is very minimalistic and you should probably go for a slightly more sophisticated approach. Usually, we would store the progress of a particular thread in a database or a cache of some sort, so that we don't rely on a shared structure, hence avoiding most of the memory and concurrency issues my example has.
如上所述,这个示例非常简约,您可能应该采用稍微复杂一些的方法。通常,我们会将特定线程的进度存储在数据库或某种缓存中,这样我们就不会依赖共享结构,从而避免我的示例中的大部分内存和并发问题。
Redis (https://redis.io) is an in-memory database store that is generally well-suited for this kind of tasks. It integrates ver nicely with Python (https://pypi.python.org/pypi/redis).
Redis ( https://redis.io) 是一种内存数据库存储,通常非常适合此类任务。它与 Python ( https://pypi.python.org/pypi/redis)很好地集成在一起。