如何指示 cron 每两周执行一次作业?

88

我想通过cron来运行一个任务,该任务将在每个月的第二个星期二在特定时间执行。对于每个星期二是很容易处理的:

0 6 * * Tue

但是如何使其在“每个第二个星期二”(或者如果您喜欢-每两周)运行?我不想在脚本中实现任何逻辑,而只想在cron中保留定义。


请参见http://unix.stackexchange.com/questions/197324/is-it-possible-to-schedule-a-cron-job-to-run-fortnightly - Jasen
1
每周运行一次,通过脚本(例如使用单独的文件)检查它是第二周还是第一周,这个想法怎么样? if [ "$(cat week.txt)" == "1" ]; then echo -n "0">week.txt; dostuff; fi - xdevs23
15个回答

56

答案

修改您的星期二cron逻辑,使其自时代以来每隔一周执行一次。

知道每周有604800秒(忽略DST更改和闰秒,谢谢),并使用GNU date:

0 6 * * Tue expr `date +\%s` / 604800 \% 2 >/dev/null || /scripts/fortnightly.sh

旁注

日历计算很令人沮丧。

@xahtep的答案非常棒,但是,正如@Doppelganger在评论中指出的那样,它在某些年份边界上会失败。 任何一个date实用程序中的“周数”格式化标识符都无法解决这个问题。 一月初的某个星期二将不可避免地重复前一年最后一个星期二的周奇偶性:2016-01-05(%V),2018-01-02(%U)和2019-01-01(%W)。


这个答案,或者使用一天中的秒数(而不是一周),例如86400,也在某些边缘情况下存在局限性:UTC时间与您的本地服务器之间的调整。假设我有一个cron作业,每隔两个星期二的凌晨3点和晚上11点运行。您可能会遇到这样的情况,根据UTC时间(自纪元以来的秒数),它们被视为不同的日期。在我的情况下,21600是我的本地偏移量(秒)与时代相比:expr \( `date +%s` - 21600 \) / 86400 % 2并且,用于测试expr \( `date +%s -d "Thursday"` - 21600 \) / 86400 % 2 - dxdc
请看下面我的更完整的解决方案。 - dxdc

53

这样怎么样,即使在前五个字段中没有明确定义,它也可以保留在crontab中:

0 6 * * Tue expr `date +\%W` \% 2 > /dev/null || /scripts/fortnightly.sh

8
我的猜测是,这个解决方案在一年的最后一周存在问题。有时候,一年中最后一个星期五在第51周,而有时候在第52周。这可能会导致你需要连续两周运行脚本,或者每隔三周才运行一次。 - Doppelganger

11

类似这样的东西

0 0 1-7,15-21 * 2

将在每月的第一个和第三个星期二运行。

注意:不要在使用vixie cron(包含在RedHat和SLES发行版中)时使用此功能,因为它会将日期字段和星期字段之间的逻辑关系从"and"改成"or"。


3
我认为这是最佳方案! - Antonio
2
有哪些发行版可以使用,哪些不行?因为Ubuntu和Mac OSX在阅读“man cron”手册时都会说“vixie cron”。你提到“不要与RedHat一起使用”。如果标准是vixie这个词,那么它排除了所有东西。也许这不是要检查的确切内容。 - Sam

11
也许有点愚蠢,但是也可以创建两个cronjob,一个用于每个月的第一个星期二,另一个用于每个月的第三个星期二。
第一个cronjob:
0 0 8 ? 1/1 TUE#1 *

第二个定时任务:

0 0 8 ? 1/1 TUE#3 *

这里的语法我不是很确定,我使用了http://www.cronmaker.com/来生成这些。


4
将工作调度安排在每个月的第一个和第三个星期二将错过一个月的第五个星期二(例如,2015年3月31日),当然添加第五个星期二将使工作频率过高。此外,本答案提到了类似于 Cron 的 Quartz 调度程序,而不是传统的 *NIX Cron。 - pilcrow
'#' 是什么意思? - Asif
@Asif "它允许您指定给定月份的“第二个星期五”等结构。" https://zh.wikipedia.org/wiki/Cron - bigtex777
1
@bigtex777:那是哪个Linux发行版上的哪个cron实现的呢?这肯定不是POSIX。 - Martin Schröder

9

pilcrow的回答非常好。然而,它导致fortnightly.sh脚本每隔偶数周(自纪元以来)运行一次。如果你需要该脚本在奇数周运行,可以稍微调整他的答案:

0 6 * * Tue expr \( `date +\%s` / 604800 + 1 \) \% 2 > /dev/null || /scripts/fortnightly.sh

将1改为0将会使其回到偶数周。

1
实际上,你不需要0或1。只需将||更改为&&即可。如果表达式评估为1,则执行&&后面的命令,否则执行||后面的命令。 - Scottie H

3
如果您想根据给定的开始日期进行操作:
0 6 * * 1 expr \( `date +\%s` / 86400 - `date --date='2018-03-19' +\%s` / 86400 \) \% 14 == 0 > /dev/null && /scripts/fortnightly.sh

应该在每个星期一开始于 2018-03-19 时触发

表达式的含义是:如果...,则在每个星期一早上6点运行

1 - 获取今天的日期,转换为距离 epoch 的天数,单位为秒,然后除以一天的秒数

2 - 对于起始日期也进行同样的步骤,将其转换为距离 epoch 的天数

3 - 获取两者之间的差值

4 - 将差值除以14并检查余数

5 - 如果余数为零,则您处于两周周期中


1
这里有很多好的答案。根据评论,我看到有很多混淆和沮丧。我的回答意图不仅是回答OP的问题如何指示cron每隔两周执行一次作业?,还要为将来可能阅读此内容的人澄清一些混淆。 简而言之:
crontab条目应该长这样:
\<minute\> \<hour\> * * \<Day of Week\> expr \\( $( date+\\%s ) \\/ 604800 \\% 2 \\) > /dev/null && \<command to run on odd weeks\> || \<command to run on even weeks\>
Crontab条目:
所有使用crontab -e创建的Crontab条目都遵循以下格式,根据man页面:
1: 执行命令的小时分钟[command]
范围:0-59
2: 执行命令的小时数[command]
范围:0-23
3: 执行命令的日期[command]
范围:1-31
4: 执行命令的月份[command]
范围:1-12
5: 执行命令的星期[command]
范围:请查看您的man页面,可能是0-6、1-7或0-7
6: 要执行的命令
注意,还有@值,这里不讨论。

某些*nix版本允许使用表达式:
*/2 = 范围内的每个偶数
1 + */2 = 范围内的每个奇数
1,15 = 第一和第十五
2-8 = 从第二到第八(包括第二和第八)

某些版本还允许使用可读性较强的单词,例如“February”或“Wednesday”。

对于月份,*/2将在二、四、六、八、十二月执行。

对于星期几,*/2将在每个偶数日运行 - 请查看您的man页面以查看星期几的编号。Tue/2不是有效的表达式。

人类可读(日历)困扰
您需要了解一些常量:
一年有365.2464天(为方便起见,我们将四舍五入为365)。
一年有12个月。
一周有7天。
一天有86,400秒。

因此,
1个月是4-1/3周
[即3个月为13周]
1年是52-1/7周

日历数学的祸根:
半月 = 每半个月 = 每月2次 = 每年24次。
双周 = 每隔一周(每两周)= 每年26次。
注意:这些术语经常被误用。
有些年份有51周,有些年份有53周,大多数年份有52周。如果我的cron运行在每个奇数周(date +%W mod 2),而该年有51或53周,它也会在接下来的一周运行,即新年的第1周。相反,如果我的cron在每个偶数周运行,它将跳过2周。这不是我想要的。

CRON可以支持半月,但不支持双周! 半月:
每个月的第一天始终在1日至7日之间。第二个星期的后半部分总是发生在15日至21日之间。
半月将有2个值,一个在月初,另一个在月中旬。例如:
2,16

Unix时间
在非常高的层面上,*nix时间有两个值:
date +%s = 自纪元(1970年1月1日00:00:00)以来的秒数
date +%N = 分数秒数(以纳秒为单位)
因此,在*nix中的时间是date +%s.%N

*Nix使用纪元时间。 /etc/shadow文件包含最后更改密码的日期。它是%s的整数部分除以86,400。

事实
GPS卫星将时间测量为“自历元时间以来的周数”和“一周内的(分数)秒数。
注意:Epoch周与年无关。无论该年是否有51、52或53周,Epoch周都不会翻转。

每两周时间算法
在*nix中,date +%W是年份的周数,而不是时代周数。*nix没有时代周值,但可以计算。
时代周 = 整数(时代秒数/每天秒数/每周天数)
每两周 = Epoch_Week Modulo 2
每两周的值始终为0或1,因此,当每两周=0时运行命令会在每个偶数周运行,1表示每个奇数周。

计算每两周
第一种方法(bc):
date +"%s / 604800 % 2" | bc
这将生成"[Epoch Seconds] / 604800 % 2"
然后将该答案发送到bc,一个基本计算器,它执行计算并将答案回显到STDOUT和任何错误到STDERR。
返回代码为0。

第二种方法(expr):
在这种情况下,将表达式发送到expr并让它进行数学运算expr $( date +%s ) / 604800 % 2
expr命令执行数学运算,将答案回显到STDOUT,错误回显到STDERR。
如果答案为0,则返回代码为1;否则,返回代码为0。
以这种方式,我不关心答案是什么,只关心返回代码。考虑以下内容:
$ expr 1 % 2 && echo Odd || echo Even
1
Odd 
$ expr 2 % 2 && echo Odd || echo Even 
0 
Even

由于结果并不重要,我可以将STDOUT重定向到/dev/null。在这种情况下,我会保留STDERR以防发生意外情况,我将看到错误消息。

Crontab条目
在crontab中,百分号和括号具有特殊含义,因此需要用反斜杠(\)进行转义。
首先,我需要决定是要在偶数周还是奇数周运行。或者,也许在奇数周运行Command_O,在偶数周运行command_E。然后,我需要转义所有特殊字符。
由于expr只评估WEEKS,因此需要指定一周中的特定日期(和时间)进行评估,例如每周二下午3:15。因此,我的crontab条目的前5个文件(时间条目)将为:
15 3 * * TUE
我的cron命令的第一部分将是expr命令,并将STDOUT发送到null:

expr \\( $( date+\\%s ) \\/ 604800 \\% 2 \\) > /dev/null

第二部分将是评估器,&& 或者 ||
第三部分将是我想运行的命令(或脚本)。它看起来像这样:
expr \\( $( date+\\%s ) \\/ 604800 \\% 2 \\) > /dev/null && \<command_O\> || \<command_E\>

希望这能澄清一些混淆的问题。


1
这篇文章和许多其他文章一样存在相同的错误。请看我的帖子。如果您的服务器运行在UTC时间,则此解决方案将起作用。但是,对于任何其他时区,由于夏令时的存在,存在遇到边缘情况的风险...而且事实上,a)并非所有地方都遵守夏令时,b)不同国家的夏令时规则也不同(例如,月份/星期未同步)。考虑纪元1616025600和1616025599。如果您的cron时间处于边界位置,并且您正在经历夏令时事件,则可能会产生错误的结果。 - dxdc
不是真的!一天有86,400秒。两周包含1,209,600秒。因此,每隔一周只是时间的流逝,与我的时区无关。如果我碰巧需要在地理多样化的网络上同步我的cron作业,那么你说得对。但这不是OP的问题。问题是(实际上):如何每1,209,600秒执行一次cron作业。 - Scottie H
2
我认为你忽略了一个微妙但至关重要的点。没有人在争论一天有多少秒。然而,Cron作业通常以“每周二@晚上11点”等方式引用。由于夏令时和UTC与每个单独的时区之间的差异,Unix纪元之间的“有效”差异可能是意外的。请考虑:1615708800 =美国/太平洋时间3/14/21 12:00AM。1615791600 =美国/太平洋时间3/15/21 12:00AM。 Unix纪元之间的差异仅为82,800秒...但根据cron作业,应该已经过去了24小时。 - dxdc

1

我发现了一些以上方法的额外限制,可能会在某些边缘情况下失败。例如:

@xahtep和@Doppelganger上面讨论了使用%W在特定年份边界上的问题。

@pilcrow的答案在某种程度上解决了这个问题,但它也会在某些边界上失败。在此和其他相关主题中的答案使用一天中的秒数(而不是星期),由于同样的原因,这也会在某些边界上失败。

这是因为这些方法依赖于UTC时间(日期+%s)。考虑这样一个情况,我们每隔两周的第二个星期二在1am和10pm运行一个作业。

假设GMT-2:

  • 当地时间上午1点=昨天晚上11点UTC
  • 当地时间晚上10点=今天晚上8点UTC

如果我们只检查每天的单个时间,这将不是一个问题,但如果我们检查多次--或者如果我们接近UTC时间并且夏令时发生,脚本将不认为这些是同一天。

为了解决这个问题,我们需要根据我们的本地时区而不是UTC计算偏移量。我在BASH中找不到简单的方法来实现这一点,因此我开发了一个解决方案,使用Perl中的一行快速代码来计算以秒为单位的与UTC的偏移量。
该脚本利用date +%z,输出本地时区。
Bash脚本:
TZ_OFFSET=$( date +%z | perl -ne '$_ =~ /([+-])(\d{2})(\d{2})/; print eval($1."60**2") * ($2 + $3/60);' )
DAY_PARITY=$(( ( `date +%s` + ${TZ_OFFSET} ) / 86400 % 2 ))

然后,确定日期是偶数还是奇数:
if [ ${DAY_PARITY} -eq 1 ]; then
...
else
...
fi

日期不使用协调世界时。它使用设置在您系统上的时区。 - Scottie H
@ScottieH请仔细阅读解决方案。它会将日期输出调整为UTC时间,无论系统时区如何。 - dxdc
你是否真的见过这种行为?在我的系统上,Cron并不像你在边缘情况下描述的那样工作。 - Scottie H
1
date +%s 返回 Unix 时间戳。这是一个标准化的格式,在 UTC 时间下,与任何本地时区设置无关。正因为如此,它将会有一些边缘情况,具体取决于您的服务器时区设置。例如,英国的夏令时不会在同一时间发生,而美国的夏令时也是如此。此外,美国的一些地方不遵守夏令时,而 UTC 则遵守夏令时。将 Unix 时间戳转换为本地时间是目标,但是根据您运行脚本的时间和您相对于 UTC 的 TZ 偏移量,可能会出现一些边缘情况。 - dxdc
你对OPS问题的看法与我不同。让工作每隔一个星期二运行要比你试图实现的要简单得多。0.02美元。 - Scottie H
1
@ScottieH,你的答案(和其他许多答案一样)看起来更简单,可能在99.9%的情况下都能正常工作。但是,由于时区差异存在边缘情况,可能会导致意外行为或错过cron作业。如果该作业每两周在同一天运行,则我提出的解决方案将有效。 - dxdc

0

试试这个

0 0 1-7,15-21 * 2

这个程序每月运行2次,都在周二,并且至少相隔一周。


0
每个月的第2天早上4:30,如果是星期一。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接