celery 定时任务 ,异步邮箱

Celery安装配置
1.Celery介绍
1.1 Celery 特性
· 方便查看定时任务的执行情况, 如 是否成功, 当前状态, 执行任务花费的时间等.
· 使用功能齐备的管理后台或命令行添加,更新,删除任务.
· 方便把任务和配置管理相关联.
· 可选 多进程, Eventlet 和 Gevent 三种模型并发执行.
· 提供错误处理机制.
· 提供多种任务原语, 方便实现任务分组,拆分,和调用链.
· 支持多种消息代理和存储后端.
· Celery 是语言无关的.它提供了python 等常见语言的接口支持.

1.2 Celery 扮演生产者和消费者的角色
· Celery Beat : 任务调度器. Beat 进程会读取配置文件的内容, 周期性的将配置中到期需要执行的任务发送给任务队列.
· Celery Worker : 执行任务的消费者, 通常会在多台服务器运行多个消费者, 提高运行效率.
· Broker : 消息代理, 队列本身. 也称为消息中间件. 接受任务生产者发送过来的任务消息, 存进队列再按序分发给任务消费方(通常是消息队列或者数据库).
· Producer : 任务生产者. 调用 Celery API , 函数或者装饰器, 而产生任务并交给任务队列处理的都是任务生产者.
· Result Backend : 任务处理完成之后保存状态信息和结果, 以供查询.

#使用于生产环境的消息代理有 RabbitMQ 和 Redis, 官方推荐 RabbitMQ.

1.3 Celery 常用配置参数汇总
配置项 说明
CELERY_DEFAULT_QUEUE 默认队列
CELERY_BROKER_URL Broker 地址
CELERY_RESULT_BACKEND 结果存储地址
CELERY_TASK_SERIALIZER 任务序列化方式
CELERY_RESULT_SERIALIZER 任务执行结果序列化方式
CELERY_TASK_RESULT_EXPIRES 任务过期时间
CELERY_ACCEPT_CONTENT 指定任务接受的内容类型(序列化)

  1. 安装
    2.1 celery安装
    pip install celery
    pip install celery[redis]

2.2 flower 安装
docker pull placr/flower
docker run -d -p 5555:5555 --name flower --link redis:redis placr/flower
访问端口5555就可以看到web 界面

  1. Celery 定时任务
    3.1 项目目录结构
    demo/
    celery_app/
    init.py
    celeryconfig.py
    tasks.py

3.2 init.py 内容

from celery import Celery

app = Celery(‘demo’) # 创建 Celery 实例
app.config_from_object(‘celery_app.celeryconfig’) # 添加配置文件

配置选项 配置分版本了 有两种可用配置版本 不能混用

http://docs.celeryproject.org/en/latest/userguide/configuration.html#include

3.3 celeryconfig.py 内容

-- coding: utf-8 --

from celery.schedules import timedelta
from celery.schedules import crontab

Broker and Backend

BROKER_URL = ‘redis://192.168.5.151:6379’ # 指定 Broker 消息代理
#BROKER_URL = ‘redis://127.0.0.1:6379’
CELERY_RESULT_BACKEND = ‘redis://192.168.5.151:6379/0’ # 指定 Backend 结果存储
#CELERY_RESULT_BACKEND = ‘redis://127.0.0.1:6379/0’

注 结果存储 和消息代理可以用不同 消息代理存储 流行 RabbitMQ 做消息代理,保证持久性 redis 做结果存储

CELERYD_PREFETCH_MULTIPLIER = 10 # 并发量 同一个worker 可以同时处理任务上限
CELERY_TASK_RESULT_EXPIRES = 3600 # 结果存储过期
CELERY_TASK_ALWAYS_EAGER = False # 如果是这样True,所有任务将通过阻塞在本地执行,直到任务返回
CELERY_ENABLE_UTC = False

Timezone

CELERY_TIMEZONE=“Asia/Shanghai” # 指定时区,不指定默认为 ‘UTC’

CELERY_TIMEZONE=‘UTC’

import # 指定导入的任务模块

CELERY_IMPORTS = (
‘celery_app.tasks’,
)

schedules

CELERYBEAT_SCHEDULE = {
‘add-every-30-seconds’: {
‘task’: ‘celery_app.tasks.add’,
‘schedule’: crontab(minute="*"), # 每 60 秒执行一次
‘args’: (5, 8) # 任务函数参数
},
}

3.4 Tasks.py 内容

coding:utf-8

import time
from celery_app import app
import os

os.environ.setdefault(‘FORKED_BY_MULTIPROCESSING’, ‘1’)
@app.task
def add(x, y):
time.sleep(2)
return x + y

3.5 启动

启动一个worker

Celery -A celery_app worker -l info

启动一个 beat # 随时检查配置变化

Celery -A celery_app beat –l info

  1. Celery 配合flask 异步发送邮件任务
    Flask-demo/
    Main.py
    Celeryconfig.py
    Celerytasks.py
    Celeryapp.py

4.1 Main.py 内容

from flask import Flask, request, render_template, session, flash, redirect,
url_for
from flask_mail import Mail, Message
from celeryapp import make_celery
app = Flask(name)
app.config[‘SECRET_KEY’] = ‘top-secret!’

Flask-Mail configuration

app.config[‘MAIL_SERVER’] = “smtp.163.com
app.config[‘MAIL_PORT’] = 25
app.config[‘MAIL_USE_TLS’] = True
app.config[‘MAIL_USERNAME’] = "[email protected]" # 配置自己的
app.config[‘MAIL_PASSWORD’] = “xxxx” # 配置自己的邮箱授权码
app.config[‘MAIL_DEFAULT_SENDER’] = ‘[email protected]

mail = Mail(app)
celery = make_celery(app)
from celerytasks import add, send_async_email # 只有在 celery 配置后 才可以调task

@app.route(’/’, methods=[‘GET’, ‘POST’])
def index():

title = 'Hello from Flask'

email = “[email protected]
body = ‘This is a test email sent from a background Celery task.’
re = send_async_email.delay(title, email, body)
print(re.result) #获取结果
print(re.ready) #是否处理
print(re.get(timeout=1)) #获取结果
print(re.status) #是否处理
return redirect(url_for(‘index’))
if name == ‘main’:
app.run(debug=True)

4.2 celeryapp.py 内容
from celery import Celery

def make_celery(app):
celery = Celery(app.import_name)
celery.config_from_object(“celerytest”)

class ContextTask(celery.Task):
    def __call__(self, *args, **kwargs):
        with app.app_context():
            return self.run(*args, **kwargs)

celery.Task = ContextTask

return celery

4.3 Celeryconfig.py 内容

-- coding: utf-8 --

from celery.schedules import timedelta
from celery.schedules import crontab

Broker and Backend

BROKER_URL = ‘redis://192.168.5.151:6379’ # 指定 Broker

BROKER_URL = ‘redis://127.0.0.1:6379’

CELERY_RESULT_BACKEND = ‘redis://192.168.5.151:6379/0’ # 指定 Backend

CELERY_RESULT_BACKEND = ‘redis://127.0.0.1:6379/2’
CELERYD_PREFETCH_MULTIPLIER = 1 # 并发量
CELERY_TASK_ALWAYS_EAGER = False # 如果是这样True,所有任务将通过阻塞在本地执行,直到任务返回
CELERY_ENABLE_UTC = False # 如果消息中的已启用日期和时间将转换为使用UTC时区。

任务序列化和反序列化使用msgpack方案

CELERY_TASK_SERIALIZER = ‘json’ # 可以是 json(默认),pickle,yaml,msgpack

读取任务结果一般性能要求不高,所以使用了可读性更好的JSON

CELERY_RESULT_SERIALIZER = ‘json’

任务过期时间,这样写更加明显

CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 # 结果过期

Timezone

CELERY_TIMEZONE = “Asia/Shanghai” # 指定时区,不指定默认为 ‘UTC’

import # 指定导入的任务模块

CELERY_IMPORTS = [‘test3’,“celerytasks”]

4.4 Celerytasks.py 内容

coding:utf-8

import os
from main import celery,mail,app
from flask_mail import Message
os.environ.setdefault(‘FORKED_BY_MULTIPROCESSING’,‘1’)

@celery.task
def add(x, y):
return x + y

@celery.task
def send_async_email(title,email,body):
“”“Background task to send an email with Flask-Mail.”""

msg = Message(title,sender="[email protected]",
              recipients=[email])
msg.body = body
with app.app_context():
    return mail.send(msg)

启动一个worker

celery -A main.celery worker -l info

启动flask项目

python main.py

5.celery 多队列、多路由
5.0 多队列、多worker流程图
如果要说celery的分布式应用的话,我觉得就要提到celery的消息路由机制,就要提一下AMQP协议。具体的可以查看AMQP的文档。简单地说就是可以有多个消息队列(Message Queue),不同的消息可以指定发送给不同的Message Queue,而这是通过Exchange来实现的。发送消息到Message Queue中时,可以指定routiing_key,Exchange通过routing_key来把消息路由(routes)到不同的Message Queue中去,

5.1 celeryconfig.py 配置
#!/usr/bin/env python

-- coding:utf-8 --

from kombu import Exchange, Queue

BROKER_URL = “redis://127.0.0.1:6379/1”
CELERY_RESULT_BACKEND = “redis://127.0.0.1:6379/2”

多队列

CELERY_QUEUES = (
Queue(“default”, Exchange(“default”), routing_key=“default”),
Queue(“for_task_A”, Exchange(“for_task_A”), routing_key=“for_task_A”),
Queue(“for_task_B”, Exchange(“for_task_B”), routing_key=“for_task_B”)
)

路由 通过CELERY_ROUTES来为每一个task指定队列,如果有任务到达时,通过任务的名字来让指定的worker来处理。

CELERY_ROUTES = {
‘tasks.taskA’: {“queue”: “for_task_A”, “routing_key”: “for_task_A”},
‘tasks.taskB’: {“queue”: “for_task_B”, “routing_key”: “for_task_B”}
}

5.2 tasks.py 内容

#!/usr/bin/env python
#-- coding:utf-8 --
from celery import Celery

app = Celery()
app.config_from_object(“celeryconfig”) # 指定配置文件

@app.task
def taskA(x,y):
return x + y

@app.task
def taskB(x,y,z):
return x + y + z

@app.task
def add(x,y):
return x + y

5.3 测试文件 run_redis_queue.py

coding:utf-8

from redis_queue.tasks import *

re1 = taskA.delay(100, 200)
re2 = taskB.delay(1, 2, 3)
re3 = add.delay(1, 2)
5.4 启动

windows

celery -A tasks worker -l info -n workerA.%h -Q for_task_A -P eventlet
celery -A tasks worker -l info -n workerB.%h -Q for_task_B -P eventlet
celery -A tasks worker -l info -n worker.%h -Q celery -P eventlet

linux

celery -A tasks worker -l info -n workerA.%h -Q for_task_A
celery -A tasks worker -l info -n workerB.%h -Q for_task_B
celery -A tasks worker -l info -n worker.%h -Q celery

linux 直接运行 python run_redis_queue.py
windows 无法使用 运行后 无效果 只能直接在python命令窗口中调用

各个任务 只在指定队列中的worker中运行

  1. celery multi
    6.1 后台启动worker
    celery multi start w1 -A proj -l info # 启动
    celery multi restart w1 -A proj -l info # 重新启动
    celery multi stop w1 -A proj -l info # 异步关闭 立即返回
    celery multi stopwait w1 -A proj -l info # 等待关闭操作完成

默认情况下,celery会在当前目录下创建pidfile和logfile.为了防止多个worker在启动时相互影响,你可以指定一个特定的目录。

$ mkdir -p /var/run/celery
$ mkdir -p /var/log/celery
$ celery multi start w1 -A proj -l info --pidfile=/var/run/celery/%n.pid
–logfile=/var/log/celery/%n%I.log

在linux下执行 windows有报错

网上例子

原文链接
https://www.cnblogs.com/ifkite/p/4257721.html

Celery是什么?
Celery是个异步分布式任务队列。
通过Celery在后台跑任务并不像用线程那么的简单,但是用Celery的话,能够使应用有较好的可扩展性,因为Celery是个分布式架构。下面介绍Celery的三个核心组件。
生产者(Celery client)。生产者(Celery client)发送消息。在Flask上工作时,生产者(Celery client)在Flask应用内运行。
消费者(Celery workers)。消费者用于处理后台任务。消费者(Celery client)可以是本地的也可以是远程的。我们可以在运行Flask的server上运行一个单一的消费者(Celery workers),当业务量上涨之后再去添加更多消费者(Celery workers)。
消息传递者(message broker)。生产者(Celery client)和消费者(Celery workers)的信息的交互使用的是消息队列(message queue)。Celery支持若干方式的消息队列,其中最常用的是RabbitMQ和Redis.
话不多说上代码先。代码中包含两个例子:异步发送邮件;开始一或多个异步工作,然后在网页上更新其进度。

Flask结合Celery
Flask与Celery结合极其简单,无需其他扩展。一个使用Celery的Flask应用的初始化过程如下:通过创建Celery类的对象,完成Celery的初始化。创建Celery对象时,需要传递应用的名称以及消息传递者(message broker)的URL。

from flask import Flask
from celery import Celery

app = Flask(name)
app.config[‘CELERY_BROKER_URL’] = ‘redis://localhost:6379/0’
app.config[‘CELERY_RESULT_BACKEND’] = ‘redis://localhost:6379/0’

celery = Celery(app.name, broker=app.config[‘CELERY_BROKER_URL’])
celery.conf.update(app.config)

其中的URL参数告诉了Celery,消息传递的服务的位置。如果消息传递者用的不是Redis,或者Redis部署在其他机器,那么需要做适当的改变。

而通过调用 celery.conf.update() 方法,我们能够为Celery同步Flask上的配置。
仅当需要Celery存储状态即存储结果时, CELERY_RESULT_BACKEND 选项才会被用到。
下文第一个例子不需要存储状态以及存储结果,但是第二个例子是需要的,所以一次配置好。

任何想要在后台运行的任务,都需要使用装饰者celery.task 进行包装,如下。
@celery.task
def my_background_task(arg1, arg2):
# some long running task here
return result
现在Flask 应用就能够发起“在后台执行任务”的请求了,如下。
task = my_background_task.delay(10, 20)
其中 delay() 方法是 apply_async() 的快捷调用。

此处用apply_async()同样奏效,如下。
task = my_background_task.apply_async(args=[10, 20])
相比于 delay() 方法,当使用 apply_async() 方法时,我们能够对后台任务的执行方式有更多的控制。例如任务在何时执行等。

举例来说,下面的代码可以让任务在一分钟之后开始运行。
task = my_background_task.apply_async(args=[10, 20], countdown=60)
delay() 和 apply_async() 的返回值是一个 AsyncResult 的对象。通过该对象,能够获得任务的状态。
一些其他的配置选项不再叙述。

例一:异步发邮件
第一个例子的需求比较广泛:发电子邮件的时候无需阻塞主应用线程。
本例使用了扩展Flask-Mail。

网页包含了一个Text类型的域的表单。用户需要在其中输入邮箱地址,点击提交,然后服务器向该地址发送一封测试邮件。该表单包含两个提交按钮,其中一个会立即发送邮件,而另一个会在点击后延迟一分钟后再发送。html代码如下。

Flask + Celery Examples

Flask + Celery Examples

Example 1: Send Asynchronous Email

{% for message in get_flashed_messages() %}

{{ message }}

{% endfor %}

Send test email to:

用于发送邮件的Flask-Mail需要一些配置,主要与发送邮件的邮件服务器、发送邮件时间相关。
考虑到用户名密码安全性,作者将其放到了环境变量中。

Flask-Mail configuration

app.config[‘MAIL_SERVER’] = ‘smtp.googlemail.com
app.config[‘MAIL_PORT’] = 587
app.config[‘MAIL_USE_TLS’] = True
app.config[‘MAIL_USERNAME’] = os.environ.get(‘MAIL_USERNAME’)
app.config[‘MAIL_PASSWORD’] = os.environ.get(‘MAIL_PASSWORD’)
app.config[‘MAIL_DEFAULT_SENDER’] = ‘[email protected]

异步发送代码如下。

@app.route(’/’, methods=[‘GET’, ‘POST’])def index():
if request.method == ‘GET’:
return render_template(‘index.html’, email=session.get(‘email’, ‘’))
email = request.form[‘email’]
session[‘email’] = email

# send the email
msg = Message('Hello from Flask',
              recipients=[request.form['email']])
msg.body = 'This is a test email sent from a background Celery task.'
if request.form['submit'] == 'Send':
    # send right away
    send_async_email.delay(msg)
    flash('Sending email to {0}'.format(email))
else:
    # send in one minute
    send_async_email.apply_async(args=[msg], countdown=60)
    flash('An email will be sent to {0} in one minute'.format(email))

return redirect(url_for('index'))

用 session 将用户键入的信息保存,以便页面刷新时能够使用该信息。
朋友们发现了,重点在发送邮件的代码,使用的是Celery 的任务send_async_email,通过调用它的 delay() 方法或 apply_async() 进行异步发送。

最后来看异步任务代码。
@celery.task
def send_async_email(msg):
“”“Background task to send an email with Flask-Mail.”""
with app.app_context():
mail.send(msg)

使用装饰者 celery.task 包装 send_async_email , 使其成为后台运行的任务。因为Flask-Mail需要应用的context,所以需要在调用send方法前先创建应用的context环境。
另一点很重要,从异步调用的返回值是不会保存的,所以应用本身无法知道是否异步调用是否成功。在这个例子之中需要看Celery的消费者的输出才能确定发送邮件过程是否有问题。
第一个例子比较简单,我们起了后台任务然后就不必再去管它了。很多应用的需求与例子一相仿。

然而也会有一些应用,需要监控后台任务的运行,获得任务的结果。下面来看第二个例子。

例二:显示状态更新进度
用户可以点击按钮以启动一个或者多个长时间任务,此时在网页使用ajax技术不断轮询服务器以更新所有的这些长时间任务们的状态。
而对于每一个长时间任务,网页上会有一个窗台条、一个进度百分比、一个状态消息与之对应,当完成时会显示相应结果。

状态更新时后台任务代码。

@celery.task(bind=True)
def long_task(self):
“”“Background task that runs a long function with progress reports.”""
verb = [‘Starting up’, ‘Booting’, ‘Repairing’, ‘Loading’, ‘Checking’]
adjective = [‘master’, ‘radiant’, ‘silent’, ‘harmonic’, ‘fast’]
noun = [‘solar array’, ‘particle reshaper’, ‘cosmic ray’, ‘orbiter’, ‘bit’]
message = ‘’
total = random.randint(10, 50)
for i in range(total):
if not message or random.random() < 0.25:
message = ‘{0} {1} {2}…’.format(random.choice(verb),
random.choice(adjective),
random.choice(noun))
self.update_state(state=‘PROGRESS’,
meta={‘current’: i, ‘total’: total,
‘status’: message})
time.sleep(1)
return {‘current’: 100, ‘total’: 100, ‘status’: ‘Task completed!’,
‘result’: 42}

代码中作者在Celery 装饰者中加入了 bind=True 参数,这使得Celery向函数中传入了self参数,因此在函数中能够记录状态更新。
本例中随机挑选了一些单词作为状态的更新,同时,选取随机数作为每个后台任务运行时间。
self.update_state() 方法用于指明 Celery如何接收任务更新。
Celery有很多内建状态比如 STARTED , SUCCESS 等等,当然Celery也允许程序员自定义状态。本例子中使用的是自定义状态, PROGRESS 。与 PROGRESS 一起的还有 metadata 。 metadata 是一个字典,包含当
前进度,任务大小,以及消息。
当循环跳出时,返回字典,字典中包含任务的执行结果。

long_task() 函数在 Celery消费者进程中运行。下面看一下Flask应用如何启动该后台任务。
@app.route(’/longtask’, methods=[‘POST’])
def longtask():
task = long_task.apply_async()
return jsonify({}), 202, {‘Location’: url_for(‘taskstatus’,
task_id=task.id)}
用户需要向 /longtask 发送 POST 请求以触发后台任务执行。服务器启动任务并存储返回值。作者使用了状态码202,在REST API中有“请求正在处理中”的意思,而加入了Location头则是为了生产者能够获取任务执行时的状态信息。url_for用于生成路由到taskstatus函数的url,并且该url包含task id,task id的值是 task.id .

taskstatus 函数用于获取后台任务的更新状态。

@app.route(’/status/<task_id>’)
def taskstatus(task_id):
task = long_task.AsyncResult(task_id)
if task.state == ‘PENDING’:
// job did not start yet
response = {
‘state’: task.state,
‘current’: 0,
‘total’: 1,
‘status’: ‘Pending…’
}
elif task.state != ‘FAILURE’:
response = {
‘state’: task.state,
‘current’: task.info.get(‘current’, 0),
‘total’: task.info.get(‘total’, 1),
‘status’: task.info.get(‘status’, ‘’)
}
if ‘result’ in task.info:
response[‘result’] = task.info[‘result’]
else:
# something went wrong in the background job
response = {
‘state’: task.state,
‘current’: 1,
‘total’: 1,
‘status’: str(task.info), # this is the exception raised
}
return jsonify(response)

为了得到后台任务产生的数据,使用了task id作为参数创建了一个task 对象。
本函数产生了JSON响应,JSON响应中的内容与 update_state() 更新的一致。
我们使用 task.state 区分后台任务的状态:本例有未运行、未发生错误、发生错误三种状态。
我们使用 task.info 访问任务相关信息。而发生错误时, task.state 的状态是 FAILURE 时,异常会包含在 task.info 之中。

前端JS代码
作者用的是nanobar.js实现进度条,用了jQuery的ajax。

启动后台任务的按钮的JS代码如下。

function start_long_task() {
// add task status elements
div = $(‘

0%
 

‘);
$(’#progress’).append(div);

    // create a progress bar
    var nanobar = new Nanobar({
        bg: '#44f',
        target: div[0].childNodes[0]
    });

    // send ajax POST request to start background job
    $.ajax({
        type: 'POST',
        url: '/longtask',
        success: function(data, status, request) {
            status_url = request.getResponseHeader('Location');
            update_progress(status_url, nanobar, div[0]);
        },
        error: function() {
            alert('Unexpected error');
        }
    });
}

其中被加入的HTML元素与任务的信息的对应关系如下。

<-- Progress bar
0%
<-- Percentage
...
<-- Status message
 
<-- Result

start_long_task() 函数通过ajax向 /longtask 发送POST请求,使得后台任务开始运行。 当ajax的POST请求返回时,回调函数获得响应,响应中包含形如 /status/的url, 其他函数(如 update_progress )用此url从 taskstatus 函数获取数据。 调用函数 update_progress() ,向函数传入 start_url 以及 nanoba r变量,用于生成进度条。

update_progress 函数向/status/<task_id>发送GET请求,获得json数据然后更新相应的页面元素。

function update_progress(status_url, nanobar, status_div) {
// send GET request to status URL
$.getJSON(status_url, function(data) {
// update UI
percent = parseInt(data[‘current’] * 100 / data[‘total’]);
nanobar.go(percent);
$(status_div.childNodes[1]).text(percent + ‘%’);
$(status_div.childNodes[2]).text(data[‘status’]);
if (data[‘state’] != ‘PENDING’ && data[‘state’] != ‘PROGRESS’) {
if (‘result’ in data) {
// show result
$(status_div.childNodes[3]).text('Result: ’ + data[‘result’]);
}
else {
// something unexpected happened
$(status_div.childNodes[3]).text('Result: ’ + data[‘state’]);
}
}
else {
// rerun in 2 seconds
setTimeout(function() {
update_progress(status_url, nanobar, status_div);
}, 2000);
}
});
}

当后台任务完成时,result会加载到页面之中。如果没有result的话,这就意味着任务的执行以失败告终,此时任务的状态是 FAILURE 。
任当后台任务运行时,为了能够持续获得任务状态并更新页面,作者使用了定时器,定时器每个两秒一更新直到后台任务完成。

运行例子
读者先安装好virtualenv(强烈推荐!但是virtualenv非必需安装)。
下载代码,安装相应库,如下。
1 $ git clone https://github.com/miguelgrinberg/flask-celery-example.git
2 $ cd flask-celery-example
3 $ virtualenv venv
4 $ source venv/bin/activate
5 (venv) $ pip install -r requirements.txt
未安装virtualenv的话直接跳过第三行第四行命令。

redis server端读者自行安装。安装后运行启动。
Celery 消费者也需要读者运行,使用 celery命令。
邮件用户名密码自行设置。
$ export MAIL_USERNAME=
$ export MAIL_PASSWORD=
$ source venv/bin/activate
(venv) $ celery worker -A app.celery --loglevel=info
Celery的 -A选项是应用中的celer对象,与文章最开头的代码对应。
–loglevel=info 则是让日志内容更为详细。

最后启动应用。
$ source venv/bin/activate
(venv) $ python app.py
访问http://localhost:5000/ 即可。

猜你喜欢

转载自blog.csdn.net/WJ844908240/article/details/90038562