只要把两个问题定下来,转换本身很简单

Unix 时间戳 1700000000 转换结果是:

2023-11-14T22:13:20Z
2023 年 11 月 14 日星期二 22:13:20 UTC

在 JavaScript 里,一行就够:

new Date(1700000000 * 1000).toISOString();
// "2023-11-14T22:13:20.000Z"

代码是最容易的部分。真正该在相信结果之前回答的,是这两个问题:

  1. 输入的单位是秒、毫秒、微秒,还是纳秒?
  2. 结果该按 UTC 显示,还是按某个具体的本地时区?

单位搞错,一个 2023 年的事件就落到了 1970 年。时区搞错,瞬间倒是没错,但用户看到的小时数、甚至那个日历日期,都不是他预期的。

第一步:把单位坐实

对于接近今天的时间戳,数位数可以做第一道判断。但这是经验规则,不是数据约定。

示例 大概率的单位 常见来源 按秒收的 API 需要怎么处理
1700000000 Unix/POSIX API、PHP、Python 原样使用
1700000000000 毫秒 JavaScript、Java、业务数据库 除以 1,000
1700000000000000 微秒 数据仓库、事件流 除以 1,000,000
1700000000000000000 纳秒 Go、链路追踪、遥测 除以 1,000,000,000

那个经典的单位 bug 很容易复现:

new Date(1700000000).toISOString();
// "1970-01-20T16:13:20.000Z" —— 秒被当成了毫秒

new Date(1700000000 * 1000).toISOString();
// "2023-11-14T22:13:20.000Z"

但别把数位数的规则固化成长期的业务逻辑。9 位的值可能是 2001 年之前的合法 Unix 时间戳,而到 2286 年 Unix 秒会变成 11 位。更要紧的是,01 这种小测试值,压根不告诉你它想表达什么单位。

在系统边界上,请用能自带约定的名字:createdAtSecondscreated_at_mseventTimeMicros。如果这个值来自 API、webhook、数据库列、JWT 或 CSV 导出,那它的文档说了算,优先级高于任何靠长度做的猜测。

本站的转换器能自动把毫秒和常规的 Unix 秒区分开。如果输入是微秒或纳秒,请先自行折算成秒或毫秒再粘贴进来。

第二步:把瞬间和它的显示分开

Unix 时间表示的是从 1970-01-01 00:00:00 UTC 这个 Unix 纪元起流逝的时间。在日常开发里,它标识的是一个瞬间。它内部并不包含纽约、东京或服务器本地时区这类信息。

The Open Group 的 POSIX 定义比“1970 年以来的秒数”这种简写要精确得多,尤其是在闰秒方面。但对普通应用的转换来说,好用的心智模型依然是:把数字解码成一个瞬间,再把这个瞬间渲染到选定的时区里。

同一个值在四个时区里的样子:

时区 1700000000 对应的本地日期时间
UTC 2023-11-14 22:13:20
America/New_York 2023-11-14 17:13:20(EST)
Asia/Tokyo 2023-11-15 07:13:20(JST)
Australia/Sydney 2023-11-15 09:13:20(AEDT)

同一个事件,可纽约和东京的日历上写的日期都不一样。时间戳没有移动,变的只是钟面表示。

把下面这套当默认策略:

  • 日志、API 负载、数据库比较和审计数据一律保持 UTC。
  • 在展示层才按用户的 IANA 时区(比如 America/New_York)格式化。
  • 当用户的本地时间语境重要时,把 IANA 时区另外存一份。
  • 别把一个光秃秃的本地日期时间字符串,当成某个瞬间的唯一记录。

按任务挑对应的转换方式

任务 推荐做法
查看单个时间戳 Epoch 转日期工具
JavaScript 秒 new Date(seconds * 1000)
JavaScript 毫秒 new Date(milliseconds)
Python 的 UTC datetime datetime.fromtimestamp(seconds, tz=timezone.utc)
Linux 输出 UTC date -u -d @seconds
macOS 输出 UTC date -u -r seconds
PostgreSQL to_timestamp(seconds)
MySQL 先确认会话时区,再用 FROM_UNIXTIME(seconds)
SQLite datetime(seconds, 'unixepoch')
BigQuery TIMESTAMP_SECONDSTIMESTAMP_MILLISTIMESTAMP_MICROS
Excel 秒 =A1/86400 + DATE(1970,1,1)
Google 表格秒 =EPOCHTODATE(A1, 1)

觉得某次转换不对时,先把 UTC 的值算出来。只要那个瞬间是对的,本地格式化就是另一个小得多的独立问题了。

JavaScript:记住 Date 用的是毫秒

JavaScript 的 Date 时间戳值是自 Unix 纪元起的毫秒数。Unix 秒要乘以 1,000

const createdAtSeconds = 1700000000;
const createdAt = new Date(createdAtSeconds * 1000);

createdAt.toISOString();
// "2023-11-14T22:13:20.000Z"

13 位的毫秒值本来就是 Date 期待的单位:

const createdAtMs = 1700000000000;

new Date(createdAtMs).toISOString();
// "2023-11-14T22:13:20.000Z"

面向用户的文案,请显式设置 timeZone。否则结果取决于执行这段代码的浏览器、服务器、容器或测试运行器。

const formatter = new Intl.DateTimeFormat("zh-CN", {
  timeZone: "America/New_York",
  dateStyle: "medium",
  timeStyle: "short",
});

formatter.format(new Date(1700000000 * 1000));
// "2023年11月14日 17:13"

如果你的应用知道单位,就把它做成参数,别靠猜:

function unixToDate(value, unit = "seconds") {
  if (!Number.isFinite(value)) {
    throw new TypeError("timestamp must be a finite number");
  }

  if (unit === "seconds") return new Date(value * 1000);
  if (unit === "milliseconds") return new Date(value);
  if (unit === "microseconds") return new Date(Math.trunc(value / 1000));

  throw new RangeError(`Unsupported unit: ${unit}`);
}

unixToDate(1700000000, "seconds").toISOString();
// "2023-11-14T22:13:20.000Z"

表单和 JSON 里的值要多留个心眼。JavaScript 会接受一些技术上合法、但多半不是你本意的输入:

new Date(null).toISOString();
// "1970-01-01T00:00:00.000Z"

new Date(undefined).toString();
// "Invalid Date"

另外,纳秒级的纪元值超出了 JavaScript Number 的精确整数范围。在你有意识地降低精度之前,请把它们保留为 BigInt 或字符串。

Python:构造一个带时区的 UTC datetime

构造 datetime 的时候就把时区传进去:

from datetime import datetime, timezone

created_at = datetime.fromtimestamp(1700000000, tz=timezone.utc)

created_at.isoformat()
# '2023-11-14T22:13:20+00:00'

不传 tz 的话,fromtimestamp() 会用机器的本地时区,并返回一个 naive datetime:

datetime.fromtimestamp(1700000000)
# 结果取决于机器的本地时区

调用按秒收的 API 之前,先把亚秒单位折算好:

datetime.fromtimestamp(1700000000000 / 1_000, tz=timezone.utc)
datetime.fromtimestamp(1700000000000000 / 1_000_000, tz=timezone.utc)

Python 3.11 及以后还提供了 datetime.UTC

from datetime import UTC, datetime

datetime.fromtimestamp(1700000000, tz=UTC)

新代码请避开 datetime.utcfromtimestamp()。它返回 naive 对象,从 Python 3.12 起已被弃用;文档给出的替代写法是 datetime.fromtimestamp(timestamp, UTC)

对于远超正常工作范围的日期,fromtimestamp() 可能抛出 OverflowErrorOSError,具体看平台。单位错误经常就是以范围错误的形式冒出来的——这也正是“先验证单位、再怪日期库”的好理由。

Python datetime.fromtimestamp

PHP:有意识地在 UTC 和具名时区之间做选择

gmdate() 按 UTC 格式化 Unix 秒:

echo gmdate('c', 1700000000);
// 2023-11-14T22:13:20+00:00

相比之下,date() 用的是 PHP 配置的默认时区。应用代码若需要具名时区,DateTimeImmutable 能让转换保持显式:

$instant = new DateTimeImmutable('@1700000000');

echo $instant
  ->setTimezone(new DateTimeZone('Asia/Tokyo'))
  ->format(DateTimeInterface::ATOM);
// 2023-11-15T07:13:20+09:00

这些 API 收的都是秒。毫秒输入请先除以 1,000

PHP gmdate

Java:用 Instant 表示时间轴上的那个点

java.time 把单位直接写进了方法名里:

Instant instant = Instant.ofEpochSecond(1700000000L);

instant.toString();
// 2023-11-14T22:13:20Z

毫秒:

Instant instant = Instant.ofEpochMilli(1700000000000L);

只有在需要本地日历字段时,才附加时区:

ZonedDateTime tokyo = Instant
    .ofEpochSecond(1700000000L)
    .atZone(ZoneId.of("Asia/Tokyo"));

老的 java.util.Date(long) 收的是毫秒,所以直接把 Unix 秒传进去,就会得到那个眼熟的 1970 错误。

Oracle Java Instant · Java 时间戳代码片段

C# 和 .NET:让 DateTimeOffset 承载这个瞬间

用名字里写明输入单位的那个方法:

var createdAt = DateTimeOffset.FromUnixTimeSeconds(1700000000);

createdAt.ToString("O");
// 2023-11-14T22:13:20.0000000+00:00

毫秒:

var createdAt = DateTimeOffset.FromUnixTimeMilliseconds(1700000000000);

展示时再做时区转换:

var zone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
var local = TimeZoneInfo.ConvertTime(createdAt, zone);

时区标识的支持情况取决于操作系统和 .NET 的部署方式。别想当然地以为某个 IANA 或 Windows ID 在哪儿都能用,请在应用实际运行的平台上测一遍。

微软 DateTimeOffset.FromUnixTimeSeconds · C# 时间戳代码片段

Go:按精度选对应的辅助函数

Go 最早的构造函数收的是秒加纳秒偏移:

t := time.Unix(1700000000, 0).UTC()

fmt.Println(t.Format(time.RFC3339))
// 2023-11-14T22:13:20Z

现在的 Go 也为常见的亚秒纪元值提供了辅助函数:

time.UnixMilli(1700000000000).UTC()
time.UnixMicro(1700000000000000).UTC()

对于完整的纳秒纪元计数,这种写法能保住原本的单位含义:

time.Unix(0, 1700000000000000000).UTC()

要产出钟面输出时,加载一个 IANA location:

loc, err := time.LoadLocation("Europe/Berlin")
if err != nil {
    log.Fatal(err)
}

fmt.Println(time.Unix(1700000000, 0).In(loc).Format(time.RFC3339))

Go 的 time · Go 时间戳代码片段

用 Linux 和 macOS 的 date 命令转换 Unix 时间戳

别扭的地方不在 Unix 时间本身,而在于 date 这个命令在各处并不是同一个东西。

环境 常见实现 需要记住的点
Ubuntu、Debian、Fedora、RHEL GNU coreutils 的 date 支持 -d@SECONDS%N--rfc-3339--iso-8601
macOS 和 FreeBSD BSD 的 date 纪元转日期要用 -r SECONDS
Alpine 和精简 CI 镜像 往往是 BusyBox 的 date 选项少得多,请在具体镜像上实测
装了 GNU coreutils 的 macOS GNU 的 gdate 要用 Linux 兼容命令就调 gdate

在 GNU/Linux 上,纪元秒前面要加 @。如果结果需要在笔记本、服务器、容器和 CI 之间保持一致,就加上 -u

date -u -d @1700000000

date -u -d @1700000000 +'%Y-%m-%dT%H:%M:%SZ'
# 2023-11-14T22:13:20Z

date -u -d @1700000000 --rfc-3339=seconds
# 2023-11-14 22:13:20+00:00

只有当你确实想要机器本地时区时,才省掉 -u。想在不改动主机时钟的前提下渲染某个具名时区,就为这条命令单独设 TZ

TZ=America/New_York date -d @1700000000 +'%Y-%m-%d %H:%M:%S %Z'
# 2023-11-14 17:13:20 EST

TZ=Asia/Tokyo date -d @1700000000 +'%Y-%m-%d %H:%M:%S %Z'
# 2023-11-15 07:13:20 JST

这个 TZ= 前缀改变的只是显示出来的钟面值,它并不改变时间戳所代表的那个瞬间。

在 shell 里转换纪元毫秒

大多数命令行时间戳工具读的是秒。把一个 13 位的毫秒值直接丢给 date -d @...,它会按秒解释,于是给你一个遥远未来的日期。如果整秒精度就够用,先除以 1,000

ms=1700000000000
seconds=$((ms / 1000))
date -u -d "@$seconds" +'%Y-%m-%dT%H:%M:%SZ'
# 2023-11-14T22:13:20Z

如果小数毫秒也要保留,就把值拆成秒和最后三位:

ms=1700000000123
seconds=${ms%???}
millis=${ms#$seconds}

date -u -d "@$seconds.$millis" +'%Y-%m-%dT%H:%M:%S.%3NZ'
# 2023-11-14T22:13:20.123Z

变量名里也带上单位,比如 created_at_screated_at_msclient_sent_at_ms。这比在脚本深处埋一段聪明的位数猜测,能挡掉多得多的错误。

在 macOS 和 FreeBSD 上使用 BSD 的 date

BSD 的 date-r SECONDS,而不是 GNU 的 -d @SECONDS

date -u -r 1700000000 +'%Y-%m-%dT%H:%M:%SZ'
# 2023-11-14T22:13:20Z

TZ=America/New_York date -r 1700000000 +'%Y-%m-%d %H:%M:%S %Z'
# 2023-11-14 17:13:20 EST

同一个选项在两边可能是完全不同的意思:GNU 的 date -r FILE 显示文件的修改时间,而 BSD 的 date -r SECONDS 转换纪元秒。如果 macOS 上的脚本需要 Linux 语法,就装上 GNU coreutils 并显式调用 gdate

gdate -u -d @1700000000 +'%Y-%m-%dT%H:%M:%SZ'

调用 date 之前先校验输入

对于脚本参数,请直接拒掉意料之外的输入,而不是默默猜单位:

ts=${1:-}

case "$ts" in
  ''|*[!0-9]*)
    printf 'usage: %s UNIX_SECONDS\n' "$0" >&2
    exit 2
    ;;
esac

date -u -d "@$ts" +'%Y-%m-%dT%H:%M:%SZ'

这个例子是有意只接受正整数秒的。如果负时间戳或毫秒也算合法,那就把单位做成一个独立参数,别让脚本自己去推断。

date 命令常见故障排查

症状 可能原因 解决办法
date: illegal option -- d 用的是 macOS/BSD 的 date 改用 date -u -r SECONDS,或安装 GNU 的 gdate
输出在 1970 年附近 秒被别处按毫秒的 API 处理了 到边界上去查单位
输出在遥远的未来 毫秒被当成秒传给了 date 除以 1,000,或保留小数部分
date +%s%3N 结尾出现 3N BSD 的 date 不支持 GNU 的 %N 改用 Python、Node.js、Perl 或 gdate
Ubuntu 上好好的,Alpine 上就挂 BusyBox 的 date 选项更少 在具体镜像上实测,或装 coreutils
笔记本和服务器显示的小时不一样 用了本地时区 -u,或显式写 TZ=区域/城市

如果脚本得同时兼容 GNU 和 BSD 的 date,加一小段平台分支很实用:

if date --version >/dev/null 2>&1; then
  date -u -d @1700000000 +'%Y-%m-%dT%H:%M:%SZ'
else
  date -u -r 1700000000 +'%Y-%m-%dT%H:%M:%SZ'
fi

在可复现构建里解析 SOURCE_DATE_EPOCH

很多构建工具把 SOURCE_DATE_EPOCH 识别为纪元秒,好让生成的文件拿到稳定的时间戳:

export SOURCE_DATE_EPOCH=1700000000
date -u -d "@$SOURCE_DATE_EPOCH" +'%Y-%m-%dT%H:%M:%SZ'
# 2023-11-14T22:13:20Z

除非使用它的工具明确写了别的单位,否则这个值就保持秒。

GNU Coreutils date 手册 · Linux date(1) 手册 · FreeBSD date(1) 手册

SQL:转换和渲染用的可能是不同的时区

数据库函数写起来很简洁,但它们显示出来的结果可能取决于会话时区。

数据库 Unix 秒转换 需要注意的行为
PostgreSQL to_timestamp(1700000000) 返回 timestamp with time zone,显示跟随会话时区
MySQL FROM_UNIXTIME(1700000000) 按当前会话时区渲染
SQLite datetime(1700000000, 'unixepoch') 除非加 localtime,否则返回 UTC
BigQuery TIMESTAMP_SECONDS(1700000000) 用单位明确的构造函数

PostgreSQL:

SELECT to_timestamp(1700000000) AT TIME ZONE 'UTC';
-- 2023-11-14 22:13:20

MySQL:

SET time_zone = '+00:00';
SELECT FROM_UNIXTIME(1700000000);
-- 2023-11-14 22:13:20

SQLite:

SELECT datetime(1700000000, 'unixepoch');
-- 2023-11-14 22:13:20

BigQuery 把精度写得很明白:

SELECT TIMESTAMP_SECONDS(1700000000);
SELECT TIMESTAMP_MILLIS(1700000000000);
SELECT TIMESTAMP_MICROS(1700000000000000);

做数据迁移时,不妨在数据审计完成之前,把原始整数和解析后的时间戳都留着。万一有记录是按错误单位导进来的,你还能顺着这条线索找回去。

Excel:先把秒换算成天,再设置单元格格式

Excel 里日期是以“天”为单位的序列号存储的。把 Unix 秒除以一天的秒数,再加上纪元日期:

=A1/86400 + DATE(1970,1,1)

其他单位:

=A1/86400000 + DATE(1970,1,1)      // 毫秒
=A1/86400000000 + DATE(1970,1,1)   // 微秒

如果单元格显示成小数,套一个日期时间数字格式就行——公式本身可能已经是对的。

电子表格带来两个实际风险:它不给序列值附加时区,而且 CSV 导入时可能把大整数显示成科学计数法。如果某列时间戳对精度敏感,就按文本导入,并且在列名里写清楚它是 UTC 还是本地时间。

微软 Excel DATE 函数

Google 表格:用 EPOCHTODATE 并写明单位

Google 表格通过一个单位参数支持秒、毫秒和微秒:

=EPOCHTODATE(A1, 1)  // 秒
=EPOCHTODATE(A1, 2)  // 毫秒
=EPOCHTODATE(A1, 3)  // 微秒

结果是 UTC,不是表格的本地时区。另外这个函数不接受负时间戳。

对于正的 Unix 秒,按天换算的公式也是一种选择:

=A1/86400 + DATE(1970,1,1)

在多人共享的表格里,created_at_seconds_utccreated_at_ms_utc 这样的表头,能让下一个人不必去逆向推导单位和时区。

Google 表格 EPOCHTODATE

按症状排查错误日期

症状 可能原因 先试这个
落在 1970 年 1 月 秒被传给了按毫秒收的 API 1,000
落在几千年后的未来 毫秒或微秒被当成秒 1,0001,000,000
笔记本和服务器结果不一致 各自用了默认时区 显式指定 UTC 或某个 IANA 时区
Python 的 datetime 没有偏移 创建的是 naive datetime tz=timezone.utc
MySQL 输出整体差了几小时 会话时区不同 设置或转换会话时区
Google 表格拒绝这个值 负时间戳或单位不对 检查该函数支持的范围和单位参数
CSV 里的值掉了几位 电子表格的数值强制转换 按文本导入,或用整数安全的工具
1970 年之前的日期失败 目标平台范围受限 就在那个系统上实测负时间戳

绝大多数时间戳转换故障,最后都能归到单位、时区或范围这三样上。在动手替换本来能跑的日期代码之前,先把这三个查一遍。

在测试里留一小组已知值

只测一个顺利路径的值是不够的。下面这些用例能把最常见的假设都翻出来:

时间戳 期望的 UTC 值 它在测什么
0 1970-01-01T00:00:00Z 纪元边界
-1 1969-12-31T23:59:59Z 是否支持负时间戳
1000000000 2001-09-09T01:46:40Z 较早的 10 位秒值
1700000000 2023-11-14T22:13:20Z 普通秒值
2147483647 2038-01-19T03:14:07Z 32 位有符号边界
1700000000000 2023-11-14T22:13:20Z 毫秒输入
1700000000000000 2023-11-14T22:13:20Z 微秒输入

如果有用户界面,至少再加一条时区断言:

1700000000 在 America/New_York = 2023-11-14 17:13:20 EST
1700000000 在 Asia/Tokyo       = 2023-11-15 07:13:20 JST

这条测试能抓住那种“瞬间算对了、但展示用错了时区”的转换。

值得带进生产的那条准则

把 Unix 时间转成可读日期,其实不是一个操作,而是一条短流水线:

原始整数 → 已知单位 → UTC 瞬间 → 带时区的显示

这几个阶段不该混在一起。在边界上把单位定名,中间一路保持 UTC 瞬间,只有在渲染给人看的时候才挑时区。这份约定一旦立住,各语言的具体写法就只是例行公事,而不是什么玄学。

官方参考资料

相关文章

Frequent questions:

Q: 怎么把 Unix 时间转成日期?
A: 先认单位。1700000000 按 Unix 秒算,1700000000000 按毫秒算,两者都表示 2023-11-14T22:13:20Z。先把值按 UTC 解码,再把这个瞬间格式化到你需要的时区。
Q: 为什么我的 Unix 时间戳转出来是 1970 年?
A: 通常是单位对不上。JavaScript 的 Date 收的是毫秒,所以 new Date(1700000000) 会把它读成纪元之后 17 亿毫秒。Unix 秒请写成 new Date(1700000000 * 1000)。
Q: JavaScript 里怎么把 Unix 时间转成日期?
A: 秒用 new Date(seconds * 1000).toISOString();毫秒直接传给 new Date(milliseconds)。要输出本地格式,用 Intl.DateTimeFormat 并显式指定 timeZone。
Q: Python 里怎么把 Unix 时间转成日期?
A: 用 datetime.fromtimestamp(seconds, tz=timezone.utc)。tz 这个参数很关键——不传它,返回的是机器本地时区的 datetime。新代码请避开 utcfromtimestamp(),它返回 naive datetime 且已被弃用。
Q: SQL 里怎么把 Unix 时间转成日期?
A: 用你数据库的原生函数:PostgreSQL 的 to_timestamp(seconds)、MySQL 的 FROM_UNIXTIME(seconds)、SQLite 的 datetime(seconds, 'unixepoch'),或者 BigQuery 的 TIMESTAMP_SECONDS / TIMESTAMP_MILLIS / TIMESTAMP_MICROS。比较渲染出来的字符串之前,先确认数据库会话时区。
Q: 13 位的时间戳怎么转成日期?
A: 当下的 13 位纪元值通常是毫秒。JavaScript 的 Date、Java 的 Instant.ofEpochMilli 和 .NET 的 FromUnixTimeMilliseconds 都直接收毫秒。而 Python、PHP、PostgreSQL、MySQL 以及很多 shell 工具通常要秒,所以得先除以 1000。
Q: 16 位的时间戳怎么转成日期?
A: 当下的 16 位纪元值通常是微秒。给按秒收的 API 就除以 1,000,000,或者直接用原生的微秒函数,比如 BigQuery 的 TIMESTAMP_MICROS 或 Go 的 time.UnixMicro。
Q: Unix 时间该显示成 UTC 还是本地时间?
A: 日志、接口、数据库导出、审计记录和跨系统排查,一律用 UTC。仪表盘、截止时间、收据和日历类界面,用用户的 IANA 时区。原始的那个瞬间始终是唯一事实来源。
Q: Unix 时间可以是负数吗?
A: 可以。负的 Unix 秒表示 1970-01-01 UTC 之前的瞬间。很多语言都支持,但电子表格、数据库和操作系统调用的范围可能窄得多,所以 1970 年以前的数据请在真实的目标系统上实测。
Q: Unix 时间和纪元时间是一回事吗?
A: 在大多数开发语境里,Unix 时间、Unix 时间戳、纪元时间和 POSIX 时间都指从 1970-01-01 00:00:00 UTC 起算的计数。但单位没有保证,所以务必确认这个系统存的是秒、毫秒、微秒还是纳秒。
Q: 为什么 macOS 上 date -d 会报错?
A: macOS 自带的是 BSD date,不是 GNU date。转换纪元秒请用 date -u -r 1700000000,或者装上 GNU coreutils 后运行 gdate -u -d @1700000000。
Q: 为什么 macOS 上 date +%s%3N 会打印出 3N?
A: %N 这个纳秒格式是 GNU date 的扩展。BSD date 可能原样打印 3N,所以要拿到可移植的当前纪元毫秒,请用 Python、Node.js、Perl 或 GNU 的 gdate。