【问题标题】:How to make a progress bar on a web page for pandas operation如何在网页上制作pandas操作的进度条
【发布时间】:2019-09-03 14:58:17
【问题描述】:

我已经在谷歌上搜索了一段时间,但无法找到一种方法来做到这一点。我有一个简单的 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 添加一个进度条,这本质上是一个熊猫应用操作。我发现 tqdm 现在可以与 pandas 一起使用,它有一个 progress_apply 方法而不是 apply。但我不确定它是否与在网页上制作进度条有关。我想应该是因为它适用于 Jupyter 笔记本。如何在此处为pd_convert() 添加进度条?

我想要的最终结果是:

  1. 用户点击上传,从他们的文件系统中选择 CSV 文件
  2. 用户点击提交
  3. 进度条开始运行
  4. 一旦进度条达到 100%,就会触发下载

1 和 2 现已完成。那么接下来的问题就是如何触发下载。现在,我的convert 函数可以毫无问题地触发下载,因为响应是由文件形成的。如果我想渲染页面,我会使用return render_template(...) 形成响应。由于我只能有一个响应,是否可以通过一个呼叫 /convert 来获得 3 和 4?

不是网络开发人员,仍在学习基础知识。提前致谢!


====编辑====

我尝试了示例here 并进行了一些修改。我从数据帧的 for 循环中的行索引中获取进度并将其放入 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的理解。感觉我接近一个可行的解决方案。请分享您对执行此操作的“正确”方法的看法。我只是在猜测并尝试在我的理解有限的情况下整理出一些有用的东西。

【问题讨论】:

    标签: python ajax pandas flask tqdm


    【解决方案1】:

    好的,我缩小了我遗漏的问题范围并弄清楚了。我需要的概念包括

    后台

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

    我从几个小时的谷歌搜索中拼凑了很多结果,所以我觉得最好在此处为也需要此基本功能的人记录:在 Flask Web 应用程序中添加一个进度条以进行 Pandas 数据帧处理。

    一些有用的参考资料

    https://medium.com/code-zen/python-generator-and-html-server-sent-events-3cdf14140e56

    https://codeburst.io/polling-vs-sse-vs-websocket-how-to-choose-the-right-one-1859e4e13bd9

    What are Long-Polling, Websockets, Server-Sent Events (SSE) and Comet?

    【讨论】:

    • 嗨@Logan,非常感谢您写下这篇文章!我为一个相当相似的用例尝试了你的解决方案。然而,一个处理功能已经开始运行,进度功能被暂停。您是否遇到过关于执行并发的类似问题?
    • @user3240855 嗨,是的,我确实遇到了完全相同的问题。抱歉,这是不久前的事了,我很遗憾我没有在此处记录该解决方案。我正在使用 gunicorn 和 4 名工人,他们有某种种族。我忘记了我到底做了什么,但它很简单并且解决了比赛。我记得的一件事是我没有使用 Redis,而只是使用内存中的字典,因为它是一个非常小的应用程序
    • 您好,感谢您的回复。我实际上就我的问题提出了一个问题(stackoverflow.com/questions/59195668/…),但可以自己回答:我发现问题似乎是特定于浏览器的(Firefox),因为它适用于例如铬。
    猜你喜欢
    • 1970-01-01
    • 2023-03-11
    • 1970-01-01
    • 2011-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多