如何在不同时间格式之间转换
时间格式因系统、API 和国家而异。API 响应中的 Unix 时间戳、数据库中的 ISO 8601、美国的 12 小时制、欧洲的 24 小时制,在它们之间转换是开发者以及任何处理国际数据的人持续不断的需求。理解每种格式为何存在、它在哪里发光、在哪里咬人,会让转换成为快速的反射而不是反复出现的微妙错误的来源。
时间格式简史
时间标准化比互联网更古老。1884 年的国际本初子午线会议确立了格林尼治标准时间和本初子午线,这是世界上第一次有了共同参照。24 小时制因同样的原因在航空和军事中传播开来:它消除了 AM/PM 的歧义,这种歧义能把午夜的预订变成中午的航班。
Unix 时间随 1971 年最初的 Unix 内核出现,从 1970 年 1 月 1 日 UTC(「纪元」)起计秒数。这个选择很务实:32 位整数能覆盖数十年,排序时间戳就是排序数字,计算间隔就是减法。ISO 8601 在 1988 年出现,把可读侧标准化:年-月-日的大端序、T 作为日期/时间分隔符、Z 表示 UTC。RFC 3339(2002 年)是为互联网协议设计的更严格的 ISO 8601 子集。RFC 2822(2001 年)定义了带星期名和偏移的邮件风格格式。每种格式都解决一个真实问题;没有一个完全替代其他。
常见时间格式
六种格式覆盖了你在野外遇到的几乎一切。第一列是你在文档中看到的名称;示例列展示 2024 年 4 月 7 日 UTC 的 14:30 看起来是什么样。
| 格式 | 示例 | 使用者 |
|---|---|---|
| Unix 时间戳(秒) | 1712502600 | API、数据库、JWT 令牌、日志行 |
| Unix 时间戳(毫秒) | 1712502600000 | JavaScript、Java、Android |
| Unix 时间戳(微秒) | 1712502600000000 | Postgres、Kafka、OpenTelemetry |
| ISO 8601 / RFC 3339 | 2024-04-07T14:30:00Z | JSON API、数据库、日志 |
| RFC 2822 | Sun, 07 Apr 2024 14:30:00 +0000 | 邮件头、HTTP 头 |
| 24 小时制 | 14:30 | 欧洲、军事、航空 |
| 12 小时制 | 2:30 PM | 美国、随意场合 |
| 一年中的第几天(儒略) | 2024-098 | 航电、科学数据 |
| 周日期 | 2024-W14-7 | 工业规划、银行 |
大多数团队会定下来:存储用 Unix 毫秒,传输用 ISO 8601,显示格式按区域设定选择。选择本身不如一致性重要:挑一个主格式,把它写下来,在边界处转换。
快速转换参考
12 小时制到 24 小时制
12 小时制有两个怪点:12 AM 是午夜(一天的开始),12 PM 是中午。下表覆盖让人犯错的情况。
| 12 小时 | 24 小时 |
|---|---|
| 12:00 AM(午夜) | 00:00 |
| 1:00 AM | 01:00 |
| 11:59 AM | 11:59 |
| 12:00 PM(中午) | 12:00 |
| 12:01 PM | 12:01 |
| 1:00 PM | 13:00 |
| 6:00 PM | 18:00 |
| 11:59 PM | 23:59 |
时间戳到日期
epoch 转换器即时将 Unix 时间戳翻译成可读日期并反向转换。转换器自动检测秒(10 位)和毫秒(13 位)格式,所以你可以从任何来源粘贴值,不必担心单位。
一个有用的合理性检查:除以 86400(一天的秒数)再加上 1970 天,就能心算定位任何时间戳。1712502600 / 86400 ~ 19820 天,约 1970 之后 54 年,把它放在 2024。不精确但足以一眼抓住 1000 倍误差。
代码中的转换示例
大多数开发者最终需要在代码中转换时间戳。核心 API 都很短:
- JavaScript,
new Date(1712502600 * 1000).toISOString()返回2024-04-07T14:30:00.000Z。记得乘以 1000,因为 JavaScript 的Date期望毫秒。反向:Math.floor(Date.now() / 1000)给出秒级时间戳。 - Python,
datetime.fromtimestamp(1712502600, tz=timezone.utc).isoformat()返回2024-04-07T14:30:00+00:00。避免utcfromtimestamp();它返回的是朴素 datetime,你代码的其他部分会错误地当作本地时间。 - Go,
time.Unix(1712502600, 0).UTC().Format(time.RFC3339)返回2024-04-07T14:30:00Z。Go 的 time 包不寻常,但一旦内化参考布局Mon Jan 2 15:04:05 MST 2006就很一致。 - Shell,GNU/Linux 上用
date -u -d @1712502600 +"%Y-%m-%dT%H:%M:%SZ",BSD/macOS 上用date -u -r 1712502600 +"%Y-%m-%dT%H:%M:%SZ"。这个 flag 差异是最常被搜索的跨平台暗坑之一。 - Rust,
chrono::DateTime::<chrono::Utc>::from_timestamp(1712502600, 0).unwrap().to_rfc3339()返回同样的 ISO 字符串。标准库现在有SystemTime,但 chrono 仍是格式化的实用选择。 - SQL(Postgres),
SELECT to_timestamp(1712502600) AT TIME ZONE 'UTC';返回timestamptz。MySQL 用FROM_UNIXTIME();SQLite 用datetime(1712502600, 'unixepoch')。
当你只需要翻译一个值时(调试时基本都是这样),转换器工具比任何这些单行命令都快。
为什么存储用 ISO 8601 获胜
ISO 8601(以及它的 RFC 3339 子集)作为纯字符串能正确排序。2024-04-07T14:30:00Z 在字典序上先于 2024-04-07T15:00:00Z,后者又先于 2024-04-08T00:00:00Z。这个特性让你不必解析就能排序时间戳,用 sort | uniq -c 分组日志行,用简单的字符串比较推断范围。
它在区域格式所没有的方式上也不含糊。04/07/2024 可能是 4 月 7 日(美国)或 7 月 4 日(欧洲大部分);2024-04-07 在每个区域都是同一天。对机器对机器的交换,这种无歧义比任何其他格式考量都更值钱。
要避免的常见陷阱
- 忘记时区,没有时区的
2026-04-07 14:30是模糊的。存储 ISO 字符串时总是包含Z或+00:00偏移,永远不要依赖读者的默认时区。 - 混淆秒与毫秒,10 位的 Unix 时间戳是秒,13 位的是毫秒。混淆它们会产生 1970 年或遥远未来的日期。微秒(16 位)和纳秒(19 位)也在出现,要预先想到。
- 在服务器上依赖本地时间,如果你的服务器在 UTC 而用户在东京,存储 UTC 并在显示时转换。让服务器推断区域几乎总会在用户出差或夏令时切换时导致 bug。
- 用字符串数学解析,不要切片和拼接日期字符串。使用
Date.parse()、dateutil.parser、chrono,或你语言的标准库。手动解析会在极端日期、闰年和时区偏移上崩溃。 - 信任
new Date("2024-04-07"),JavaScript 把仅日期字符串解析为 UTC 午夜,但把无时区的日期时间字符串解析为本地午夜。行为因浏览器和 Node 版本而异。总是包含显式Z或偏移。 - 两位数年份,
04/07/24可能是 1924、2024 或 2124,取决于实现。在任何地方都使用四位数年份。 - 夏令时切换,本地时钟在春天跳一小时、在秋天重复一小时。朴素的调度器在那些日子可能把任务跑两次或一次都不跑。计划逻辑用 UTC,仅在显示时转换。
- 闰秒,Unix 时间通过平滑或重复受影响的秒来假装它们不存在。比较「经过秒数」的代码可能短暂地与遵守闰秒的墙钟不一致。
- 2038 年问题,32 位有符号
time_t将在 2038 年 1 月 19 日溢出。大多数现代系统使用 64 位时间,但传统嵌入式设备和旧数据库列仍可能撞上。在你看到INT4或int32用于存储时间戳的任何地方都要审计。 - 依赖区域的格式化,JavaScript 中的
toLocaleString()以及其他语言中的等价函数在不同机器上产生不同输出。机器可读时间戳请用toISOString()或等价;显示则显式格式化。
替代工具与库
转换器处理一次性的场景;对于生产代码,你会去拿一个库。
| 工具 / 库 | 语言 | 优势 | 注意 |
|---|---|---|---|
| 网页转换器 | 浏览器 | 即时、免安装、处理常见歧义 | 一次一个值 |
dateutil | Python | 宽容的解析器,时区感知 | 批量处理比 datetime 慢 |
arrow | Python | 友好的 API,ISO 默认 | 对小项目是额外依赖 |
date-fns | JavaScript | 可摇树、不可变 | 每个函数都是单独的 import |
dayjs | JavaScript | 体积小,moment 兼容 API | 插件模型用于附加功能 |
luxon | JavaScript | 一流的 IANA 时区 | 包体更大 |
chrono | Rust | 丰富类型,原生 RFC 3339 | 维护节奏会变 |
Joda-Time / java.time | Java | 影响了其他设计的参考设计 | 避免遗留的 Date 和 Calendar |
NodaTime | C#/.NET | 瞬时、时长、日历清晰分离 | 从 DateTime 迁移并不简单 |
GNU date | Linux shell | -d 的灵活解析 | BSD/macOS 使用不兼容的 flag |
合适的库取决于你的技术栈;合适的纪律在哪里都一样:存储 UTC、传输 ISO 8601、仅在显示时格式化,永远不要信任没有显式时区的时间戳。
隐私与转换器
时间格式转换器完全在你的浏览器中运行。你粘贴的时间戳和生成的日期从不离开页面。没有服务器日志记录哪些值被转换,没有关于哪些格式流行的分析,也没人能把请求与你的 IP 联系起来。时间转换本身不是个人数据,但你日志中的时间戳(登录时间、交易时间、计划窗口)往往是,把它们送过第三方转换器可能会泄露运营细节。把工作放在客户端可以让那些信息留在它该在的地方:你的机器上。对于像在秒与 ISO 8601 之间切换这样的日常任务,默认的隐私级别应当是:不传输、不记录、回路中没有第三方。
常见问题
什么是 ISO 8601 格式?
ISO 8601 是表示日期和时间的国际标准。它看起来像 2026-04-07T14:30:00Z,其中 T 分隔日期和时间,Z 表示 UTC。它不因区域不同而产生歧义。
为什么 API 使用 Unix 时间戳而不是可读日期?
Unix 时间戳是单个数字,易于存储、排序和比较。它与时区无关(始终是 UTC),占用的空间比格式化日期字符串少。代价是对人类不可读。
时间戳末尾的 Z 是什么意思?
Z 代表「Zulu 时间」,是 UTC(协调世界时)的另一种说法。以 Z 结尾的时间戳是 UTC,而非本地时间。
如何将 24 小时制转换为 12 小时制?
对于 1–12 的小时,小时保持不变(0–11 添加 AM,12 添加 PM)。对于 13–23,减去 12 并添加 PM。00:00 变为 12:00 AM(午夜)。12:00 变为 12:00 PM(中午)。
What is the Year 2038 problem?
32-bit signed Unix timestamps overflow on 19 January 2038 at 03:14:07 UTC, after which they wrap around to negative values that look like dates in 1901. Most modern systems use 64-bit timestamps and are unaffected, but legacy embedded devices, old databases, and 32-bit time_t in some C code still need to be audited.
How are leap seconds handled in Unix time?
Unix time pretends leap seconds do not exist. The clock simply repeats or stretches the affected second, so a Unix timestamp is not a strict count of elapsed SI seconds. For most applications this is fine, but precision-timing code (astronomy, GPS, high-frequency trading) needs TAI or UTC with explicit leap-second handling.