转换之前,先把时区和单位定下来

一个日历值要变成 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) 或对应数据库的等价函数把纪元值取出来。