想要看更加舒服的排版、更加準時的推送
關注公衆號“不太靈光的程序員”
每日八點有乾貨推送
有粉絲留言問什麼時候可以寫一個關於自動化任務的文章
準備上!~
感覺有用關注公衆號 “不太靈光的程序員”
每天9:30分析一些Python小知識,爬坑的路與你同行
什麼是自動化任務
自動化任務就是將一系列可以手動去實現的操作的集合,在特定的條件下自動去執行,比較常見的場景:
- 運行監控報警,監測要素的變化,觸發閾值條件時執行一系列通知郵件、短信的發送,也可以是更復雜的觸發
- 自動化田間灌溉控制,對土壤含水量、天氣預報、植物生長週期分析需水量來調節灌溉水量
- 自動駕駛,根據行駛路線、實時路況信息進行分析後實施自動的駕駛
- 自動化測試,對目標系統有已知的預期,按照配置的流程去驗證每一個功能的結果
- 定時爬蟲,比如我們需要長期的爬取招聘信息,來進行數據分析,就需要定時的去爬取某段時間的增量信息
- 還有些統計類問題,比如我們需要對請求的IP進行判斷是否是一個壞的IP(同一個IP下有一定比例的用戶從事違規操作時,判斷爲非法IP),我們可以在用戶請求是實時去計算壞IP的列表,但這當用戶量非常大的時候就不是一個很好的選擇了,我們可以選擇定期執行這個壞IP列表的計算,因爲對於不同用戶的壞IP列表是同一個
以上這些任務都可以認爲是自動化任務,我們今天主要針對其中的定時任務進行討論。
定時任務
定時任務的實現方式又有N多種,我就瞭解的幾種實現舉例說明:
- Windows操作系統的任務計劃程序
- Linux操作系統的 Crontab 定時任務
- Python的 循環+sleep
- Python的 Timer
- Python的 schedule
- Python的 APScheduler
- Python的 Celery
我以兩個場景來設計實例代碼
- 每30秒打印一下當前時間
- 在2020-06-21 00:00:00 (具體時刻)打印一下當前時間
1. Windows任務計劃程序
當前版本win10
- 右鍵“計算機”->選擇“管理”;
- 然後就可以打開 “計算機管理”界面;
- 在界面的左側有點擊“系統工具”->任務計劃程序 ;
- 可以看到右側有“操作”“任務計劃程序”;
- 有創建基本任務、創建任務、導入任務;
我以兩個場景來設計實例代碼 - 每30秒打印一下當前時間
- 在2020-06-21 00:00:00 (具體時刻)打印一下當前時間
任務1:由於任務計劃程序最小執行週期是“5分鐘”,是在高級設置中設置重複任務間隔,並選擇持續時間“無限制” 無法實現30秒的打印
任務2:設置中選擇一次,開始時間設置2020-06-21 00:00:00 (具體時刻)可以滿足需求
2. Linux Crontab定時任務
Crontab定時任務,在每個任務週期中執行一次特定任務,固無法控制執行次數
時間格式如下:
* * * * * program
- - - - - -
| | | | | |----- 要執行的程序
| | | | +----- 星期中星期幾 (0 - 7) (星期天 爲0) 默認*
| | | +---------- 月份 (1 - 12) 默認*
| | +--------------- 一個月中的第幾天 (1 - 31) 默認*
| +-------------------- 小時 (0 - 23) 默認*
+------------------------- 分鐘 (0 - 59) 默認*
我以兩個場景來設計實例代碼
- 每30打印一下當前時間;
- 在2020-06-21 00:00:00 (具體時刻)打印一下當前時間 ;
任務1:* * * * * echo $(date +%F%n%T)
含義每分鐘打印當前時間,最小執行週期每分鐘 無法實現30s的打印
任務2:* * 21 6 * echo $(date +%F%n%T)
含義每年的6月21日打印當前時間 無法實現只執行一次的打印
3. Python 循環+sleep
我以兩個場景來設計實例代碼
- 每30打印一下當前時間;
- 在2020-06-21 00:00:00 (具體時刻)打印一下當前時間 ;
任務1: 可以實現30s的打印
藉助循環和等待時間來實現任務執行,但是在一個線程組阻塞時不能進行其他操作,可以使用協程優化
import time
import datetime
# 每30秒執行
while 1:
time.sleep(30)
print(datetime.datetime.now())
任務2: 可以實現特定時間的打印
每次循環判斷當前時刻和目標是否一致,需要注意執行一次就需要跳出點前時刻的判斷,不然在同一時刻會重複打印很多次
import time
import datetim
# 特定時間執行
while 1:
now = datetime.datetime.now()
if now.year == 2020 and now.month == 6 and now.day == 21:
print(now)
break
time.sleep(1)
4. 線程Timer
看下源碼的描述
class Timer(Thread):
"""Call a function after a specified number of seconds:
t = Timer(30.0, f, args=None, kwargs=None)
t.start()
t.cancel() # stop the timer's action if it's still waiting
"""
在指定的秒數後調用函數
因爲創建的定時是異步執行,所以不存在等待順序執行問題。
- 創建定時器 Timer(interval, function, args=None, kwargs=None);
- 取消定時器cancel();
- 使用線程方式執行start();
- 等待線程執行結束join(self, timeout=None);
- 定時器只能執行一次,如果需要重複執行,需要重新添加任務;
我以兩個場景來設計實例代碼
- 每30打印一下當前時間
- 在2020-06-21 00:00:00 (具體時刻)打印一下當前時間
任務1: 可以實現30s的打印
雖然是可以實現沒30秒一次的打印,但是定時器只能執行一次,如果需要重複添加任務,創建線程又比較浪費系統資源
from threading import Timer
import datetime
import time
def run_time(seconds, flag=False):
t = Timer(seconds, run_time, [seconds, True])
t.start()
if flag:
print(datetime.datetime.now())
time.sleep(3)
t.join()
run_time(30)
任務2: 實現特定時間的打印
需要計算當前時間到目標時間間的秒數來實現
from threading import Timer
import datetime
import time
def run_time(seconds, flag=False):
if seconds > 0:
t = Timer(seconds, run_time, [seconds, True])
t.start()
if flag:
print(datetime.datetime.now())
time.sleep(3)
t.join()
def get_seconds(run_date):
start = datetime.datetime.now()
end = datetime.datetime.strptime(run_date, "%Y-%m-%d %H:%M:%S")
if end > start:
return (end-start).seconds
else:
return -1
seconds = get_seconds("2020-06-21 00:00:00")
run_time(seconds)
5. schedule模塊
首先看下源碼的描述
Python job scheduling for humans.
github.com/dbader/schedule
An in-process scheduler for periodic jobs that uses the builder pattern
for configuration. Schedule lets you run Python functions (or any other
callable) periodically at pre-determined intervals using a simple,
human-friendly syntax.
Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the
"clockwork" Ruby module [2][3].
Features:
- A simple to use API for scheduling jobs.
- Very lightweight and no external dependencies.
- Excellent test coverage.
- Tested on Python 2.7, 3.5 and 3.6
Usage:
>>> import schedule
>>> import time
>>> def job(message='stuff'):
>>> print("I'm working on:", message)
>>> schedule.every(10).minutes.do(job)
>>> schedule.every(5).to(10).days.do(job)
>>> schedule.every().hour.do(job, message='things')
>>> schedule.every().day.at("10:30").do(job)
>>> while True:
>>> schedule.run_pending()
>>> time.sleep(1)
任務1: 可以實現30s的打印
可以實現沒30秒一次的打印,還可以同時創建多個定時任務同時去執行,由於執行過程是順序執行,pn休眠2秒,循環任務查詢休眠1秒,會存在實際間隔時間並不是設定的30秒,當工作任務回非常耗時就會影響其他任務的觸發時間
import datetime
import schedule
import time
def pn():
print(datetime.datetime.now())
time.sleep(2)
schedule.clear()
schedule.every(30).seconds.do(pn)
while 1:
schedule.run_pending()
time.sleep(1)
任務2: 實現特定時間的打印
需要計算當前時間到目標時間間的秒數來實現
import datetime
import schedule
import time
def pn():
print(datetime.datetime.now())
time.sleep(2)
def get_seconds(run_date):
start = datetime.datetime.now()
end = datetime.datetime.strptime(run_date, "%Y-%m-%d %H:%M:%S")
if end > start:
return (end - start).seconds
else:
return -1
schedule.clear()
seconds = get_seconds("2020-06-21 00:00:00")
if seconds > 0:
schedule.every(seconds).seconds.do(pn)
while 1:
schedule.run_pending()
time.sleep(1)
6. APScheduler定時任務框架
APScheduler是Python的一個定時任務框架,用於執行週期或者定時任務,
可以基於日期、時間間隔,及類似於Linux上的定時任務crontab類型的定時任務;
該該框架不僅可以添加、刪除定時任務,還可以將任務存儲到數據庫中,實現任務的持久化,使用起來非常方便。
- triggers(觸發器):觸發器包含調度邏輯,每一個作業有它自己的觸發器;
- job stores(作業存儲):用來存儲被調度的作業,默認的作業存儲器是簡單地把作業任務保存在內存中,支持存儲到MongoDB,Redis數據庫中;
- executors(執行器):執行器用來執行定時任務,只是將需要執行的任務放在新的線程或者線程池中運行;
- schedulers(調度器):調度器是將其它部分聯繫在一起,對使用者提供接口,進行任務添加,設置,刪除。
add_job 創建任務
創建任務支持三種類型觸發器date
、interval
和cron
:
date觸發器
run_date
具體的日期執行,時間參數run_date,可以是dete/time格式的字符串,當不寫時分秒是 默認是 00:00:00
interval觸發器
weeks
參數weeks 每n周後執行,int類型days
參數days每n天后執行,int類型hours
: 參數hours每n小時後執行,int類型minutes
:參數minutes每n分鐘後執行,int類型seconds
: 參數seconds每n秒後執行,int類型datetime|str
參數start_date,可以是dete/time格式的字符串,控制執行循環的時間範圍datetime|str
參數 end_date,可以是dete/time格式的字符串,控制執行循環的時間範圍
cron觸發器
使用方式與 Crontab定時任務類似
year
int類型 取值範圍1970~9999, 默認*month
int類型 取值範圍1~12, 默認1day
int類型 取值範圍1~31, 默認1week
一年中的第幾周,int類型 取值範圍1~53, 默認*day_of_week
一週中的第幾天,int類型 取值範圍0~6, 默認*hour
int類型 取值範圍0~23, 默認0minute
int類型 取值範圍0~59, 默認0second
int類型 取值範圍0~59, 默認0
任務1: 可以實現30s的打印
使用interval
類型觸發器添加定時任務
可以實現沒30秒一次的打印,還可以同時創建多個定時任務同時去執行,由於執行過程是順序執行,pn休眠3秒,會存在實際間隔時間並不是設定的30秒,當工作任務回非常耗時就會影響其他任務的觸發時間
from apscheduler.schedulers.blocking import BlockingScheduler
import datetime
import time
def pn():
print(datetime.datetime.now())
time.sleep(3)
def run_time(seconds):
scheduler = BlockingScheduler()
scheduler.add_job(pn, 'interval', seconds=seconds, id=str(time.time()))
scheduler.start()
run_time(30)
任務2: 可以實現特定時間的打印
使用date
類型或cron
類型觸發器添加定時任務
from apscheduler.schedulers.blocking import BlockingScheduler
import datetime
import time
def pn():
print(datetime.datetime.now())
time.sleep(3)
def run_time():
scheduler = BlockingScheduler()
scheduler.add_job(pn, 'cron', year=2020, month=6, day=21, id=str(time.time()))
scheduler.add_job(pn, 'date', run_date="2020-06-21", id=str(time.time()))
scheduler.start()
run_time()
7. Celery分佈式任務調度的框架
Celery是實時處理的異步任務調度的框架,需要藉助中間件完成調度任務,也可以很好的用來做定時任務服務。
Celery是一個簡單、靈活且可靠的,處理大量消息的分佈式系統,由三部分組成:
- 消息中間件(message broker):Celery本身不提供消息服務,依賴於中間件RabbitMQ, Redis等
- 任務執行單元(worker): 是Celery提供的任務執行的單元,worker併發的運行在分佈式的系統節點中
- 任務執行結果存儲(task result store): 用來存儲Worker執行的任務的結果,Celery支持以不同方式存儲任務的結果,包括AMQP, Redis等
以上面代碼使用的 python3.7不同
當前版本 python2.7 Celery 3.2
目錄結構
celery_app_init_.py
# -*- coding: utf-8 -*-
# 拒絕隱式引入,如果celery.py和celery模塊名字一樣,避免衝突,需要加上這條語句
# 該代碼中,名字是不一樣的,最好也要不一樣
from __future__ import absolute_import
from celery import Celery
app = Celery('tasks')
app.config_from_object('celery_app.celeryconfig')
celery_app\celeryconfig.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from celery.schedules import crontab
from datetime import timedelta
# 使用redis存儲任務隊列
BROKER_URL = 'redis://127.0.0.1:6379/1'
# 使用redis存儲結果
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2'
# 時區設置
CELERY_TIMEZONE = 'Asia/Shanghai'
# 導入任務所在文件
CELERY_IMPORTS = [
'celery_app.celery_task.tasks',
]
# 需要執行任務的配置
CELERYBEAT_SCHEDULE = {
'np-seconds-10': {
# 具體需要執行的函數
# 該函數必須要使用@app.task裝飾
'task': 'celery_app.celery_task.tasks.pn',
# 定時時間
# 每30秒分鐘執行一次
'schedule': timedelta(seconds=30),
'args': ()
},
'np-month-day': {
'task': 'celery_app.celery_task.tasks.pn',
# 每年6月21日執行一次
'schedule': crontab(day_of_month=21, month_of_year=6),
'args': ()
},
}
celery_app\celery_task\tasks.py
# -*- coding: utf-8 -*-
from .. import app
import datetime
import time
@app.task
def pn():
print(datetime.datetime.now())
time.sleep(3)
我以兩個場景來設計實例代碼
- 每30打印一下當前時間
- 在2020-06-21 00:00:00 (具體時刻)打印一下當前時間
任務1: 可以實現30s的打印
'np-seconds-10': {
# 具體需要執行的函數
# 該函數必須要使用@app.task裝飾
'task': 'celery_app.celery_task.tasks.pn',
# 定時時間
# 每30秒分鐘執行一次
'schedule': timedelta(seconds=30),
'args': ()
},
任務2: 不可以實現指定時間的打印
一般指定時間也沒有意思,每年的某個月日的任務比較多
使用方式與 Crontab定時任務類似
'np-month-day': {
'task': 'celery_app.celery_task.tasks.pn',
# 每年6月21日執行一次
'schedule': crontab(day_of_month=21, month_of_year=6),
'args': ()
},
啓動定時任務
由於是分佈式服務,我們需要啓動 中間件,觸發器和處理模塊
start redis
celery_app 統計目錄下執行
發佈任務
celery -A celery_app beat
執行任務
celery worker -A celery_app -l info
總結
簡單總結上面七種定時定點任務實現:
- Windows任務計劃程序合適重啓服務,定時執行些bat腳本,清理日誌等操作;
- Linux Crontab定時任務適合執行shell腳本或者一系列Linux命令;
- 循環+sleep方式適合簡單測試;
- Timer可以實現定時任務,但是對定點任務來說,需要檢查當前時間點;
- schedule可以定點定時執行,但是需要在循環中檢測任務,而且存在阻塞;
- APScheduler框架更加強大,可以直接在裏面添加定點與定時任務;
- Celery框架穩定,可動態添加任務,與Django、Flask等框架可以完美結合使用,對於大任務量的服務支持比較好。