Unix 时间戳是相对于某个纪元、以明确时间尺度表示的数字。只有同时知道纪元、单位、时间尺度、正负号含义和展示时区,转换结果才有意义。数字位数可以为当代数据提供线索,但不能证明它一定是秒、毫秒、微秒或纳秒。

核心要点

  • 在协议中明确写出单位:smsusns,不要把位数猜测当成数据契约。
  • Unix 时间通常相对于 1970-01-01T00:00:00Z 表示;负值代表纪元之前的时刻。
  • UTC 适合交换和存储;本地展示应使用 Asia/ShanghaiAmerica/New_York 等 IANA 时区标识。
  • Date、Python datetime 和数据库类型的精度、范围不同;纳秒转毫秒是有损展示操作。
  • 单调时钟用于测量时长,不是纪元时间,不能转换成日历日期。

先定义时间戳契约

在打开在线转换器或编写代码前,应把以下元数据和数值放在一起:

字段 示例
纪元 Unix epoch,1970-01-01T00:00:00Z
时间尺度 POSIX 风格时间,或有文档说明的系统尺度
单位 ms
编码 有符号十进制整数、字符串或二进制整数
精度策略 保留、截断或舍入
展示方式 UTC,或使用 IANA tzdata 的 Europe/Berlin

Unix/POSIX 模型不会把闰秒作为独立的民用时间戳编码。如果来源使用 TAI、GPS time、设备 tick 计数器或厂商自定义时间尺度,就需要另一套转换契约。“epoch 时间”不是可以随意假定时间尺度的理由。

当单位已经确认时,时间戳转换器适合查询单个、非敏感的数值。对于敏感数据,应先确认处理和留存策略;也可以直接运行本地脚本,避免粘贴凭据、完整日志或个人信息。

单位与精度

单位 换算为秒的倍率 常见表示
秒(s 1 1738886400
毫秒(ms 1,000 1738886400000
微秒(us 1,000,000 1738886400000000
纳秒(ns 1,000,000,000 1738886400000000000

上面的示例恰好表示同一时刻,但这些数字不能互换。今天的 13 位数字可能是毫秒,应用也可以选择其他单位或日期范围。负值还会让简单的 length <= 10 规则失效。只有在确认字段名、Schema、生产者代码或样本后,才能把位数识别当作有限的诊断启发式。

精度、分辨率、准确度、同步性和事件排序保证不是同一个概念。一个长得像纳秒的整数不能证明时钟真的测量到了 1 纳秒间隔;跨机器的两个纳秒字段也不能自动证明因果顺序。

时区与民用时间

存储时应保留时刻和单位,优先使用有类型的数据库列或规范化 UTC 表示;只在界面边界转换为本地民用时间。时区不是固定偏移量,IANA 时区包含历史变化和夏令时规则。UTC+08:00Asia/Shanghai 对所有历史日期并不等价。

夏令时切换可能产生不存在或重复的本地时间。把本地日期时间解析回时刻时,应明确选择较早偏移、较晚偏移,或直接拒绝歧义输入。保留原始时刻,才能在以后更换展示时区而不丢失信息。

正确的转换代码

JavaScript:显式单位并保留毫秒以下部分

下面的示例接受整数或 bigint,返回毫秒级 Date 以及余数,而不是假装 Date 能保存纳秒:

javascript
const NS_PER_SECOND = 1_000_000_000n;
const NS_PER_MILLISECOND = 1_000_000n;

function toEpochNanoseconds(value, unit) {
  if (!/^-?\d+$/.test(String(value))) {
    throw new TypeError("value must be a signed integer");
  }
  const integer = BigInt(value);
  const multipliers = {
    s: NS_PER_SECOND,
    ms: NS_PER_MILLISECOND,
    us: 1_000n,
    ns: 1n,
  };
  if (!(unit in multipliers)) throw new RangeError("unsupported unit");
  return integer * multipliers[unit];
}

export function epochToUtc(value, unit) {
  const ns = toEpochNanoseconds(value, unit);
  const milliseconds = ns >= 0n
    ? ns / NS_PER_MILLISECOND
    : -((-ns + NS_PER_MILLISECOND - 1n) / NS_PER_MILLISECOND);
  const remainder = ns - milliseconds * NS_PER_MILLISECOND;
  const numericMilliseconds = Number(milliseconds);
  if (!Number.isSafeInteger(numericMilliseconds)) {
    throw new RangeError("outside the exact JavaScript Date range");
  }
  const date = new Date(numericMilliseconds);
  if (Number.isNaN(date.getTime())) throw new RangeError("invalid Date range");
  return { date, remainderNanoseconds: remainder };
}

console.log(epochToUtc("1738886400000000000", "ns"));

Date 只保存毫秒,因此余数是应用层数据,不属于 Date 对象。展示时使用带明确 timeZoneIntl.DateTimeFormat。当原始值可能超过 2^53 - 1 时,不要先转换为不安全的 Number

Python:对负值使用整除语义

Python 整数本身是精确的,而 datetime 通常只暴露微秒。先拆分整数,并明确说明被舍弃的精度:

python
from datetime import datetime, timezone

UNITS = {"s": 1, "ms": 1_000, "us": 1_000_000, "ns": 1_000_000_000}


def epoch_to_utc(value: int, unit: str) -> tuple[datetime, int]:
    if unit not in UNITS:
        raise ValueError("unit must be s, ms, us, or ns")
    if not isinstance(value, int):
        raise TypeError("value must be an integer")

    total_ns = value * (1_000_000_000 // UNITS[unit])
    seconds, remainder_ns = divmod(total_ns, 1_000_000_000)
    microseconds, discarded_ns = divmod(remainder_ns, 1_000)
    instant = datetime.fromtimestamp(seconds, tz=timezone.utc).replace(
        microsecond=microseconds
    )
    return instant, discarded_ns


instant, discarded = epoch_to_utc(-1, "ms")
assert instant.isoformat() == "1969-12-31T23:59:59.999000+00:00"
assert discarded == 0

divmod 对负值也能得到正确的向下取整结果。Python datetime 无法保存微秒字段之外的纳秒,因此若排序或审计依赖余数,就不能静默丢弃它。

Go:使用带单位的 API

Go 的 time.Unix 可以同时接收秒和纳秒;UnixMilliUnixMicro 让其他契约更明确:

go
package main

import (
	"fmt"
	"time"
)

func epochToUTC(value int64, unit string) (time.Time, error) {
	switch unit {
	case "s":
		return time.Unix(value, 0).UTC(), nil
	case "ms":
		return time.UnixMilli(value).UTC(), nil
	case "us":
		return time.UnixMicro(value).UTC(), nil
	case "ns":
		seconds := value / 1_000_000_000
		nanos := value % 1_000_000_000
		return time.Unix(seconds, nanos).UTC(), nil
	default:
		return time.Time{}, fmt.Errorf("unsupported unit %q", unit)
	}
}

int64time.Time 仍有范围限制。协议可能超出范围时,应先校验并保留字符串或更宽的表示。

排障工作流

处理 API 响应或日志时,可以按以下步骤操作:

  1. 从 Schema 或生产者代码确认字段类型、纪元、单位和时间尺度。
  2. 保存原始值及事件/日志标识,只提取必要字段并脱敏。
  3. 先无舍入地转换为 UTC,再按操作人员的 IANA 时区展示。
  4. 对多个已知事件进行交叉核对,必要时加入负值或边界值。
  5. 检查时钟同步、序列化、数据库精度,以及该字段是否其实是时长而非时刻。

转换只能检查表示是否一致,不能证明请求确实发生、各机器已经同步,或日志本身未被篡改。

纪元时间不是单调时钟

墙上时钟可能因为校时、人工修改、虚拟化或闰秒摊平策略向前或向后跳变。单调时钟用于测量经过时长:

  • JavaScript:performance.now()
  • Python:time.monotonic_ns()
  • Java:System.nanoTime()
  • Go:time.Now() 返回的 time.Time 中携带的单调部分

这些值没有 Unix 纪元,不能送入时间戳转换器。事件发生时间应使用墙上时钟,超时和延迟测量应使用单调时钟;需要同时追踪时,保存两者及其含义。

常见问题

只看位数能判断单位吗?

不能。它只是绑定特定日期范围和生产者习惯的启发式。应从 Schema、API 契约、代码或重复样本确认单位。

负 Unix 时间戳代表什么?

按 Unix 纪元约定,它代表 1970-01-01T00:00:00Z 之前的时刻。历史民用时间的展示仍取决于时间尺度和使用的时区数据库。

纳秒时间戳是否保证纳秒准确度?

不保证。它可能只是存储或序列化分辨率。时钟准确度、测量延迟、同步和跨主机排序需要单独的证据。

为什么 Python 或 JavaScript 会丢精度?

目标类型的精度或范围可能更小。JavaScript Date 保存毫秒,Python datetime 保存微秒,JSON/Number 也可能损失大整数精度。重要信息应保留原始整数和明确的余数。

生产数据适合使用在线转换器吗?

在单位已知且数据不敏感时,在线工具适合查询单个值。生产流水线应使用版本化代码、测试、访问控制和可审计数据路径;除非确认实现与策略,否则不能仅凭浏览器页面断言数据一定在本地处理或已经删除。

总结

时间戳转换首先是契约问题,其次才是界面问题。明确纪元、时间尺度、单位、精度策略和展示时区;保留精确输入;区分时刻与时长;把按位数自动识别限制为诊断提示。这样既能让一次性在线查询保持便利,也不会让一个看似合理的日期变成错误证据。

参考资料