如何在Python应用程序中用Django_cron自动进行工作调度

176 阅读9分钟

在Python应用程序中使用Django_cron自动进行工作调度

作业调度在所有的流媒体应用中都是至关重要的。有各种方法可以通过自动化将数据从外部的API流到应用程序中。

有时,开发者需要自动处理一些事情,比如每分钟或每小时向数据库写入数据。在本教程中,我们将制作一个Python应用程序,用一个叫做django_cron 的库来演示作业队列。

前提条件

要跟上本教程,读者需要具备以下条件。

教学目的

在本教程中,我们将学习在Python应用程序中使用django_cron 的作业调度自动化。此外,你将学习并在你的项目中应用以下内容。

  1. django_cron 如何工作。
  2. 使用django_cron 库进行自动化作业调度。

作业调度的概述

在后台进行的每一个重复的任务在软件开发中都被称为job 。作业调度处理各种任务,确保系统应用的连接。

行业中使用的调度器遵循公司的要求。例如,Oracle FLEXCUBE FCJ使用两种类型的作业调度器,名为QuartzFlux

此外,在需要重复性任务的地方,作业调度器也很重要。例如,社交媒体应用程序向用户账户发送的通知就是通过这个过程发送的。

可以安排一个工作,一旦完成或执行一个特定的动作就需要执行。在每个社交应用中,Like 按钮一旦被点击就会不断地更新,这个动作也会更新数据库或缓存。然而,数以百万计的用户可以在同一时间喜欢这些内容。

所有这些都是通过工作调度自动化执行的。请继续关注,以了解更多信息以及如何将其整合到你的下一个Python项目中。

接下来,我将介绍一个名为django_cron 的库来实现这种自动化。

django_cron如何工作

在这里,我们将谈论用这个库实现作业调度自动化。它允许Django-Python 代码在重复的基础上运行。

它有助于跟踪和执行重复性的任务。有两种主要的方式来进行自动化的东西。

它们是如下的。

1.通过一个自定义的脚本文件

在这种情况下,开发人员编写他们的脚本来实现作业的排队。这可能需要时间,而工程师要根据他们的需求来设计他们想要的东西。

下面是这个文件可能的样子。

cron:
- description: "daily summary job"
  url: /tasks/summary
  schedule: every 24 hours
- description: "monday morning mailout"
  url: /mail/weekly
  schedule: every monday 09:00
  timezone: Australia/NSW
- description: "new daily summary job"
  url: /tasks/summary
  schedule: every 24 hours
  target: beta

注意:这是一个cron.yml 文件,用于根据定义的步骤实现作业的自动化。

2.使用预定义的库

django_cron,celery 这样的库被用来实现作业的调度。这些都是预先定义的包。

使用django_cron库的作业调度自动化

现在,让我们看看如何在Python应用程序中纳入django_cron 。要将django_cron 包安装到项目中,运行下面的命令。

pip install django_cron

惯例是为代码准备一个Python脚本文件。python类在文件中扩展了已安装库中的CronJobBase 类。

同时,一个名为Schedule 的方法需要一个将被导入的minutes 的参数。这样的工作将在提供的分钟间隔内以重复方式运行。然而,这将在cron job类里面执行一个do() 方法。

然后,可以通过执行下面的片段来手动运行该作业。

python manage.py runcrons

注意:上面的命令将运行所有可用的cron job。要运行一个特定的cron job,你应该提供该job的类名,而不是run cron

我们将开发一个使用外部端点的演示APIhackernews.api-docs.io数据。此外,这个应用程序将确保根据配置的数量从端点写入数据库minutes 。在做任何事情之前,让我们设置项目目录本身。

项目设置

我们将首先按照Django的做事方式来设置项目结构。首先,我们要为项目设计一个存储位置。

打开你的终端,并运行以下命令。

cd Desktop
mkdir project && cd project

现在为该项目创建一个虚拟环境,并安装所需的依赖项,如下图所示。

python -m venv env
source env/Scripts/activate

pip install Django==3.2 django_cron djangorestframework
pip freeze > requirements.txt

你已经为该项目配置了环境,并安装了所有的依赖项。你正在通过requirements.txt 文件来跟踪它们。

现在启动该项目,启动Django服务器,用下面的命令创建一个名为news 的应用程序。

django-admin startproject hackernewsdemo
python manage.py startapp news

python manage.py runserver

如果你打开浏览器到http://127.0.0.1:8000,你应该看到默认的Django欢迎页面,如下图所示。

First page

现在打开到项目settings.py 文件,导航到INSTALLED_APPS ,并添加下面的片段。

# ...
'rest_framework',
'django_cron',
'news'

此外,必须在settings.py 文件的底部注册CRON_CLASSES 列表。该列表包含了应用程序拥有的所有cron类。另外,我们还需要在settings.py 里面添加API的权限类。

CRON_CLASSES = [
    "news.cron.MyCronJob",
    # ...
]

REST_FRAMEWORK = {
  'DEFAULT_PERMISSION_CLASSES': [
      'rest_framework.permissions.AllowAny',
  ],
}

为端点制作模型

在这个API中,从外部端点需要的是外部新闻数据的ids 。因此,你将制作只有两个属性的模型,iddatetime fetched。

最好的办法是让获取的ids 是数据库的主键。他们需要从外部端点获得最新消息。

将下面的片段添加到你的news 应用程序的models.py

from django.db import models
from datetime import datetime

class HackerNewsID(models.Model):
    hackernews = models.BigIntegerField(unique=True, primary_key=True)
    fetched_at = models.DateTimeField(default=datetime.now())

    def save(self, *args, **kwargs):
        self.id = self.hackernews # replacing the id(primary key) as the hackernews id
        super(HackerNewsID, self).save(*args, **kwargs)

    def __str__(self):
        return str(self.hackernews)

注意:HackerNewsID 类下的save 方法在调用时保存数据。这只是对获取的id 的主键进行格式化。

制作cron作业

在这里,我们将制作cron job类。这个类需要django_cron ,以便按照下面的代码配置,每分钟执行一次。

下面的代码片段描述了cron job类。它被放在news 应用程序目录下的cron.py 文件中。

from django_cron import CronJobBase, Schedule
import requests
from .models import HackerNewsID

class MyCronJob(CronJobBase):
    RUN_EVERY_MINS = 5 # every 5 minutes
    RETRY_AFTER_FAILURE_MINS = 1
    schedule = Schedule(run_every_mins=RUN_EVERY_MINS, retry_after_failure_mins=RETRY_AFTER_FAILURE_MINS)
    code = 'news.my_cron_job'    # a unique code

    def do(self):

        NEWS_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty' # external endpoint that returns list of ids

        headers = {'user-agent': 'quickcheck/0.0.1'}
        response = requests.get(NEWS_URL, headers=headers)

        result = response.text.split(',')[1:len(response.text.split(','))-2] # in order to trim the last element
        last = response.text.split(',')[-1] # got this from API " 499287535 ] /n" --> reshaped to that below
        result.insert(len(result), last.strip().split()[0]) # "499287535"

        news = 400 # 100 downward/latest
        res = [int(id.strip()) for id in result[news+1:news+6]] # list comprehension

        for id in res:
            news_id = HackerNewsID(hackernews=id)
            news_id.save()

注意:运行下面的命令,将最新的黑客新闻ID添加到数据库中。它还通过循环API的响应来执行cron job类,然后对每个类调用save() 方法。

python manage.py runcrons

MyCronJob 类扩展了从库中导入的CRONJoBBase 类。在该类的属性中,code ,作为一个特定的cron job的标识。该标识引用了settings.py

do() 方法的情况下,它发送数据到外部源,并有一些格式化。产生的响应将是来自外部源的最新的5个ids

我们循环浏览ids ,并使用save() 方法将每个id 添加到数据库中。

将模型中的数据序列化

惯例是在news 应用程序文件夹内制作一个seralizers.py 文件。该框架将来自models.py 的数据包装成JSON格式,然后保存到数据库中。

from rest_framework import serializers
from .models import HackerNewsID

class NewsIdSerializer(serializers.ModelSerializer):

    class Meta:
        model = HackerNewsID
        fields = ('hackernews',)

注意:在这个应用程序中,我们只关心id ,这就是为什么我们把字段做成一个只有一个元素的元组,来自我们的模型。

设置视图

我们将制作两个基于类的视图,称为NewsIdViewNewsItemView 。第一个视图向外部端点请求,然后通过类下的get 方法获得端点的id列表。

然而,另一个视图将匹配数据库中保存的id,从外部端点获得特定的数据。它从数据库中匹配一半的id,然后对每个id执行请求,以获取该id的数据。

import json
import requests
from rest_framework.generics import ListAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework import status
from .models import HackerNewsID


class NewsIdView(APIView):
  permission_classes = [AllowAny]

# get a list of all news ids from hacker news
  def get(self, request, format=None):

    NEWS_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'

    headers = {'user-agent': 'quickcheck/0.0.1'}
    response = requests.get(NEWS_URL, headers=headers)

    result = response.text.split(',')[1:len(response.text.split(','))-2]  # to trim the last element
    last = response.text.split(',')[-1]  #got this from API " 499287535 ] /n" --> reshaped to that below
    result.insert(len(result), last.strip().split()[0]) # "499287535"

    res = [int(id.strip()) for id in result] # list comprehension to strip each element of the data

    return Response(res, status=status.HTTP_200_OK)


class NewsItemView(APIView):
    permission_classes = [AllowAny]

    def get_data_from_API(self):
        """
            This helps to return 
            formatted data fetched from endpoint provided
            using request.
        """
        # latest = HackerNewsID.objects.all()[len(HackerNewsID.objects.all())-1].hackernews # getting latest id from db
        result = []
        half = 0
        total = len(HackerNewsID.objects.all()) # getting the total ids from the db

        #slicing into half based on even or odd total
        if total % 2 == 0:
            half = len(HackerNewsID.objects.all()) / 2
        else:
            half = (len(HackerNewsID.objects.all()) / 2) + 1

        ids = HackerNewsID.objects.all()[:half] #slicing the queryset to get last half

        for id in ids:
            NEWS_URL = f'https://hacker-news.firebaseio.com/v0/item/{str(id)}.json?print=pretty'
            headers = {'user-agent': 'quickcheck/0.0.1'} 
            response = requests.get(NEWS_URL, headers=headers)
            data = json.loads(response.text)
            result.append(data)

        return result


#GET the latest hackernews streamed
    def get(self, request, format=None):
        return Response(self.get_data_from_API(),status=status.HTTP_201_CREATED)

为你的端点配置路由

在项目目录下,打开urls.py ,粘贴下面的片段。这些是rest-framework,admin, 和news 应用程序的路由。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api-auth/', include('rest_framework.urls')),
    path('api/v0/items/', include('news.urls'))
]

要将news 应用程序与hacker news demo 项目路由连接起来,你必须在应用程序目录内制作你自定义的urls.py 。然后添加下面的代码。

from django.urls import path
from .views import NewsIdView, NewsItemView

urlpatterns = [
    path('all/', NewsIdView.as_view(), name='index'),
    # http://127.0.0.1:8080/api/v0/items/all --> lists all news id from hackernews endpoint
    path('hackernews/', NewsItemView.as_view(), name='news-item'), 
    # http://127.0.0.1:8080/api/v0/items/hackernews --> GET the latest hackernews from db
]

创建一个管理站点

作为一个Django开发者,你应该熟悉默认配置的管理页面。执行下面的命令可以制作一个新的管理页面,管理活动都在这里完成。

python manage.py createsuperuser

在创建了一个超级用户管理账户后,你必须在admin.py 中注册news 应用程序模型。 在admin.py 文件中添加这段代码。

from django.contrib import admin
from .models import HackerNewsID


class HackerNewsIDAdmin(admin.ModelAdmin):
    list_display = ('hackernews', 'fetched_at') #these are the features to be listed
    list_display_links = ('hackernews', 'fetched_at') #these are the features links

admin.site.register(HackerNewsID, HackerNewsIDAdmin)

此外,你可以通过执行以下命令来测试管理页面。

python manage.py runserver

现在在你的浏览器中打开http://127.0.0.1:8000/admin,并登录。

运行下面展示的命令将从cron类中执行对外部端点的API请求。它在cron.py 工作类里面的端点中循环。通过这样做,最新生成的ids 添加到应用程序数据库中。

python manage.py runcrons

Admin page

此外,你可以向端点http://127.0.0.1:8000/api/v0/items/hackernews发出API请求,以获取每个保存的ID所返回的数据。你应该得到类似下面的图片。

Response page

Response page 2

总结

在本教程中,我们学习了如何在Python应用程序中从库中制作一个cron job。

我们还通过获取数据并使用作业调度将其写入数据库来执行对外部源的API请求。现在你可以在一个真实世界的应用中应用你的工作调度技能。