Как сделать индикатор на веб-странице для работы с пандами - PullRequest
10 голосов
/ 12 апреля 2019

Я некоторое время гуглил и не мог найти способ сделать это. У меня есть простое приложение 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 теперь работает с пандами и имеет progress_apply метод вместо apply. Но я не уверен, имеет ли это отношение к созданию индикатора на веб-странице. Думаю, так и должно быть, поскольку он работает на ноутбуках Jupyter. Как мне добавить индикатор выполнения для pd_convert() здесь?

Конечный результат, который я хочу получить:

  1. Пользователь нажимает кнопку загрузки, выбирает файл CSV из своей файловой системы
  2. Клики пользователей отправляют
  3. Запускается индикатор выполнения
  4. Когда индикатор выполнения достигает 100%, запускается загрузка

1 и 2 сделаны. Тогда следующий вопрос - как запустить загрузку. На данный момент моя функция convert запускает загрузку без проблем, потому что ответ формируется из файла. Если я хочу отобразить страницу, я формирую ответ с return render_template(...). Поскольку у меня может быть только один ответ, возможно ли иметь 3 и 4 только с одним вызовом на /convert?

Не веб-разработчик, все еще изучающий основы. Заранее спасибо!


==== EDIT ====

Я попробовал пример здесь с некоторыми изменениями. Я получаю прогресс из индекса строки в цикле for на фрейме данных и помещаю его в Redis. Клиент получает информацию о прогрессе от Redis из потока, запрашивая эту новую конечную точку /progress. Что-то вроде

@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')

В настоящее время работает, но с некоторыми проблемами. Причиной, безусловно, является мое непонимание этого решения.

Вопросы:

  • Мне нужно, чтобы прогресс сбрасывался на 0 при каждом нажатии кнопки submit. Я пробовал несколько мест, чтобы сбросить его до 0, но еще не нашел рабочую версию. Это определенно связано с моим непониманием того, как работает поток. Теперь он сбрасывается только при обновлении страницы.
  • Как обрабатывать параллельные запросы, такие как состояние гонки Redis? Если несколько пользователей делают запросы одновременно, ход выполнения должен быть независимым для каждого из них. Я подумываю дать случайное job_id для каждого submit события и сделать его ключом в Redis. Поскольку мне не нужна запись после каждой работы, я просто удалю запись после того, как она будет сделана.

Я чувствую, что моя недостающая часть - это понимание text/event-stream. Ощущение, что я близок к рабочему решению. Пожалуйста, поделитесь своим мнением о том, что такое «правильный» способ сделать это. Я просто угадываю и пытаюсь собрать что-то, что работает с моим очень ограниченным пониманием.

1 Ответ

6 голосов
/ 22 мая 2019

ОК, я сузил проблемы, которые мне не хватало, и понял это. Понятия, которые мне нужны, включают

Backend

  • Redis как база данных значений ключей для хранения хода выполнения, который может быть запрошен конечной точкой /progress для потока событий (HTML5)
  • Отправленное на сервер событие (SSE) для потоковой передачи прогресса: text/event-stream Ответ MIME-типа
  • Генератор Python в приложении Flask для SSE
  • Запись прогресса (обрабатывается индекс строки) цикла for в кадре данных Pandas в Redis

Frontend

  • Открыть поток событий: запустить SSE со стороны клиента с помощью кнопки HTML
  • Закрыть поток событий: как только данные события достигнут 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>

Пример внутреннего кода колбы

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')

Остальное - код для панд для циклической записи в Redis.

Я собрал воедино множество результатов, полученных за несколько часов поиска в Google, поэтому я считаю, что лучше документировать здесь для людей, которым также нужна эта базовая функция: добавить индикатор выполнения в веб-приложение Flask для обработки кадров данных 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

Что такое Long-Polling, Websockets, Серверные события (SSE) и Comet?

...