能直接抄走的日期正则
最稳妥的日期形态是 ISO 8601 日历日期,它表面形式的正则短得可怜:
^\d{4}-\d{2}-\d{2}$
它要求四位数字、一个连字符、两位数字、一个连字符、两位数字。这就是它全部的结构承诺。它只字不提 2 月 30 日是否存在,因为正则不做算术。
这条正则到底匹配了什么
\d{4}—— 年,四位数字,无符号,无上限。0000和9999都能过。-—— 字面连字符分隔符,ISO 的选择。(斜杠不是 ISO,而且引歧义。)\d{2}—— 月,两位数字。00和13都能过,因为模式只数数字。\d{2}—— 日,两位数字,对现实同样 blindness。
这个模式是格式的门卫,不是真相的门卫。
美式与欧式,以及它们怎么撞车
在 ISO 之外你会遇到两种本地习惯,而它们在"哪位是月"上意见相左:
- 美式 MM/DD/YYYY:
^(0?[1-9]|1[0-2])/(0?[1-9]|[12]\d|3[01])/\d{4}$ - 欧式 DD-MM-YYYY:
^(0?[1-9]|[12]\d|3[01])-(0?[1-9]|1[0-2])-\d{4}$
撞车就是那桩著名的 03/04/2026 事故。在美国它是 3 月 4 日,在世界大部分地区它是 4 月 3 日。如果你的表单默默假定其一、而用户假定其二,记录会整月录错,且要很久以后才被发现。光这个歧义,就够成为处处默认 ISO 的理由。
为什么正则抓不出真实日期
这是本页核心论点,毫无商量余地。正则作用于字符,不作用于公历。想想:
2026-02-30—— 二月从没有 30 天,可模式照收。02和30各自都是合法的数字对。2026-13-01—— 第 13 月不存在,但13不过是两位数字。- 闰年:
2024-02-29是真的,2025-02-29不是。判定的前提是"模 4、模 100、模 400"的算术,没有正则能优雅表达,除非写成恐怖的巨长分支。
你可以把月收紧成 0[1-9]|1[0-2]、把日收紧成 0[1-9]|[12]\d|3[01],但这仍会欣然收下 4 月 31 日和每一个非闰年里的 2 月 29 日。形状被框住了,真相没有。
正则能推到多远
如果你铁了心不靠解析器也要挡掉最离谱的错,可以把每月天数上限按月份写死,再给二月开特例:
^(?:\d{4}-(?:0[13-9]|1[0-2])-(?:0[1-9]|[12]\d|30)|...)$
老实说代价是:模式膨胀成几十条分支,还得再额外用更多分支把闰年例外焊上去,最后既难维护又容易写错。在它身上耗的每个小时,都是本可以拿来调一次 strptime 的时间。
常见错误写法与修正
| 错误 | 症状 | 修正 |
|---|---|---|
| \d{2}-\d{2}-\d{4} | 分不清月日,美式欧式打架 | 显式钉死顺序,优先用 ISO YYYY-MM-DD |
| ^....-..-..$ | 接受字母和符号 | 数字段用 \d 而不是点号 |
| 日用 [1-31] | 字符类写错,实际只匹配 1、2、3 | 用 (0?[1-9]|[12]\d|3[01]) |
| 没锚点 | 能匹配藏在别的文本里的日期 | 保留 ^ 和 $ |
该用什么替代
把日期交给真正的解析器,它懂日历。Python 里:
from datetime import datetime
def parse_date(s):
d = datetime.strptime(s, "%Y-%m-%d")
# 防 2026-02-30 被规范化成 3 月 2 日
if d.strftime("%Y-%m-%d") != s:
raise ValueError("日期不存在")
return d
JavaScript 里要先解析,再回读比较,因为 new Date('2026-02-30') 会悄悄滚到三月:
function parseDate(s) {
const d = new Date(s + "T00:00:00");
if (d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,"0") + "-" + String(d.getDate()).padStart(2,"0") !== s)
throw new Error("日期不存在");
return d;
}
解析器能抓住正则抓不住的东西:2 月 30 日是虚构的。
生成与解析是两回事
如果你是在产出日期、而不是校验用户输入,别用拼接字符串再配一条正则。用语言自带的日期格式化器来生成,分隔符和补零永远正确,然后以无歧义的格式(UTC、ISO)存储,只在显示层做本地化。正则唯一诚实的角色,仍然是解析器动手之前、一道廉价的前置过滤,先把明显畸形的输入挡掉。
真正决定"日期是否合法"的,从来不是正则里那几个 \d,而是背后那套公历规则。把这套规则交给标准库,你的代码会更短、更对,也更经得起闰年二月的考验。当你纠结要不要为正则再多加一条分支时,先问自己:这活儿,解析器一行不就干完了么?与其和 2 月 29 日、4 月 31 日反复搏斗,不如把这套规则一次性交给标准库,让它替你记着哪个月有几天。
时区与完整 ISO 8601
日期常常还会长出时间。完整 ISO 8601 形如 2026-02-28T14:30:00Z 或 2026-02-28T14:30:00+05:00。给它的正则更长、在闰秒和混合时差上更不可信。还是那句话,解析它:每种现代语言都有 ISO 日期时间解析器(datetime.fromisoformat、小心使用的 new Date,或 Luxon 这类库)。用解析器校验、用解析器格式化,把正则放回它唯一擅长的一件事——在麻烦解析器之前,先粗略查一下外形。