本篇复用上一篇回写后的 sync.db 和下载摘要,以 load_completed_items() 的查询结果作为 report_job.py 输入;浏览器任务尚未完成的业务键不进入日报。新增的报告文件和完成标记又会成为下一篇测试与日志的观察对象。日报风险不在公式,而在重复触发、数据晚到或状态未保存。
一、用日期与配置生成唯一任务键
python
import hashlib
import json
from datetime import date
from pathlib import Path
import sqlite3
config = {"region": "east", "currency": "CNY"}
raw = json.dumps(config, sort_keys=True, ensure_ascii=False)
job_key = f"{date.today()}-{hashlib.sha256(raw.encode()).hexdigest()[:8]}"
marker = Path("state") / f"{job_key}.done"
marker.parent.mkdir(exist_ok=True)
def load_completed_items(path: Path) -> list[tuple[str, str]]:
if not path.is_file():
raise FileNotFoundError("先运行 article_076 回写 sync.db")
with sqlite3.connect(path) as db:
return db.execute(
"select id, payload from items where export_sha256 is not null order by id"
).fetchall()
def main() -> int:
if marker.exists():
print(f"skip completed {job_key}")
return 0
try:
rows = load_completed_items(Path("sync.db"))
except (OSError, sqlite3.Error) as exc:
print(f"report_failed type={type(exc).__name__}")
return 2
print(f"run {job_key} rows={len(rows)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
输出示例:
text
run 2026-07-27-85d2434e
配置变化会得到不同任务键,同一配置在同一天重复触发则安全退出。
二、先写临时文件,再原子替换
python
import os
from pathlib import Path
target = Path("reports/daily.csv")
staging = target.with_suffix(".tmp")
target.parent.mkdir(exist_ok=True)
staging.write_text("metric,value\norders,42\n", encoding="utf-8")
os.replace(staging, target)
marker.write_text("ok\n", encoding="utf-8")
运行输出:
text
published=reports/daily.csv marker=state/2026-07-27-85d2434e.done
只有报告完整落盘后才创建完成标记。发送邮件或上传网盘属于另一个步骤,应记录独立状态,避免重新计算报告。
三、调度器只负责触发与收集退出码
任务应支持手动运行,退出码区分完成、已处理和失败。上线前模拟数据源缺失、磁盘不可写与重复触发,确认不会留下半份报告。
四、为什么原子替换仍不等于业务完成
os.replace() 保证读者看到旧文件或完整新文件,不会看到半份内容,但它不保证邮件已经发出,也不保证网盘上传成功。生成、发布、通知是三个不同状态,若用一个布尔值概括,通知失败后的重跑可能重复生成或重复发送。状态表应记录 generated_at、published_at 和 notified_at,每一步只推进自己的状态。
完成标记必须最后写,而且内容应包含输入摘要、配置摘要和报告摘要。只用日期作为任务键会把同日不同区域互相覆盖;把排序后的配置摘要加入键,既能区分业务范围,又不会受 JSON 字段顺序影响。记忆点是:原子性保护一扇门,状态机保护整条走廊。文件替换解决单次写入,状态转换才解决跨系统流程。
五、确定性让重跑成为恢复手段
相同快照、配置和业务日期应生成相同字节。查询必须显式排序,日期与小数格式必须固定,报告中不应混入当前运行时间。这样发送失败后可以安全复用已有报告,并用摘要证明内容未变。若上游数据迟到,应产生新的输入版本,而不是偷偷覆盖已验收日报。
调度器只负责在约定时间调用命令并收集退出码,业务代码自己判断已完成、可重试和永久失败。验收要模拟并发启动、磁盘不足、通知失败和重复触发。下一篇会复用 job_key、状态标记和固定数据库样本,为这些恢复分支建立测试与结构化事件。
并发启动还需要互斥,而不能只做 marker.exists() 后再写标记,因为两个进程可能同时看到"不存在"。可用数据库唯一键或操作系统文件锁竞争执行权,赢家生成报告,输家读取既有状态后退出。锁必须包含持有者、启动时间和过期规则,避免机器断电留下永久死锁;清理过期锁前还要确认对应进程已不存在。
数据晚到则是业务版本问题,不应靠调度器不断覆盖当天报告。任务键加入数据截止时间或上游批次号,补数会形成新版本并保留旧版摘要;通知里明确标注"修订版"。这让客户能够解释两个同日期文件为什么不同,也让审计者复现当时采用的数据快照。稳定的版本语义比"每天九点跑一次"更接近真正的报告契约。
📌 本文用任务键、原子文件与完成标记设计了可重复触发的定时报告,让生成和发送各自可恢复。
💬 你的定时任务更常遇到重复执行、数据晚到、发送失败还是无人发现?
👉 关注《Python 自动化接单实战》,下一篇继续用测试与结构化日志降低后续维护成本。