转换之前,先把时区和单位定下来
一个日历值要变成 Unix 时间戳,得先回答两个问题:它属于哪个时区,以及使用方期待的是什么单位。拿一个固定参照来说,2026-06-20 09:25:00 UTC 对应 1781947500 Unix 秒,或者 1781947500000 Unix 毫秒。算出别的结果,就说明解析器用了另一个时区、对输入做了另一种解释,或者用了另一个单位。
| 输入 | 含义 | Unix 秒 |
|---|---|---|
| 2026-06-20 09:25 UTC | 一个 UTC 瞬间 | 1781947500 |
| 2026-06-20 09:25 America/New_York | 同样的钟面,美国东部夏令时 | 1781961900 |
| 2026-06-20 09:25 Asia/Tokyo | 同样的钟面,日本时间 | 1781915100 |
| 2026-06-20 | 所选时区里这一天开始的零点 | 取决于时区 |
第一步:选时区
钟面日期不配上时区就不是一个瞬间。接口、日志、消息队列、审计记录和跨地域系统一律用 UTC。只有当这个日期确实是当地的日历事件时——比如营业时间、账单日、续费截止、定时提醒——才用业务时区或用户时区。
第二步:转成 Unix 秒
Unix 秒是 shell 命令、Python、PHP、Go、SQL 纪元提取、JWT claims 和大量后端接口的默认单位。而且它在日志里更好读,因为当代的值是 10 位。
第三步:只在必要时才用毫秒
JavaScript 的 Date、Java 里 currentTimeMillis 风格的系统、浏览器埋点,以及那些文档示例写着 13 位数字的接口,才用毫秒。不要在应用中途换单位——在边界上转换一次,并且把单位写进字段名。
日期转纪元到底在做什么
Unix 时间戳是从 1970-01-01 00:00:00 UTC 起算的计数。日期转纪元,就是从一个人读的钟面值走到这个计数。难的从来不是算术,而是确定这个人类日期究竟指哪一个确切瞬间。
下面这几种输入长得像,含义却不等价:
- 2026-06-20 表示所选时区里那个日历日的开始。
- 2026-06-20 09:25 是一个本地钟面时间,但没有时区它就是不完整的。
- 2026-06-20T09:25:00Z 本身就是 UTC,可以放心转换。
- 2026-06-20T09:25:00-04:00 带明确偏移,唯一对应一个瞬间。
- 1781947500 已经是以秒为单位的 Unix 时间戳了,别再转一遍。
「日期的 Unix 时间戳形式」
当有人问某个日期的 Unix 时间戳形式时,答案是某个具体边界对应的纪元整数。光说“2026-06-20”是不够的,你得说“2026-06-20 的 UTC 零点”或者“2026-06-20 的 America/New_York 零点”。
「datetime 的 Unix 纪元值」
带 Z 后缀、偏移量或 IANA 时区的 datetime,能干净地映射到唯一的纪元值。而不带时区的 datetime,结果取决于解析它的那台机器、数据库会话、电子表格或运行时。
时区是输入的一部分
日期转纪元最常见的 bug,就是把一个本地钟面值当成 UTC 来处理。于是上线之后,发布截止时间、优惠券过期、报表窗口整体错开几个小时——算术本身一点没错。如果这个值来自用户输入,就把用户的 IANA 时区名保留下来;如果来自 Web 服务或接口约定,就用 UTC,并且让字段名或 schema 把这一点写清楚。
- 错:datetime(2026, 6, 20, 9, 25).timestamp() —— Python 会用宿主机时区
- 对:datetime(2026, 6, 20, 9, 25, tzinfo=timezone.utc).timestamp() —— 明确的 UTC
- 错:new Date('2026-06-20 09:25') —— JavaScript 解析器会退回到本地时间行为
- 对:Date.UTC(2026, 5, 20, 9, 25) —— UTC 分量,月份索引 5 表示六月
- 对:Date.parse('2026-06-20T09:25:00Z') —— Z 后缀锁定 UTC
- 本地业务时间的正确做法:存 America/New_York,而不是只存 -04:00
UTC 输入
当生成的 Unix 时间戳要跨服务、日志、队列、数据库或 API 使用方流转时,用 UTC。基准层永远是 UTC。
本地业务输入
报表窗口、营业时间、订阅续费、工资截止日和当地日历日,用业务时区。记得把 IANA 名字留下来,这样将来的夏令时规则才能正确套用。
秒还是毫秒?
输出单位应该由接收方决定,而不是凭个人偏好。秒和毫秒表示的是同一个瞬间,但把两者混着用,是最快交付一个日期 bug 的方式。症状通常很明显:日期落在 1970 年附近,或者落在几千年后的未来。
- 10 位 → Unix 秒,比如 1781947500
- 13 位 → Unix 毫秒,比如 1781947500000
- 16 位 → 微秒,数据库和事件流里常见
- 19 位 → 纳秒,链路追踪和一些高精度系统里常见
- 字段名里带上单位:startsAtSeconds、scheduledAtMs、capturedAtMicros
在 JavaScript 里把日期转成 Unix 时间戳
当你手上的日期分量本来就该按 UTC 理解时,用 Date.UTC。它返回毫秒,所以要 Unix 秒就除以 1000。解析字符串时,只解析带 Z 后缀或明确偏移的 ISO 8601。避开 06/20/2026 这类看着像本地习惯的写法——不同运行时对它的解析可能不一样。
- Date.UTC(2026, 5, 20, 9, 25) / 1000 // 1781947500 秒;月份索引 5 = 六月
- Date.UTC(2026, 5, 20, 9, 25) // 1781947500000 毫秒
- Math.floor(Date.parse('2026-06-20T09:25:00Z') / 1000) // ISO 8601 UTC
- Math.floor(Date.parse('2026-06-20T09:25:00-04:00') / 1000) // 明确偏移
- 避免:new Date('2026-06-20 09:25').getTime() / 1000 // 按运行时本地时区解释
- 有 Temporal 时:Temporal.ZonedDateTime.from('2026-06-20T09:25-04:00[America/New_York]').epochSeconds
只有日期的 UTC 输入,就显式构造零点:Date.UTC(2026, 5, 20) / 1000。要处理用户时区,请用 Temporal 或一个时区库——传统的 Date 没有干净的内置办法,把“America/New_York 的 9:25”变成一个瞬间。
在 Python 里把日期转成 Unix 时间戳
Python 的 datetime.timestamp() 只有在 datetime 带时区时才是对的。naive datetime 会被当作本地时间,于是同一段脚本在开发机、Docker 容器和生产主机上会算出不同的纪元值。
- from datetime import datetime, timezone
- datetime(2026, 6, 20, 9, 25, tzinfo=timezone.utc).timestamp() # 1781947500.0
- int(datetime(2026, 6, 20, 9, 25, tzinfo=timezone.utc).timestamp()) # 1781947500
- from zoneinfo import ZoneInfo
- int(datetime(2026, 6, 20, 9, 25, tzinfo=ZoneInfo('America/New_York')).timestamp()) # 1781961900
- 避免:datetime(2026, 6, 20, 9, 25).timestamp() # 依赖宿主机本地时区
- 从 ISO 8601 转:int(datetime.fromisoformat('2026-06-20T09:25:00+00:00').timestamp())
真实的用户时区或业务时区,请用 ZoneInfo。固定偏移用在一次性的时间戳上没问题,但它不携带任何夏令时规则。
在 PHP 里把日期转成 Unix 时间戳
PHP 的时间 API 默认以秒为单位。strtotime() 很方便,但光秃秃的字符串会继承 php.ini 或 date_default_timezone_set() 设定的服务器时区。用 DateTimeImmutable 配 DateTimeZone,时区就直接写在代码里,评审时一眼可见。
- strtotime('2026-06-20 09:25:00 UTC') // 1781947500
- (new DateTimeImmutable('2026-06-20 09:25:00', new DateTimeZone('UTC')))->getTimestamp()
- (new DateTimeImmutable('2026-06-20T09:25:00Z'))->getTimestamp()
- (new DateTimeImmutable('2026-06-20 09:25:00', new DateTimeZone('America/New_York')))->getTimestamp()
- 避免:strtotime('2026-06-20 09:25:00') // 依赖服务器时区
- 亚秒输入:DateTimeImmutable::createFromFormat('U.u', '1781947500.123456')
在 Go 里把日期转成 Unix 时间戳
Go 把时区和单位都摆在明面上:time.Date 的最后一个参数就是 location,Unix() 返回秒,UnixMilli()、UnixMicro()、UnixNano() 返回更细的单位。这种显式风格在代码评审时很省事,因为时区和输出单位就写在同一行里。
- time.Date(2026, time.June, 20, 9, 25, 0, 0, time.UTC).Unix() // 1781947500
- time.Date(2026, time.June, 20, 9, 25, 0, 0, time.UTC).UnixMilli() // 1781947500000
- loc, _ := time.LoadLocation(“America/New_York”)
- time.Date(2026, time.June, 20, 9, 25, 0, 0, loc).Unix() // 1781961900
- t, _ := time.Parse(time.RFC3339, “2026-06-20T09:25:00Z”); t.Unix()
- 亚秒:t.UnixMicro()、t.UnixNano()
生产代码里别忽略 time.LoadLocation 和 time.Parse 返回的 error。上面的例子把它丢掉,纯粹是为了让转换那一行读起来清爽。
在 Linux 或 macOS 的 shell 里把日期转成 Unix 时间戳
Linux 和 macOS 都用 +%s 打印 Unix 秒,但它们解析输入用的标志并不一样。大多数 Linux 发行版自带 GNU 的 date;macOS 和 FreeBSD 自带 BSD 的 date;精简的 Alpine 和 CI 镜像可能用功能更少的 BusyBox 实现。
| 环境 | 常见实现 | 解析日期的语法 |
|---|---|---|
| Ubuntu、Debian、Fedora、RHEL | GNU 的 date |
date -d INPUT +%s |
| macOS 和 FreeBSD | BSD 的 date |
date -j -f FORMAT INPUT +%s |
| Alpine 和精简容器 | 往往是 BusyBox 的 date |
选项支持各异,请在具体镜像上实测 |
| 装了 GNU coreutils 的 macOS | GNU 的 gdate |
gdate -d INPUT +%s |
在 GNU/Linux 上把 UTC 写明确,这样命令在笔记本、服务器和 CI runner 上返回的值才一致:
date -u -d '2026-06-20 09:25:00 UTC' +%s
# 1781947500
date -u -d '2026-06-20T09:25:00Z' +%s
# 1781947500
如果是当地的业务时间,就设它的 IANA 时区,而不是依赖宿主机默认值:
TZ=America/New_York date -d '2026-06-20 09:25:00' +%s
# 1781961900
避开 06/20/26 这类有歧义的输入。明确的顺序加时区,既扛得住区域设置变化,也更好评审:
# 避免:解释结果取决于 locale 和具体实现
date -d '06/20/26' +%s
# 更好:顺序和时区都明确
date -u -d '2026-06-20 09:25:00 UTC' +%s
BSD 的 date 用 -j 表示只解析、不设置系统时钟,用 -f 声明输入格式:
date -j -u -f '%Y-%m-%d %H:%M:%S' '2026-06-20 09:25:00' +%s
# 1781947500
date -j -u -f '%Y-%m-%dT%H:%M:%SZ' '2026-06-20T09:25:00Z' +%s
# 1781947500
如果 macOS 上的脚本需要 GNU 语法,就装 GNU coreutils 并调用 gdate,别指望系统自带的 date 认识 -d。
在 shell 里取当前 Unix 时间戳
取当前 Unix 秒,在 GNU 和 BSD 实现之间都是通用的:
date +%s
GNU 的 date 可以补上纳秒的前三位来得到当前毫秒:
date +%s%3N
但这在 macOS 上不通用:因为 %N 是 GNU 扩展,BSD 的 date 可能原样打印出 3N 后缀。跨平台脚本要取当前纪元毫秒,请用 Python、Node.js 或 gdate。
给生产日志的话,在纪元值旁边留一个可读的 UTC 值,排查故障时会轻松很多:
now_s=$(date +%s)
now_iso=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
printf 'time=%s epoch=%s event=%s\n' "$now_iso" "$now_s" 'job_started'
构造日期区间时,别把零点算两遍
当这些时间戳要用作报表或 SQL 的边界时,请用左闭右开区间:
start=$(date -u -d '2026-07-01 00:00:00 UTC' +%s)
end=$(date -u -d '2026-08-01 00:00:00 UTC' +%s)
printf 'created_at >= %s AND created_at < %s\n' "$start" "$end"
不包含的上界,能防止一个恰好落在零点的事件同时出现在相邻两个周期里。
GNU Coreutils date 手册 · Linux date(1) 手册 · FreeBSD date(1) 手册
在 SQL 里把日期转成 Unix 时间戳
SQL 里的转换取决于列类型和会话时区。最稳妥的是 PostgreSQL 的 TIMESTAMPTZ,因为它本身就代表一个瞬间。MySQL 的 DATETIME 类型不带自己的时区,起作用的是会话的 time_zone 设置。做迁移和报表时,请在查询里把时区钉死,而不是依赖连接的默认值。
- PostgreSQL:SELECT EXTRACT(EPOCH FROM TIMESTAMPTZ '2026-06-20 09:25:00+00')::BIGINT; // 1781947500
- PostgreSQL 取列值:SELECT EXTRACT(EPOCH FROM created_at)::BIGINT FROM events;
- MySQL(UTC 会话):SET time_zone = '+00:00'; SELECT UNIX_TIMESTAMP('2026-06-20 09:25:00');
- MySQL(具名/本地来源):SELECT UNIX_TIMESTAMP(CONVERT_TZ('2026-06-20 09:25:00', 'America/New_York', '+00:00')); // 需要时区表
- SQLite:SELECT strftime('%s', '2026-06-20T09:25:00Z');
- BigQuery:SELECT UNIX_SECONDS(TIMESTAMP '2026-06-20 09:25:00 UTC');
- BigQuery 毫秒:SELECT UNIX_MILLIS(TIMESTAMP '2026-06-20 09:25:00 UTC');
- ClickHouse:SELECT toUnixTimestamp(toDateTime('2026-06-20 09:25:00', 'UTC'));
如果数据库里某列本来就是 Unix 整数,别再拿时间戳解析函数去解析它一遍。先确认这一列存的是秒、毫秒还是微秒。
如果 MySQL 的 CONVERT_TZ 对具名时区返回 NULL,说明服务器的时区表还没加载。这种情况下,要么在应用代码里用一个感知 IANA 的库来换算本地钟面值,要么先把 MySQL 时区表装上,再去依赖具名时区。
在 Excel 里把日期转成 Unix 时间戳
Excel 把日期存成以天为单位的序列号。如果 A1 装的是一个真正的 Excel 日期/时间值,就减去 1970-01-01 对应的序列值,再乘以一天的秒数。这对导入的 CSV 日期、手工输入的日期单元格,以及返回日期的公式都适用。它不知道时区,所以请在表里标注来源时区。
- 秒:=(A1 - DATE(1970,1,1)) * 86400
- 整数秒:=INT((A1 - DATE(1970,1,1)) * 86400)
- 毫秒:=(A1 - DATE(1970,1,1)) * 86400000
- 微秒:=(A1 - DATE(1970,1,1)) * 86400000000
- 反方向:=A1/86400 + DATE(1970,1,1)
- 结果单元格设成“数值”格式,不要设成“日期”
举个例子:如果 A1 是 2026-06-20 09:25,而你想让它表示 UTC,那么秒的公式应当算出 1781947500。如果不是,先检查 A1 是不是文本而非真正的 Excel 日期。
在 Google 表格里把日期转成 Unix 时间戳
Google 表格同样使用日期序列号,所以 Excel 那套公式照用。日期转纪元直接套公式即可;Google 表格虽然有 EPOCHTODATE,但那是反方向用的,日期转纪元这条路仍然是序列日期算术。
- 秒:=(A1 - DATE(1970,1,1)) * 86400
- 整数秒:=INT((A1 - DATE(1970,1,1)) * 86400)
- 毫秒:=(A1 - DATE(1970,1,1)) * 86400000
- 微秒:=(A1 - DATE(1970,1,1)) * 86400000000
- 如果 A1 是文本,先用 DATEVALUE / TIMEVALUE 解析,或者按日期列导入
- 用 NOW()、TODAY() 或手工录入日期时间之前,先看一下「文件 → 设置 → 时区」
反方向的话,Google 的文档写的是 EPOCHTODATE(timestamp, unit),其中单位 1 是秒、2 是毫秒、3 是微秒。
钟面到瞬间:夏令时的空档与重叠怎么处理
难的那个方向,是从本地钟面时间走到 Unix 时间戳。大部分本地时间对应唯一一个瞬间,但在夏令时边界附近,有些本地时间对应零个瞬间,有些对应两个。如果你的产品要设提醒、续费、类 cron 任务或本地截止时间,就得选定并写明一条策略。
- 春季前拨的空档:一个不存在的钟面值,比如 America/New_York 的 2026-03-08 02:30。
- 秋季回拨的重叠:一个出现两次的钟面值,比如 America/New_York 的 2026-11-01 01:30。
- 靠前那次的例子:2026-11-01T01:30:00-04:00 → 1793511000。
- 靠后那次的例子:2026-11-01T01:30:00-05:00 → 1793514600。
- 产品策略:要么拒绝不可能的时间,要么明确取靠前或靠后的那个。
- 存储策略:当本地含义以后还会用到时,UTC 纪元值和 IANA 时区都要存。
入库前的检查清单
把结果存进去之前,确认四件事:来源时区、输出单位、夏令时行为、字段名。就是这么一份小清单,专门用来抓那些换台服务器部署之后才冒出来的 bug。
速查:各工具的日期转纪元写法
如果你已经知道输入时区、只是想找对语法,看这一节。下面的例子统一用 2026-06-20 09:25 UTC,除特别说明外都返回 1781947500 秒。
| 工具 | Unix 秒写法 | 备注 |
|---|---|---|
| JavaScript | Date.UTC(2026, 5, 20, 9, 25) / 1000 | 月份索引从 0 开始 |
| Python | int(datetime(2026, 6, 20, 9, 25, tzinfo=timezone.utc).timestamp()) | 绝不要用 naive datetime |
| PHP | strtotime('2026-06-20 09:25:00 UTC') | 应用代码里优先 DateTimeImmutable |
| Go | time.Date(2026, time.June, 20, 9, 25, 0, 0, time.UTC).Unix() | location 是显式的 |
| Linux shell | date -u -d '2026-06-20 09:25:00' +%s | GNU date |
| macOS shell | date -j -u -f '%Y-%m-%d %H:%M:%S' '2026-06-20 09:25:00' +%s | BSD date |
| PostgreSQL | EXTRACT(EPOCH FROM TIMESTAMPTZ '2026-06-20 09:25:00+00')::BIGINT | 优先 TIMESTAMPTZ |
| MySQL | SET time_zone = '+00:00'; SELECT UNIX_TIMESTAMP('2026-06-20 09:25:00'); | 会话时区很关键 |
| Excel | =(A1 - DATE(1970,1,1)) * 86400 | A1 必须是真正的日期单元格 |
| Google 表格 | =(A1 - DATE(1970,1,1)) * 86400 | 录入日期前先确认表格时区 |
Frequent questions:
- Q: 怎么把日期转成 Unix 纪元值?
- A: 先选时区,再在该时区里解析这个日期,然后取出 Unix 秒或毫秒。例子:2026-06-20 09:25 UTC 是 1781947500 秒。JavaScript:Date.UTC(2026, 5, 20, 9, 25) / 1000。Python:datetime(2026, 6, 20, 9, 25, tzinfo=timezone.utc).timestamp()。Shell:date -u -d '2026-06-20 09:25:00' +%s。
- Q: 「日期转纪元」到底是什么意思?
- A: 就是把 2026-06-20 09:25 UTC 这样一个可读的日历值,变成那一瞬间对应的 Unix 时间戳。Unix 秒从 1970-01-01 00:00:00 UTC 起算,Unix 毫秒用的是同一个纪元,只是把秒值乘以 1000。
- Q: 日期转纪元用的是 UTC 吗?
- A: 纪元计数本身是以 UTC 为基准的,但你输入的日期可能是 UTC,也可能是某地的钟面时间。像 2026-06-20 这种只有日期的输入,表示你所选那个时区里的零点。给接口、日志和数据库存储用时,除非这个值明确是当地的业务时间,否则一律选 UTC。
- Q: 为什么同一个日期在不同时区算出的纪元值不一样?
- A: 因为一个钟面日期在配上时区之前,根本不是某个确定的瞬间。2026-06-20 09:25 在 UTC 是 1781947500;同样的钟面时间在 America/New_York 是 1781961900,在 Asia/Tokyo 是 1781915100。
- Q: 为什么我的 Python datetime 算出的纪元值不对?
- A: 通常是因为这个 datetime 不带时区。调用 .timestamp() 时 Python 会把 naive datetime 当成本地时间,于是笔记本和 UTC 服务器会算出不同的值。转换前请先设好 tzinfo=timezone.utc 或 ZoneInfo('America/New_York')。
- Q: JavaScript 里怎么把日期转成纪元值?
- A: 如果各个日期分量本来就该按 UTC 理解,用 Date.UTC(year, monthIndex, day, hour, minute),再除以 1000 得到秒。注意 monthIndex 是从 0 开始的:六月是 5。解析字符串时,只解析带 Z 或明确偏移的,比如 2026-06-20T09:25:00Z。
- Q: 该输出纪元秒还是毫秒?
- A: 跟着接收方走。Unix 工具、Python、PHP、SQL 和很多后端接口通常要秒;JavaScript 的 Date、Java 里 currentTimeMillis 风格的代码、浏览器埋点和部分事件流要毫秒。把单位写进字段名:scheduledAtSeconds 或 scheduledAtMs。
- Q: Excel 里怎么把日期转成 Unix 时间戳?
- A: 如果 A1 是一个真正的 Excel 日期/时间值,用 =(A1 - DATE(1970,1,1)) * 86400 得到 Unix 秒,乘 86400000 得到毫秒。结果单元格要设成“数值”格式,不是“日期”。Excel 公式不感知时区,所以请在列标题里注明目标时区。
- Q: Google 表格里怎么把日期转成 Unix 时间戳?
- A: 用同样的序列日期公式:=(A1 - DATE(1970,1,1)) * 86400 得到秒,乘 86400000 得到毫秒。Google 表格有 EPOCHTODATE 用于反方向,但日期转纪元还是用公式最合适。
- Q: 把本地日期转成纪元值时,夏令时怎么处理?
- A: 用 IANA 时区加一个感知时区的 API。有些钟面时间在春季前拨时根本不存在,有些在秋季回拨时会出现两次。你得决定产品是拒绝这个有歧义的时间,还是明确取靠前或靠后的那个瞬间。
- Q: 「时间戳转纪元」和「日期转纪元」是一回事吗?
- A: 如果这个时间戳已经是 10 位的 Unix 整数,那它本身就是纪元秒;13 位的通常是纪元毫秒。如果它是数据库里的 TIMESTAMP 或 TIMESTAMPTZ 值,就用 EXTRACT(EPOCH FROM col)、UNIX_TIMESTAMP(col) 或对应数据库的等价函数把纪元值取出来。