早上好,我是まさきん。
家里的提醒和AI定时任务,越来越多地用cron格式来设置了。其中在尝试写”每月第1个星期六”时,我才发现自己结结实实地踩了一个坑。用同样写法的人可能意外地不少,所以把规范和规避方法整理一下。
※本文为发布时的信息。最新条件请以官方网站为准。
- 在cron中同时指定日期和星期,判断方式不是AND,而是OR
- 所以”0 9 1-7 * 6”不是第1个星期六,而是1〜7日的每一天加上每个星期六都会执行
- 规避方法是只用星期来触发,日期的判断交给脚本
- 部分运行环境有表示”第几个星期几”的扩展语法,但能否使用取决于环境
- 写完之后把接下来几次的执行日期列出来验算,并确认和UTC之间的时差
本想每月一次,结果月初每天都触发
起因是月初星期六发送的采购提醒。每月只要提醒一次就行,所以我是这样写的。
0 9 1-7 * 6
“1〜7日”并且”星期六”,应该就只会锁定每月第一个星期六这一天。按人的直觉,确实会这么理解吧。
但实际的行为并非如此。它变成了每月1〜7日的每一天、再加上每周的星期六都会触发。本想每月一次,结果成了月初前半段每天早上都会收到的提醒。
回顾一下5个字段
cron格式由空格分隔的5个字段组成。从左到右依次是下面的含义。
分 时 日 月 星期
0 9 1-7 * 6
”*“表示”任意”。也可以写”1-7”这样的范围,或”1,15”这样的列举。星期在很多环境中0是星期日,6是星期六。
到这里为止机制都很直观。问题出在”日”和”星期”这两个字段之间的关系上。
日期和星期按OR判断
POSIX的crontab规范中写有如下意思的内容,说的是日字段和星期字段都被具体指定的情况:此时,只要匹配其中任一个,那一天就成为执行对象。
Linux上常用的cronie的man页面也有同样的说明:当两个字段都不是”*“时,只要其中一个匹配就会执行。并举例说明,“30 4 1,15 * 5”会在每月1日和15日,再加上每周五的4点30分执行。
也就是说,一旦同时写了日期和星期,就不是”并且”而是”或者”了。个人觉得,这是cron里最反直觉的地方。
- 在日字段和星期字段都写了”*“以外的内容时,只要匹配其中一个就会执行
- 即使本意是”1-7日并且星期六”,实际也会变成”1-7日或者星期六”
- 只要其中一个是”*“,就只按另一个判断,不会出问题
实际列出来看看怎么执行
以2026年10月为例数一数。把”0 9 1-7 * 6”会执行的日期列出来,结果如下。
| 写的表达式 | 意图 | 2026年10月执行的日期 | 次数 |
|---|---|---|---|
| 0 9 1-7 * 6 | 只在第1个星期六 | 1〜7日每天,以及10·17·24·31日 | 11次 |
| 0 9 * * 6 | 每个星期六 | 3·10·17·24·31日 | 5次 |
| 0 9 * * 6 + 日期判断 | 只在第1个星期六 | 3日 | 1次 |
本意是每月一次,算下来却会收到11次。1〜7日中的星期六是重叠的,所以比简单相加要少一点。
提醒嘛,收到通知是理所当然的事。所以即使次数太多,可能也不太容易察觉到不对劲。
主要运行环境中的处理
cron格式在不同的运行环境中会有些许差异。我把在官方文档中能确认的内容整理成了表格。
| 运行环境 | 星期的计数方式 | 同时指定日期和星期 | 扩展语法 | 时间基准 |
|---|---|---|---|---|
| POSIX的crontab | 0〜6(0为星期日) | 匹配任一即执行 | 规范中没有 | 规范中未明确 |
| Linux的cronie | 0〜7(0和7为星期日) | 匹配任一即执行 | 未明确 | 可用CRON_TZ指定 |
| Cloudflare Workers的Cron Triggers | 1〜7(1为星期日) | 未明确 | 支持L·W·# | UTC |
| GitHub Actions的schedule | 0〜6(0为星期日) | 写明使用POSIX格式 | 不支持@daily等 | 默认UTC,也可指定timezone |
需要注意的是Cloudflare的星期。1是星期日、7是星期六,和其他环境错开了1位。官方也因为容易混淆,推荐使用SAT这样的3字母缩写。
GitHub Actions写明使用POSIX的cron格式,但页面上没有写日期和星期之间的关系。所以按照和POSIX一样是OR的前提来设计,应该比较稳妥。
规避方法是”按星期触发,再看日期”
最可靠的规避方法,是把其中一个字段设为”*“。只用星期来触发,日期的判断放在脚本里做。
- cron写成”0 9 * * 6”,每周六9点触发
- 触发后,获取今天的日期
- 如果日期是1〜7日,就发送提醒
- 如果是8日以后,什么都不做直接结束
第1个星期六一定落在1〜7日之间。所以只要判断”星期六并且日期在7以下”,就只会每月执行一次。相当于AND条件不交给cron,而是自己在代码里写。
这个思路可以举一反三。第2个星期六就是8〜14日,第3个星期六就是15〜21日。月末最后一个星期六,则用”7天后是否已到下个月”来判断。
用Shell写的例子
如果直接写在Linux的crontab里,例如可以写成下面这样。
0 9 * * 6 [ "$(date +\%d)" -le 7 ] && /path/to/remind.sh
这里的关键是”%“前面加了反斜杠。cronie的man页面写到,命令中的”%“会被当作换行处理。不转义的话,命令就会以意想不到的方式被截断。
如果一行太长,把判断整个放进脚本里可能更易读。crontab那边只保留”每周六触发”,以后回头看也不容易迷糊。
用在AI定时任务里的情况
把定时任务交给AI时,思路也一样。日程设为”每周六9点”,然后在指令开头写上”如果今天不是1〜7日,就什么都不做直接结束”。
如果用Python判断,例如可以这样写。
from datetime import datetime
from zoneinfo import ZoneInfo
today = datetime.now(ZoneInfo("Asia/Tokyo"))
if today.day > 7:
raise SystemExit(0) # 不是第1个星期六,什么都不做
这里明确指定日本时间很重要。运行环境的时钟很多时候是UTC,这样可以避免把日期弄错。
- cron这边一定要把其中一个字段设为”*”
- 第几个星期几的判断,写在脚本或指令那一侧
- 获取日期时要明确指定时区
也有能用扩展语法的环境
有些运行环境提供了扩展,可以用一个表达式写出”第几个星期几”。Cloudflare Workers的Cron Triggers在表格中列出了星期字段可以使用”#“。说明是支持接近Quartz这个调度器的扩展。
不过,官方页面上并没有给出使用”#“的具体示例。Cloudflare的星期数字从1开始,写法上尤其要小心。如果要用,最好在部署前确认它会在预想的日期执行。
另一方面,POSIX规范和cronie的man页面里都没有出现”#“。GitHub Actions的表格里也没有这个符号。考虑到迁移环境后可能无法运行,把日期判断放到代码里的方法或许更便于移植。
| 方法 | 写起来的难度 | 迁移环境时 | 适合的场景 |
|---|---|---|---|
| 按星期触发并判断日期 | 代码会稍微多一点 | 通常可以直接运行 | 家庭自动化的各种场景 |
| ”#“等扩展语法 | 一行就能写完 | 不支持的环境无法使用 | 只在明确支持的环境中使用 |
| 同时写日期和星期 | 看起来简单 | 在哪里都容易变成OR | 最好不要用 |
列出下次执行日期来验算
写好cron表达式后,推荐把接下来几次的执行时间列出来确认一下。这次的坑,只要列出接下来几次,一眼就能看出来。
也有像crontab.guru这样,能把表达式转换成自然语言的网站。GitHub Actions的官方文档中,也把它介绍为可用于编写格式和确认执行时间的工具。不过它是第三方工具,显示结果不一定和运行环境的实际行为完全一致。扩展语法的处理等,最好结合各环境的官方文档一起看。
自己确认的话,把一个月的日期循环一遍、数出符合条件的日子就足够了。本文10月的表格,也是把日期一天天列出来数的。几行代码就能搞定,不妨养成每次修改表达式都跑一遍的习惯。
也要注意UTC时差
另一个陷阱是时区。Cloudflare的Cron Triggers明确写着按UTC运行。GitHub Actions默认也是UTC,同时提供了指定timezone的方法。
日本时间比UTC快9小时。所以日本时间早上9点,在UTC正好是0点。比这更早的时间,情况就不一样了。例如日本时间星期六8点,在UTC是星期五23点。
这种情况下,cron的星期字段就需要写星期五而不是星期六。而且脚本里的日期判断,如果不用日本时间来做也会出现偏差。最好记住:星期和日期都有可能偏差1天。
家庭财务自动化中容易踩坑的场景
我感觉,这个坑在设置每月一次的提醒时特别容易踩。比如下面这些场景。
- 在月初的星期六,提醒自己去集中采购
- 在第1周的周末,检查信用卡账单金额
- 在每月第一个休息日,检查订阅的续费或取消
- 在第2个星期六,确认孩子学校相关的缴费
这些都很容易变成”第几周的星期几”这种形式。通知太多的话,人会慢慢开始无视它们。如果连重要的账单确认通知都开始一扫而过,自动化的意义可能就打折扣了。
家庭财务相关的自动检查,通知频率本身就决定了好不好用。用自动化省下的时间,顺便把每月的固定支出也检查一下,效果可能会更大。
我的最终做法,以及另一个绊脚石
最后改成了”每周六”
前面写了这么多规避方法,我自己其实选了稍微不同的做法。重新审视了使用方式后,我干脆把提醒本身改成了每周六9点。
0 9 * * 6
仔细想想坚持每月一次的理由,其实并没有那么充分。既然每周都会收到,那时候需不需要自己判断就行了。让机制保持简单,也是一种答案吧。
定时任务放得七零八落
还有一件这次让我卡住的事:想修改的定时任务,怎么也找不到。
定时任务分散在云端的日程和Mac本地应用两个地方。按名字搜也只在其中一边有,光是找它在哪儿就费了不少工夫。
如果按存放位置分别做一份清单,遇到这种情况时可能会轻松一点。
常见失误与陷阱
整理一下用cron设置定时任务时容易犯的错误。
- 同时写了日期和星期,本意是”并且”却变成了”或者”
- 在Cloudflare中用数字写星期,结果在错开1天的星期执行
- 由于UTC和日本时间的时差,星期或日期偏移到了前一天
- 忘了转义crontab命令中的”%”
- 写完后没有确认下次执行日期,过了几天才发现
- 定时任务存放在多个地方,找不到需要修改的那个
我踩到的是第1条和第6条。这两种都属于只要知道一次就很容易预防的失误。
开始前的确认事项
- 日字段和星期字段中至少有一个是“*”
- 已确认所用运行环境中星期是从0开始还是从1开始
- 已确认运行环境的时区是UTC还是日本时间
- 第几个星期几的判断已写在脚本或指令一侧
- 已列出接下来几次的执行日期,确认符合意图
- 已把定时任务的存放位置整理成清单
常见问题
Q. 同时写日期和星期,一定会变成OR吗?
POSIX规范和cronie的man页面写的是只要匹配其中之一就会执行。另一方面,有些运行环境的页面上并没有写明如何处理。所以,设计成不同时写两者的形式是最安全的。
Q. 有没有用一行写出第1个星期六的方法?
像Cloudflare Workers的Cron Triggers这样,有的环境可以在星期字段使用”#“。不过这是标准cron中没有的扩展。最好先在所用环境的官方文档中确认支持情况后再用。
Q. 每月最后一个星期六该怎么写?
Cloudflare官方给出了用”L”表示最后一个星期几的例子。如果用标准cron来写,就是每周六触发后再判断。用”7天后如果是下个月就是最后一个星期六”来判断可能比较好理解。
Q. 日期判断用1〜7日真的没问题吗?
每月的第一个星期六一定落在1日到7日之间。所以在星期六触发后,只要看日期是否在7以下,就能只锁定第1个星期六。不过,日期如果不按日本时间获取可能会出现偏差,请注意。
Q. 怎么确认下次的执行日期?
可以用crontab.guru这样的网站确认表达式的含义。GitHub Actions的官方文档里也有介绍。不过它是第三方工具,最终最好结合运行环境的官方信息一起确认。
Q. GitHub Actions能指定日本时间吗?
官方文档中说默认是UTC。在此基础上,写明可以指定IANA格式的时区,例如Asia/Tokyo这样的写法。文档里也提到了有夏令时地区的注意事项,不过日本时间的话应该影响不大。
参考的官方信息
- crontab(The Open Group Base Specifications)
- crontab(5) - Linux manual page(man7.org)
- Cron Triggers(Cloudflare Workers docs)
- Events that trigger workflows(GitHub Docs)
- Workflow syntax for GitHub Actions(GitHub Docs)
总结
在cron中同时指定日期和星期,判断方式不是AND而是OR。所以”0 9 1-7 * 6”不是第1个星期六,而是月初每天加上每个星期六都会执行。只按星期触发、用代码判断日期,就能准确地限定为每月1次。
写完后,推荐把接下来几次的执行日期列出来确认。也要留意和UTC的时差,以及不同运行环境对星期的计数方式,这样会更安心。
关于定时执行时间怎么定,我在自动检查学校通知的经历里也写过。家庭财务的自动检查,可以参考每晚自动检查信用卡消费的机制。
设置了每月一次提醒的朋友,不妨把表达式重新检查一遍。
本文包含联盟营销广告。如果您通过本站链接申请相关商品或服务,我们可能会从合作企业获得报酬。此外,运营者是乐天集团的员工,也可能通过员工推荐计划获得报酬。文章内容与评价均基于运营者的真实体验与调查完成,与是否存在广告无关,但请您在阅读前了解上述关系。详情请参阅免责声明与联盟营销说明。 免责声明与联盟营销说明
本文包含机器翻译内容。正式条件请以乐天移动官方网站的多语言页面为准。
如果您对乐天移动的通信区域·信号状况有疑问,可以通过官方的 电波改善调查申请表 进行咨询。