杂记 › 技术笔记 › Crontab 表达式 0 0 13 * 5 到底哪天跑:这一页只排 2 次,真实 cron 是 62 次

Crontab 表达式 0 0 13 * 5 到底哪天跑:这一页只排 2 次,真实 cron 是 62 次

同一条 0 0 13 * 5,这一页按交集只排 2 次,真实 cron 的并集是 62 次;短月 31 号被跳过,永不存在日期扫满 527040 分钟才返回空。

同一条 crontab,测试页和线上跑在不同日子

接手一台闲置机器时见过这样一行:0 0 13 * 5,注释写着「每月 13 号周五跑一次 demo-report.csv」。把这行丢进解析页,第一条预测时间是 2024-09-13;可执行日志里每个周五都躺着一行记录。同一个表达式,一边说 2024 年只有 2 次,一边真实跑了 62 次——中间差的 60 次不是运气,是两套语义。

这一页的输入只有一行文本。我把 Crontab解析 模板里的 nextRun 与 parseField 逐字抽出来,起点钉死在 2024-01-01 00:00:00 逐分钟往后走,量到两个语义空洞:日和周同时限定时的交集与并集,以及短月、永不存在日期下的空返回。

空洞一:五个字段被 && 串成一串

命中判定写在模板 240–244 行,条件 f.minute && f.hour && f.day && f.month && f.week 是五段全交集,而真实 vixie-cron 在「日」与「周」都不是 * 时走「或」。给 2024 年算一笔账:13 号有 12 天,周五有 52 天,既是 13 号又是周五只有 2 天,并集是 12 + 52 - 2 = 62,交集只剩那 2 天,页面等于把 62 次压成 2 次、差 60 次。

描述文字也站在交集这边:它给出的自然语言是「在每小时的第0分钟0点每月13日周五执行一次」,读起来是「且」,与 nextRun 自洽,却一起偏离 cron 本身。五个字段各自是什么、界面怎么演示,站内 Crontab表达式看不懂?在线解析并预测下次执行时间 讲透了,这里只补它没量过的分支。

同一个 0 0 13 * 5,2024 年命中几次 本页 nextRun 交集 2 次 真实 vixie-cron 并集 62 次 12 个 13 号 + 52 个周五 - 2 个交集 = 62

图 1:两种读法在同一年窗口里的命中数。

只有一个字段受限时,两套语义合流

表达式本页给出的前几次(上海时区)歧义
0 0 * * 12024-01-08、01-15、01-22、01-29、02-05,全是周一 00:00无,只有周受限
0 0 1 * *2024-02-01、03-01、04-01、05-01、06-01无,只有日受限
0 0 13 * 52024-09-13、2024-12-13、2025-06-13、2026-02-13、2026-03-13、2026-11-13、2027-08-13有,第 8 次返回空

前两行只限定一个字段,交集并集重合、链条规矩;日和周同时收窄时,第一次命中直接推到 2024-09-13,而线上每个周五照跑。

空洞二:日历本身不给那一天

0 0 31 * * 从 2024-01-01 起链 12 次,只命中 7 个月:1、3、5、7、8、10、12 月,2、4、6、9、11 月被直接跳过。从 2 月往后问下一次,它答 2024-03-31,不报错也不解释;闰年 2024 同样是 7 个含 31 号的月份。

更极端的是 2 月 30 日。0 0 30 2 * 字段值与字段数都合法,解析一路通过,nextRun 老老实实扫满 366 × 24 × 60 = 527040 分钟再返回空,实测单次约 58 毫秒,不死循环也不抛异常,界面只留一句「未找到下次执行时间(一年内)」。0 0 31 2 * 走同一条路。

这个上限还会误伤活着的排程:0 0 13 * 5 跑到第 7 次 2027-08-13 之后,下一个「既是周五又是 13 号」落在 2028-10-13,距上一次超过 366 天,于是第 8 次同样显示未找到——稀疏不等于不存在,但页面上这两种情况文案一模一样。另一条静默路径是反向区间 8-1:字段被解析成空集且不报错,此后无论怎么问都只返回空。

报错文案:英文名一律不是名字

输入页面抛出的原文
SUN/sun/Sat/FRI无效的值: SUN(大小写都一样,没有名字表)
周字段写 8值 8 超出范围 [0-7]
61 * * * *值 61 超出范围 [0-59]
0 0 0 * *值 0 超出范围 [1-31](日的下限是 1)
*/0 * * * *无效的步长: */0
四段或六段表达式必须为5个字段(分 时 日 月 周)

周字段里 7 与 0 等价,parseCron 第 145 行把 7 归成 0,所以 0 0 * * 7 与 0 0 * * 0 同解;可 FRI 这种真实 cron 认得的缩写,在这里只是 parseInt 出来的一个 NaN。字段数校验排在拆分之后,于是 0 0 1, * * * 这种「逗号后多打一个空格」被拆成六段,报的仍是那句五字段提示。

预测还随环境漂

nextRun 读的是本地时钟(getHours、getDate、getDay),第 229 行先把秒归零,输入里带的秒被丢掉;起点还是独占的,after 正好压在命中时刻上时不会把自身再报一遍。我把起点固定成上海时区的 2024-01-01 00:00,换算成毫秒就是 时间戳转换 给的 1704038400000,免得每跑一次换个结果。

夏令时是最直观的一刀:本机解析出的时区是 Asia/Shanghai,没有夏令时,必须强制 TZ=America/New_York 才触发得到。那一档里 0 2 * * * 从 2024-03-08 起命中 2024-03-08、2024-03-09、2024-03-11、2024-03-12——春季前拨把 3 月 10 日的 02:00 整个抹掉,预测从来不是纯表达式决定的。

这一页自己的体量

顺带量一下:模板 337 行、13855 字节,7 个 id、14 个 button、1 个 input,逻辑全在前端。解析按钮固定循环 10 趟去生成「未来 10 次执行时间」,每趟都自带那 527040 分钟上限,任何一趟返回空就当场收尾,所以稀疏的 0 0 13 * 5 只列出 7 条。

怎么把两套语义落回同一条

要「每月 13 号或每周五」,别指望一行表达式两全,拆成 0 0 13 * * 与 0 0 * * 5 两行,把并集交给调度器;要「13 号恰好是周五」,就让定时只管触发、脚本自己判日期,此时那 2 次预测正好对得上。验证的办法很笨但有效:把页面前几次命中的星期和日子逐个念一遍。

常见疑问

Q:这一页给的下次执行时间能直接照抄上线上吗?

只限定日或只限定周时可以,两种读法在那时重合;两段同时收窄就不行,本页算交集,线上算并集。

Q:出现「未找到下次执行时间(一年内)」是不是任务永远不跑?

不一定。永不存在日期 0 0 30 2 * 与合法的稀疏排程都会走到这句话,前者是日历里没有那天,后者只是下一个命中点超出 366 天的遍历上限。

Q:周日写 7 还是写 0?

在这页等价,第 145 行把 7 归 0;但 SUN、sat 一律按无效值抛错,别把真实 cron 的缩写习惯带进来。

📎 相关文章

粘贴 100 行分数,第 45 行被悄悄算成 1/4:约分这页的负号、空行和大数各归谁管 2026-11-03 把 3. 起步贴进 Markdown 转 HTML,为什么有序列表的编号只认第一个数 2026-11-03 文本切成 43 片再拼回去少了 42 个字节,切口其实都落在换行上 2026-11-03 16 组开关实测 HTML 去标签:报 19 个标签,实际删掉 148 个字符 2026-10-30 SVG 转 PNG 后外链图丢了一半:同一份 594 B 文件预览有 3 张图、成品只剩 1 张 2026-10-30