使用Django时,通过进度条显示文件上传后的处理进度

这篇文章是关于什么的?

由于尝试在Django中显示进度条遇到了一些困难,我决定记录下我所查找到的内容。

我想实现的目标。

我希望能够选择zip文件并上传,在后台对zip文件内的文件进行处理,并在屏幕上显示处理的进度。

实现的方式

经过调查发现,可能有多种方法,比如使用websocket等,但是本次我们将尝试使用Python的异步处理库Celery和Redis的任务队列来实现异步处理。

西芹

这是一个使用Python编写的异步任务队列/工作程序的分布式任务队列系统。它主要用于高效处理需要在后台运行并持续较长时间的任务或作业。

 

任务队列

任务队列是一个用来存放耗时处理(在这里是文件处理)任务的地方。队列中的任务会被代理人传递给工作者并执行处理,详细内容稍后会提到。

工人

タスクを実行する役割を持ちます。ブローカーから受け取ったタスクを処理し、結果をブローカーに返します。

红薯

redisはオープンソースのインメモリデータベースで、高性能なデータキャッシュ、キーバリューストア、およびメッセージブローカーとして使用されることが一般的です。ディスクではなくメモリ上にデータを保持するため、高速なデータアクセスが可能であり、多くのリクエストを処理することができます。
ここでは redisを Celeryのメッセージブローカーとして使用します。djangoから処理を受け取り、Djangoと Celery worker との間でtaskの受け渡しや結果の受信を行います。

image.png

芹菜进展

使用Celery Progress库,该库专门用于在Django中使用Celery来显示进度条,我们尝试创建一个进度条。

 

环境设置

由于代码已经在以下网址上公开,因此我将简要解释一下内容。
https://github.com/ekity1002/django-progressbar-sample

以下是使用Python和相关主要库的版本。

    • python 3.9.5

 

    • django 4.2

 

    • django-redis 5.2.0

 

    • celery 5.2.7

 

    celery-progress 0.3

创建和配置Django项目。

我正在使用名为django_progressbar的项目来创建项目。由于没有特别不同之处,所以省略了详细信息。

安装必要的库

我正在安装以下的库。

pip install django-redis celery celery-progress 

The configuration of Celery.

为了在Django中使用Celery,我在django_progressbar目录下创建了一个名为celery.py的文件,并进行了Celery的配置。(几乎与下面的官方文档中提供的配置相同。)
https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html

以下是不同之处,即将Celery的broker配置为使用redis的部分。

import os

from celery import Celery

# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_progressbar.settings")

# Celery app作成
# project名, brokerのホストを指定
app = Celery("django_progressbar", broker="redis://redis:6379/0")
app.conf.result_backend = "redis://redis:6379/0"

我还在settings.py中添加了以下内容。

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    ...
    # Celery apps
    "celery", #追加
    "celery_progress", #追加
]

App设置

观看

我正在创建一个名为upload_file的Django应用程序,并创建一个视图来上传zip文件。

    views.py
import time
import zipfile
from io import BytesIO

from celery import shared_task
from celery_progress.backend import ProgressRecorder
from django.core.files.storage import default_storage
from django.shortcuts import render

from .forms import UploadZipFileForm


def get_file_list(zip_file):
    # zipファイル内の全てのファイルを取得する
    buffer = BytesIO(zip_file.read())
    with zipfile.ZipFile(buffer, "r") as zip_ref:
        files = zip_ref.namelist()
    return files


@shared_task(bind=True)
def process_files(self, file_list):
    print(type(self))
    progress_recorder = ProgressRecorder(self)

    # 進行状況の初期化
    total_files = len(file_list)
    progress_recorder.set_progress(0, total_files)
    print("total files: ", total_files)

    # ファイルを読み込んで、内容を取得する
    result = 0
    for idx, file in enumerate(file_list):
        # 重い処理
        print(f"Processing {file}")
        time.sleep(0.4)
        # 進行状況を更新
        print(f"Done!")
        result += 1

        progress_recorder.set_progress(idx + 1, total_files, description=f"処理中...({idx+1}/{total_files})")
    # return "File upload success!"


def upload_zip_file(request):
    if request.method == "POST":
        form = UploadZipFileForm(request.POST, request.FILES)

        if form.is_valid():
            # クライアントから送信されたzipファイルを取得する
            print("zip uploaded.")
            zip_file = request.FILES["zip_file"]
            file_list = get_file_list(zip_file)

            # ファイルを処理する
            result = process_files.delay(file_list)

            # zip削除
            zip_file.close()
            default_storage.delete(zip_file.name)
            print(type(result), result)
            # 処理が完了したら、リダイレクトなど適切なレスポンスを返す
            return render(request, "upload_file/upload.html", context={"form": form, "task_id": result.task_id})
    else:
        form = UploadZipFileForm()
    # GETリクエストの場合は、ファイルアップロードのフォームを表示する
    return render(request, "upload_file/upload.html", {"form": form})

所有的流程是,将zip文件上传 -> 在zip文件中,在带有shared_task修饰器的process_files函数中执行的处理由Celery的工作进程异步执行。

在调用process_file时,加上delay()使其变为异步处理。

此外,在该函数内部创建了一个 ProgressRecorder 对象,并记录了用于在 celery-progress 中显示进度条的进度。

模板

除了在upload.html中进行文件上传设置外,还添加了以下描述来实现使用celery-progress显示进度条。

<div class='progress-wrapper'>
  <div id='progress-bar' class='progress-bar' style="background-color: #68a9ef; width: 0%;">&nbsp;</div>
</div>
<div id="progress-bar-message">Waiting for progress to start...</div>
<div id="celery-result">
</div>

{% if task_id %}
<script type="text/javascript">
  function processProgress(progressBarElement, progressBarMessageElement, progress) {
    console.log(`@@@@@ ${progress.percent} processProgress @@@@@`)
    console.log(progress)
    progressBarElement.style.width = progress.percent + "%";
    var description = progress.description || "アップロード中...";
    progressBarMessageElement.innerHTML = description;
  }

  // Progress Bar (JQuery)
  $(function () {
    var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
    CeleryProgressBar.initProgressBar(progressUrl, {
      onProgress: processProgress,
    })
  });
</script>
{% endif %}

我們幾乎直接使用了 celery-progress 文檔中的配置。

 

当上传zip文件时,后端会异步执行process_file函数并返回task_id作为响应。然后开始显示进度条。

Docker的配置设置

使用docker创建并运行redis和django的容器。在docker-compose中进行如下设置:

version: '3'

services:
  web:
    container_name: web
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - ./app:/app/app
    command: bash -c "poetry run celery -A django_progressbar worker -l INFO & poetry run python manage.py runserver 0:8000"
 
 redis:
    image: "redis:latest"
    container_name: redis
    ports:
      - "6379:6379"
    volumes:
      - "./data/redis:/data"

在启动Django容器时,请执行以下两个命令作为注意事项。

poetry run celery -A django_progressbar worker -l INFO  #celery worker 起動
poetry run python manage.py runserver 0:8000 #djangoサーバー起動

我们首先执行了Celery命令来启动配置的worker进程,然后启动了Django服务器。

打开

运行docker-compose up -d命令,并访问http://localhost:8000/upload/来上传zip文件,结果如下所示。

动画.gif

另外,celery-progress 还提供了多种自定义外观和行为的选项,如果您有兴趣,请查阅官方文档了解详情。

总结

大致上说,我在Django中记录了在文件上传时显示进度条的方法。
虽然一开始看起来很简单,但比预想中困难,因此对我来说是一次学习机会。
关于与其他实现方法,比如websocket等的优缺点的比较,我还不太了解,所以也想调查一下这方面的信息。

广告
将在 10 秒后关闭
bannerAds