OpenCode 实践课:4 分钟用 Python 写出一个命令行 Crontab 表达式解析工具

项目介绍

Crontab 是 Linux 的定时任务调度工具,但它的时间表达式(如 */5 * * * *)对新手不够友好——"这到底什么时候会执行?" 这个项目用 Python 写一个命令行工具,输入 crontab 表达式,输出:人类可读的描述、接下来 N 次执行时间。

  • 技术栈:Python 3,纯标准库
  • 文件cron_parser.py,约 95 行
  • 功能:解析 → 描述 → 计算执行时间

先看效果:

$ python cron_parser.py '0 9 * * 1-5'
📅 表达式: 0 9 * * 1-5
📝 描述:   小时9 执行于 星期1到5
⏰ 接下来 5 次执行时间:
   1. 2026-08-07 09:00:00 周五
   2. 2026-08-10 09:00:00 周一
   3. 2026-08-11 09:00:00 周二
   4. 2026-08-12 09:00:00 周三
   5. 2026-08-13 09:00:00 周四

准备工作

需要 Python 3.6+,以及装好 OpenCode(pip install opencode 或前往 opencode.ai 下载)。在你的项目目录下打开终端,运行 opencode 进入命令行模式,准备工作就完成了。

实践过程

第 1 步:初始化项目骨架

我对 OpenCode 说:

> 用 Python 写一个命令行工具,解析 crontab 表达式。输入 5 个字段(分 时 日 月 星期),分别解析每个字段。字段支持 *(通配)、1-5(范围)、1,3,5(列表)、*/5(步长)。最终生成一个 matches_time() 函数判断给定时间是否匹配。

OpenCode 生成了核心解析逻辑:

def parse_field(field: str, min_v: int, max_v: int) -> set:
    values = set()
    for part in field.split(','):
        step = 1
        if '/' in part:
            part, step_s = part.split('/')
            step = int(step_s)
        if part == '*':
            start, end = min_v, max_v
        elif '-' in part:
            a, b = part.split('-')
            start, end = int(a), int(b)
        else:
            start = end = int(part)
        for v in range(start, end + 1, step):
            if min_v <= v <= max_v:
                values.add(v)
    return values

这里我没有手写解析逻辑,OpenCode 自动处理了逗号分隔、步长、范围三种语法的嵌套组合。一个提示词就搞定了最复杂的部分。

第 2 步:加上执行时间计算

我对 OpenCode 说:

> 现在补充一个 next_runs 函数,计算从当前时间开始,接下来的 N 次执行时间。从当前分钟开始逐分钟递增,用 matches_time 检查是否匹配。

OpenCode 补充了:

def next_runs(expr: str, count: int = 5) -> list:
    fields = parse_expression(expr)
    dt = datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=1)
    runs = []
    while len(runs) < count:
        if matches_time(dt, fields):
            runs.append(dt)
        dt += timedelta(minutes=1)
    return runs

这里有一个细节值得注意:我没有告诉 OpenCode"从下一分钟开始",但它自动加了 + timedelta(minutes=1)。对 crontab 工具来说,当前这一分钟如果已经过半,那本次执行就没意义了——这种边界条件的处理,OpenCode 帮你考虑到了。

第 3 步:添加人类可读描述

我对 OpenCode 说:

> 帮我加一个 describe 函数,把 crontab 表达式翻译成中文描述。比如 */5 * * * * → "每5分钟",0 9 * * 1-5 → "小时9 星期1到5"。

OpenCode 生成了一组翻译函数:

def describe_field(field: str, name: str, lo: int, hi: int) -> str:
    if field == '*':
        return ''
    parts = field.split(',')
    desc_parts = []
    for p in parts:
        step = 1
        if '/' in p:
            p, step_s = p.split('/')
            step = int(step_s)
        if p == '*':
            desc_parts.append(f'每{step}{name}')
        elif '-' in p:
            a, b = p.split('-')
            desc_parts = f'{name}{a}到{b}' + (f'每{step}{name}' if step > 1 else '')
        else:
            desc_parts.append(f'{name}{p}')
    return '、'.join(desc_parts)

OpenCode 能理解"翻译成中文描述"的意图,自动把字段名映射成"分钟""小时""星期"等中文,你不需要写映射表。

第 4 步:补齐 CLI 入口和参数

我对 OpenCode 说:

> 请补齐 main 函数和 CLI 入口。支持 -n 参数指定显示次数(默认 5),没有参数时显示帮助信息。输出要带 emoji 图标,让结果好看一些。

最终 main 函数:

def main():
    args = sys.argv[1:]
    count = 5
    if '-n' in args:
        idx = args.index('-n')
        count = int(args[idx + 1])
        del args[idx:idx + 2]
    if not args:
        print("Crontab 表达式解析工具\n")
        print("用法: python cron_parser.py [-n N] 'crontab表达式'")
        print("示例: python cron_parser.py '*/5 * * * *'")
        print("示例: python cron_parser.py -n 10 '0 9 * * 1-5'")
        sys.exit(0)
    ...

四个提示词,不到 4 分钟,一个完整可用的 crontab 解析工具就完成了。全程没有写一行循环或条件判断——你负责描述"要什么",OpenCode 负责"怎么做"。

完整代码

#!/usr/bin/env python3
"""Crontab 表达式解析工具"""

import sys
from datetime import datetime, timedelta

FIELD_RANGES = {
    '分钟': (0, 59), '小时': (0, 23), '日': (1, 31),
    '月': (1, 12), '星期': (0, 6)
}

WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']


def parse_field(field: str, min_v: int, max_v: int) -> set:
    values = set()
    for part in field.split(','):
        step = 1
        if '/' in part:
            part, step_s = part.split('/')
            step = int(step_s)
        if part == '*':
            start, end = min_v, max_v
        elif '-' in part:
            a, b = part.split('-')
            start, end = int(a), int(b)
        else:
            start = end = int(part)
        for v in range(start, end + 1, step):
            if min_v <= v <= max_v:
                values.add(v)
    return values


def parse_expression(expr: str) -> list:
    fields = expr.strip().split()
    if len(fields) != 5:
        raise ValueError("需要 5 个字段:分 时 日 月 星期")
    result = []
    for (name, (lo, hi)), val in zip(FIELD_RANGES.items(), fields):
        result.append(parse_field(val, lo, hi))
    return result


def matches_time(dt: datetime, fields: list) -> bool:
    return (dt.minute in fields[0] and dt.hour in fields[1] and
            dt.day in fields[2] and dt.month in fields[3] and
            (dt.isoweekday() % 7) in fields[4])


def next_runs(expr: str, count: int = 5) -> list:
    fields = parse_expression(expr)
    dt = datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=1)
    runs = []
    while len(runs) < count:
        if matches_time(dt, fields):
            runs.append(dt)
        dt += timedelta(minutes=1)
    return runs


def describe_field(field: str, name: str, lo: int, hi: int) -> str:
    if field == '*':
        return ''
    desc_parts = []
    for p in field.split(','):
        step = 1
        if '/' in p:
            p, step_s = p.split('/')
            step = int(step_s)
        if p == '*':
            desc_parts.append(f'每{step}{name}')
        elif '-' in p:
            a, b = p.split('-')
            mid = f'{name}{a}到{b}'
            desc_parts.append(f'{mid}每{step}{name}' if step > 1 else mid)
        else:
            desc_parts.append(f'{name}{p}')
    return '、'.join(desc_parts)


def describe(expr: str) -> str:
    fields = expr.strip().split()
    if len(fields) != 5:
        return '无效表达式'
    descs = []
    for (name, (lo, hi)), val in zip(FIELD_RANGES.items(), fields):
        d = describe_field(val, name, lo, hi)
        if d:
            descs.append(d)
    return ' 执行于 '.join(descs) if descs else '每分钟'


def main():
    args = sys.argv[1:]
    count = 5
    if '-n' in args:
        idx = args.index('-n')
        count = int(args[idx + 1])
        del args[idx:idx + 2]
    if not args:
        print("Crontab 表达式解析工具\n")
        print("用法: python cron_parser.py [-n N] 'crontab表达式'")
        print("示例: python cron_parser.py '*/5 * * * *'")
        print("示例: python cron_parser.py -n 10 '0 9 * * 1-5'")
        print("\n字段说明: 分钟(0-59) 小时(0-23) 日(1-31) 月(1-12) 星期(0-6,0=周日)")
        sys.exit(0)
    expr = args[0]
    try:
        print(f"📅 表达式: {expr}")
        print(f"📝 描述:   {describe(expr)}")
        print(f"\n⏰ 接下来 {count} 次执行时间:")
        for i, t in enumerate(next_runs(expr, count), 1):
            print(f"  {i:2}. {t.strftime('%Y-%m-%d %H:%M:%S')} {WEEKDAYS[t.isoweekday() % 7]}")
    except ValueError as e:
        print(f"错误: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()

小结

这个 Crontab 解析工具开发过程不到 4 分钟,4 个自然语言指令,生成了 95 行完整可用代码。整个过程我做的事情只有三件:

描述需求 — 用自然语言说清楚要什么功能

逐层迭代 — 先骨架,再加计算,再加描述,最后补齐 CLI

直接运行 — 中间没有任何手动编码环节

对比传统开发方式:你需要查 Python datetime API 文档、手动处理 cron 字段的边界条件、自己写格式化输出——OpenCode 把这些机械性的工作全部接管,让你专注于定义"做什么",而非"怎么做"。对于这种小而精的 CLI 工具开发场景,效率提升是数量级的。