如何在网页上为pandas操作制作进度条

11

我已经搜索了一段时间,但无法找到解决方法。我有一个简单的Flask应用程序,它接收CSV文件,将其读入Pandas数据框中,转换并输出为新的CSV文件。我已经成功地使用HTML上传和转换了它。

<div class="container">
  <form method="POST" action="/convert" enctype="multipart/form-data">
    <div class="form-group">
      <br />
      <input type="file" name="file">
      <input type="submit" name="upload"/>
    </div>
  </form>
</div>

当我点击提交后,它会在后台运行一段时间并在完成后自动触发下载。负责接收result_df并触发下载的代码如下:

@app.route('/convert', methods=["POST"])
def convert(
  if request.method == 'POST':
    # Read uploaded file to df
    input_csv_f = request.files['file']
    input_df = pd.read_csv(input_csv_f)
    # TODO: Add progress bar for pd_convert
    result_df = pd_convert(input_df)
    if result_df is not None:
      resp = make_response(result_df.to_csv())
      resp.headers["Content-Disposition"] = "attachment; filename=export.csv"
      resp.headers["Content-Type"] = "text/csv"
      return resp

我想为 pd_convert 添加一个进度条,它本质上是一个 Pandas 应用操作。我发现 tqdm 现在可以与 Pandas 一起工作,并且它有一个 progress_apply 方法,而不是 apply。但我不确定它是否适用于在网页上制作进度条。我猜应该可以,因为它在 Jupyter 笔记本上运行。这里如何为 pd_convert() 添加进度条?
我想要的最终结果是:
  1. 用户点击上传,从他们的文件系统中选择 CSV 文件
  2. 用户点击提交
  3. 进度条开始运行
  4. 一旦进度条达到100%,就会触发下载
1和2现在已完成。然后下一个问题是如何触发下载。目前,我的 convert 函数可以轻松触发下载,因为响应是使用文件形成的。如果我想呈现页面,我使用 return render_template(...) 形成响应。由于我只能有一个响应,所以是否可能通过对 /convert 的一次调用来实现3和4?
我不是 Web 开发人员,仍在学习基础知识。先感谢您!

====编辑====

我尝试了这里的示例(链接)并进行了一些修改。我从数据帧上的行索引中获取进度,并将其放入 Redis 中。客户端通过请求此新端点 /progress 从流中从 Redis 获取进度。类似于:

@app.route('/progress')
def progress():
  """Get percentage progress for the dataframe process"""
  r = redis.StrictRedis(
    host=redis_host, port=redis_port, password=redis_password, decode_responses=True)
  r.set("progress", str(0))
  # TODO: Problem, 2nd submit doesn't clear progress to 0%. How to make independent progress for each client and clear to 0% on each submit
  def get_progress():

    p = int(r.get("progress"))
    while p <= 100:
      p = int(r.get("progress"))
      p_msg = "data:" + str(p) + "\n\n"
      yield p_msg
      logging.info(p_msg)
      if p == 100:
        r.set("progress", str(0))
      time.sleep(1)

  return Response(get_progress(), mimetype='text/event-stream')

目前它能够运行但存在一些问题。原因肯定是我对这个解决方案的理解不足。

问题:

  • 每次按下submit按钮时,我需要将进度重置为0。我尝试了几个地方将其重置为0,但还没有找到可行的版本。这肯定与我对流如何工作的理解有关。现在只有在刷新页面时才会重置。
  • 如何处理并发请求,也就是Redis竞争条件?如果多个用户同时发出请求,则每个用户的进度应独立。我考虑为每个submit事件提供一个随机的job_id,并将其作为Redis中的键。由于每个作业完成后我都不需要该条目,所以完成后我将删除该条目。

我感觉我缺少的部分是对text/event-stream的理解。我觉得我离一个可行的解决方案很近了。请分享您对正确操作方式的看法。我只是在猜测并尝试组合一些东西,以满足我的非常有限的理解。

1个回答

9

好的,我缩小了错过的问题并解决了。我需要的概念包括:

后端

  • Redis作为键值数据库存储进度,可以通过端点/progress查询事件流(HTML5)
  • 服务器发送事件(SSE)用于流式传输进度:text/event-stream MIME类型响应
  • Flask应用程序中的Python生成器用于SSE
  • 将Pandas数据帧上正在处理的行索引的进度写入Redis

前端

  • 打开事件流:通过HTML按钮从客户端触发SSE
  • 关闭事件流:一旦事件数据达到100%
  • 使用jQuery动态更新事件流的进度条

示例HTML

  <script>
  function getProgress() {
    var source = new EventSource("/progress");
    source.onmessage = function(event) {
      $('.progress-bar').css('width', event.data+'%').attr('aria-valuenow', event.data);
      $('.progress-bar-label').text(event.data+'%');

      // Event source closed after hitting 100%
      if(event.data == 100){
        source.close()
      }
    }
  }
  </script>

  <body>
    <div class="container">
      ...
      <form method="POST" action="/autoattr" enctype="multipart/form-data">
        <div class="form-group">
        ...
          <input type="file" name="file">
          <input type="submit" name="upload" onclick="getProgress()" />
        </div>
      </form>

      <div class="progress" style="width: 80%; margin: 50px;">
        <div class="progress-bar progress-bar-striped active"
          role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
          <span class="progress-bar-label">0%</span>
        </div>
      </div>
    </div>
  </body>

示例后端Flask代码

redis_host = "localhost"
redis_port = 6379
redis_password = ""
r = redis.StrictRedis(
  host=redis_host, port=redis_port, password=redis_password, decode_responses=True)

@app.route('/progress')
def progress():
  """Get percentage progress for auto attribute process"""
  r.set("progress", str(0))
  def progress_stream():
    p = int(r.get("progress"))
    while p < 100:
      p = int(r.get("progress"))
      p_msg = "data:" + str(p) + "\n\n"
      yield p_msg
      # Client closes EventSource on 100%, gets reopened when `submit` is pressed
      if p == 100:
        r.set("progress", str(0))
      time.sleep(1)

  return Response(progress_stream(), mimetype='text/event-stream')

剩下的是Pandas for循环写入Redis的代码。
我从谷歌搜索了几个小时,整理了很多结果,所以我觉得最好在这里记录下来,为那些也需要在Flask Web应用程序中为Pandas数据帧处理添加进度条的人提供基本功能。
一些有用的参考资料
https://medium.com/code-zen/python-generator-and-html-server-sent-events-3cdf14140e56https://codeburst.io/polling-vs-sse-vs-websocket-how-to-choose-the-right-one-1859e4e13bd9What are Long-Polling, Websockets, Server-Sent Events (SSE) and Comet?

嗨,Logan,非常感谢你写下这篇文章!我尝试了你的解决方案,它适用于一个相似的用例。然而,一旦处理函数开始运行,进度函数就会停止。你是否遇到了类似的执行并发问题? - user3240855
@user3240855 你好,是的,我也遇到了完全相同的问题。很抱歉那是一段时间之前的事情,我很后悔没有在这里记录下解决方案。当时我正在使用gunicorn和4个worker,它们之间存在某种竞争关系。我忘记了我具体做了什么,但解决方法很简单。我记得的一件事是我没有使用Redis,而只是在内存中使用了一个字典,因为这是一个非常小的应用程序。 - Logan Yang
1
嗨,感谢您的回复。我实际上已经在我的问题上开了一个问题(https://stackoverflow.com/questions/59195668/flask-server-sent-event-sse-stream-function-is-halted-when-processing-input),但是我可以自己回答:我发现问题似乎是特定于浏览器(Firefox),因为它可以与例如Chrome一起工作。 - user3240855

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接