简介:本资源是一份面向Python初学者与自动化运维实践者的定时任务实战教程,聚焦每日定时执行场景,如股票数据自动采集、系统监控或报表生成等典型需求。压缩包共5个Python源文件,总大小仅2KB,结构清晰:stock_main.py为程序入口,day_tasks.py封装日级定时逻辑,sec_tasks.py预留秒级扩展能力,stock_functions.py提供业务函数支持,stock_settings.py统一管理API密钥与执行时间等配置项。资源详细对比了schedule与APScheduler两大主流库的实现方式——前者语法简洁适合轻量调度,后者支持Cron表达式与后台常驻,更适配生产环境。已有2036人学习下载,内容涵盖完整可运行代码、配置分离设计、异常处理提示及视频教程参考(BV1tR4y1W7ec),开箱即用,便于快速掌握定时任务工程化落地的关键细节。
1. 不用写服务、不改系统配置,Python 每天定时执行任务的轻量级落地方案
你写好了一个数据清洗脚本,想每天早上 8 点自动跑一次;或者有个日志归档函数,需要在凌晨 2 点准时触发——但又不想碰 Linux 的 crontab,不想部署 systemd service,更不想搭一套 Airflow 或 Celery。这种「单机、单任务、低维护」的每日定时需求,在运维自动化、数据分析、本地实验场景中极其高频。Python 自身没有内置的“定时器守护进程”,但通过组合标准库与少量第三方工具,完全可以在 Windows/macOS/Linux 上实现稳定、可调试、无外部依赖的每日调度。本文聚焦最简路径:用schedule库定义时间规则 +threading或subprocess保活 + 日志与异常兜底,全程不修改系统级定时服务,所有代码可直接复制运行,且能清晰看到任务是否真正触发、失败在哪一行、下次何时执行。
2. 用 schedule 定义每日任务:语法简洁、语义明确、支持复杂周期
schedule是 Python 生态中专为轻量定时任务设计的库,它不启动后台服务、不写入系统配置、不监听端口,只提供一套人类可读的时间表达 DSL(Domain Specific Language)。相比APScheduler的多模式抽象或手写time.sleep()循环,schedule在“每天执行一次”这类场景下,代码更短、逻辑更直、调试更透明。
2.1 安装与基础结构:三行代码启动一个每日任务
pip install schedule安装后,最小可运行示例如下:
import schedule import time def job(): print("任务执行于:", time.strftime("%Y-%m-%d %H:%M:%S")) # 每天上午 9 点执行 schedule.every().day.at("09:00").do(job) # 启动调度循环(阻塞式) while True: schedule.run_pending() time.sleep(60) # 每分钟检查一次提示:
schedule.run_pending()是核心调用,它扫描所有注册任务,检查是否到达触发时间。time.sleep(60)并非精确到秒级,而是控制检查频率——值越小越及时,但 CPU 占用越高;60 秒是平衡日常使用与资源消耗的常见选择。
2.2 支持的每日时间表达方式:覆盖真实业务中的 95% 场景
schedule对“每天执行”的支持远不止.at("HH:MM")。以下是最常被搜索但文档未突出说明的几种写法,全部实测有效:
| 写法 | 含义 | 示例 | 适用场景 |
|---|---|---|---|
every().day.at("08:30") | 每天固定时刻 | "08:30"、"23:59" | 数据报表生成、邮件发送 |
every(24).hours | 每 24 小时一次(从首次注册起算) | every(24).hours.do(job) | 需要严格间隔而非日历日的任务,如 API 调用配额重置 |
every().monday.at("09:00") | 每周一 9 点 | 可替换为tuesday/wednesday… | 周报汇总、周度备份 |
every().day.at("12:00").tag("midday") | 添加标签便于后续管理 | .clear("midday")可批量取消 | 多任务共存时按组启停 |
注意:
.at("HH:MM")中的HH必须为 24 小时制,且MM不能省略(即"9:00"会报错,必须写"09:00")。这是新手最常踩的格式坑。
2.3 为什么不用 threading.Timer 或 while+sleep?对比分析
有人会问:既然只是“每天执行”,为何不自己写个死循环?
# ❌ 不推荐:手动 sleep 容易漂移、难管理、无日志 import time while True: now = time.localtime() if now.tm_hour == 9 and now.tm_min == 0: job() time.sleep(60)问题在于:
- 时间判断依赖
tm_hour/tm_min,跨天时需额外处理tm_mday; - 若
job()执行超时(如网络卡顿),下次触发将延迟; - 无法动态增删任务,重启即丢失;
- 无失败重试、无执行记录、无下次预计时间查询。
而schedule内部已封装上述逻辑:它基于datetime计算下次触发时间,支持.next_run属性实时查询,且.do()接收任意 callable,包括带参数的函数、类方法、lambda 表达式。
3. 让任务真正“每天执行”:进程保活、异常捕获与日志落盘
仅靠schedule定义任务还不够——Python 脚本退出,任务就终止。必须让调度器长期运行,同时确保单次失败不影响后续执行,并留下可追溯的操作痕迹。
3.1 使用 threading 启动独立调度线程:避免阻塞主程序
若你的主程序还需做其他事(如 Flask 接口响应、PyQt 界面交互),不能让while True占用主线程。此时应将调度器放入后台线程:
import schedule import time import threading import logging # 配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("daily_job.log", encoding="utf-8"), logging.StreamHandler() ] ) def daily_backup(): try: logging.info("开始执行每日备份...") # 这里放你的实际逻辑,例如 shutil.copytree / subprocess.run logging.info("备份完成") except Exception as e: logging.error(f"备份失败: {e}") # 注册任务 schedule.every().day.at("02:00").do(daily_backup) # 启动调度线程(守护线程,主程序退出时自动结束) def run_scheduler(): while True: schedule.run_pending() time.sleep(60) scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) scheduler_thread.start() # 主程序继续执行其他逻辑(此处仅为演示,可删除) logging.info("调度器已启动,等待每日 02:00 触发...") time.sleep(86400) # 模拟主程序长期运行逻辑说明:
daemon=True确保该线程不会阻止主程序退出;logging同时输出到文件和控制台,便于线上排查;try/except包裹任务体,防止一次异常导致整个调度器崩溃。
3.2 使用 subprocess 替代 threading:隔离环境、避免 GIL 争抢
当任务本身是 CPU 密集型(如 Pandas 大表处理、OpenCV 图像批处理),或需独立 Python 环境(如不同版本依赖),subprocess比threading更安全:
import subprocess import sys def run_script_in_subprocess(): # 调用另一个 .py 文件,完全隔离 result = subprocess.run( [sys.executable, "backup_script.py"], capture_output=True, text=True, timeout=3600 # 最长运行 1 小时,超时强制终止 ) if result.returncode != 0: logging.error(f"子进程异常退出: {result.stderr}") else: logging.info(f"子进程输出: {result.stdout[:200]}...") schedule.every().day.at("02:00").do(run_script_in_subprocess)参数说明:
sys.executable动态获取当前 Python 解释器路径,兼容虚拟环境;timeout=3600防止脚本卡死,subprocess会抛出subprocess.TimeoutExpired异常;capture_output=True和text=True使stdout/stderr为字符串而非 bytes,便于日志记录。
3.3 关键配置表:每日任务稳定运行的 5 个必调参数
| 参数位置 | 默认值 | 推荐值 | 作用说明 | 检索关联词 |
|---|---|---|---|---|
time.sleep()间隔 | — | 60(秒) | 控制检查频率,值过小增加 CPU,过大导致延迟 | python定时执行任务 精度 |
logging.level | WARNING | INFO | 记录任务触发、成功、失败,是排错第一依据 | python日志记录 |
subprocess.timeout | None | 3600 | 防止单次任务无限挂起,保障每日节奏不被破坏 | python超时控制 |
schedule.clear(tag) | — | 按需调用 | 多任务场景下,可通过 tag 分组管理启停 | python定时任务管理 |
schedule.next_run() | None | print(schedule.next_run()) | 实时查看下次执行时间,用于验证配置是否生效 | python查看下次执行时间 |
4. 实战:从零部署一个每天 7 点抓取天气数据并存 CSV 的完整脚本
现在把前述所有要点整合成一个可直接运行的生产级脚本。目标:每天早上 7:00 调用公开天气 API,获取北京温度,追加写入weather.csv,失败时发邮件告警(使用 SMTP,不依赖第三方服务)。
4.1 依赖安装与环境准备
pip install schedule requests openpyxl # 如需邮件告警,还需安装:pip install yagmail(轻量 SMTP 封装)注意:
yagmail仅需邮箱账号密码(或 App Password),无需搭建邮件服务器,符合“不改系统配置”原则。
4.2 完整可运行脚本(含注释与错误兜底)
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 每日天气采集脚本:每天 07:00 执行 功能:调用和风天气免费 API → 提取温度 → 追加到 weather.csv → 失败时邮件通知 """ import schedule import time import csv import requests import logging import os from datetime import datetime import yagmail # pip install yagmail # ====== 配置区(按需修改)====== API_KEY = "YOUR_HEFENG_API_KEY" # 申请地址:https://www.heweather.com/ CITY_ID = "CN101010100" # 北京 CSV_FILE = "weather.csv" SMTP_USER = "your_email@gmail.com" SMTP_PASSWORD = "your_app_password" # Gmail 需用 App Password ALERT_EMAIL = "admin@company.com" # ====== 日志配置 ====== logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("weather_job.log", encoding="utf-8"), logging.StreamHandler() ] ) # ====== 核心任务函数 ====== def fetch_and_save_weather(): try: logging.info("开始获取天气数据...") url = f"https://devapi.qweather.com/v7/weather/now?location={CITY_ID}&key={API_KEY}" resp = requests.get(url, timeout=10) resp.raise_for_status() data = resp.json() # 解析温度(单位℃) temp = data["now"]["temp"] obs_time = data["lastUpdate"] # API 返回的时间戳 # 写入 CSV(追加模式,首行是 header) file_exists = os.path.isfile(CSV_FILE) with open(CSV_FILE, "a", newline="", encoding="utf-8") as f: writer = csv.writer(f) if not file_exists: writer.writerow(["date", "time", "temperature_c", "api_update_time"]) writer.writerow([ datetime.now().strftime("%Y-%m-%d"), datetime.now().strftime("%H:%M:%S"), temp, obs_time ]) logging.info(f"天气数据已保存:{temp}℃") except requests.exceptions.RequestException as e: error_msg = f"网络请求失败: {e}" logging.error(error_msg) send_alert(error_msg) except KeyError as e: error_msg = f"API 响应字段缺失: {e}" logging.error(error_msg) send_alert(error_msg) except Exception as e: error_msg = f"未知错误: {type(e).__name__}: {e}" logging.error(error_msg) send_alert(error_msg) def send_alert(message): """发送告警邮件""" try: yag = yagmail.SMTP(user=SMTP_USER, password=SMTP_PASSWORD, host='smtp.gmail.com', port=587) yag.send( to=ALERT_EMAIL, subject=f"[天气采集告警] {datetime.now().strftime('%Y-%m-%d %H:%M')}", contents=f"任务执行失败,详情:\n\n{message}\n\n请检查 weather_job.log" ) logging.info("告警邮件已发送") except Exception as e: logging.critical(f"邮件发送失败,可能影响告警:{e}") # ====== 注册任务并启动 ====== schedule.every().day.at("07:00").do(fetch_and_save_weather) logging.info("天气采集任务已注册,等待每日 07:00 触发...") logging.info(f"下次执行时间:{schedule.next_run()}") # 启动调度(主线程运行) while True: schedule.run_pending() time.sleep(60)4.3 首次运行与验证步骤
- 替换配置项:填入你的和风天气 API Key、邮箱账号与 App Password;
- 测试 API 调用:临时在脚本末尾加
fetch_and_save_weather()并运行,确认能生成weather.csv; - 验证时间逻辑:修改
at("07:00")为at("15:30"),观察 30 分钟后是否触发; - 模拟失败场景:断网后运行,检查
weather_job.log是否记录错误,并确认收到告警邮件; - 长期驻留:在 Linux 上用
nohup python weather_job.py > /dev/null 2>&1 &启动;Windows 可用pythonw.exe静默运行。
提示:
nohup命令确保终端关闭后进程不退出;2>&1将 stderr 合并到 stdout,避免日志分裂;&放入后台。这是 Linux 下最轻量的“伪服务化”方案。
5. 进阶技巧:动态调整时间、热重载任务、监控下次执行时间
当每日任务进入稳定期,你会遇到这些真实需求:临时跳过某天执行、根据数据库状态动态决定是否运行、或想在 Web 页面上查看下次触发时间。schedule原生支持这些能力,无需引入复杂框架。
5.1 动态禁用/启用某天任务:用job.tag()+clear()
假设今天是节假日,你想跳过备份:
# 在任务注册时打标签 schedule.every().day.at("02:00").do(daily_backup).tag("backup") # 运行时根据条件清除 if is_holiday(today): schedule.clear("backup") logging.info("今日为节假日,跳过备份")技巧:
schedule.jobs是一个列表,可遍历打印所有待执行任务;schedule.clear("tag_name")会移除所有匹配标签的任务。
5.2 查询下次执行时间:用于 Web API 或 CLI 状态页
很多用户搜索“python查看定时任务下次执行时间”,答案就是.next_run属性:
# 在任意位置调用 next_time = schedule.jobs[0].next_run if schedule.jobs else None if next_time: print(f"下次执行:{next_time.strftime('%Y-%m-%d %H:%M:%S')}") else: print("暂无待执行任务")结合 Flask,可快速暴露一个健康检查端点:
from flask import Flask app = Flask(__name__) @app.route("/health") def health(): next_run = schedule.jobs[0].next_run if schedule.jobs else None return { "status": "ok", "next_run": next_run.isoformat() if next_run else None, "pending_jobs": len(schedule.jobs) }访问http://localhost:5000/health即可看到实时调度状态。
5.3 热重载任务配置:不重启进程更新执行时间
schedule不支持直接修改已注册任务的时间,但可通过“先清空再重建”实现热重载:
def reload_schedule(new_time="03:00"): schedule.clear("backup") # 清除旧任务 schedule.every().day.at(new_time).do(daily_backup).tag("backup") logging.info(f"备份时间已更新为 {new_time}") # 外部调用此函数即可(例如从 Redis 读取新时间) reload_schedule("03:00")边界说明:该操作是原子的,不会导致任务漏执行;但若在
run_pending()执行中途调用clear(),当前正在运行的任务不受影响,仅后续调度按新规则进行。
5.4 任务执行耗时统计:识别性能瓶颈的简易方法
在任务函数开头结尾加时间戳,是定位慢任务最直接的方式:
def daily_backup(): start_time = time.time() try: # ... 实际逻辑 ... pass finally: duration = time.time() - start_time logging.info(f"本次备份耗时 {duration:.2f} 秒") if duration > 1800: # 超过 30 分钟告警 send_alert(f"备份耗时过长:{duration:.2f} 秒")配合日志分析工具(如grep "耗时" weather_job.log | awk '{print $NF}' | sort -n),可快速发现趋势性变慢。
本文还有配套的精品资源,点击获取