恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
时间转换与Unix时间戳完全指南:时区、格式及多语言实现
首页
资讯中心
/
时间转换与Unix时间戳完全指南:时区、格式及多语言实现
时间转换与Unix时间戳完全指南:时区、格式及多语言实现
发布时间:2026/10/6 14:23:06
上周帮朋友做数据迁移对方丢来一个 JSON 文件20 万条记录的 create_time 全是 1704067200 这种裸数字。我问了一句秒还是毫秒UTC 还是本地对面愣了半天不就是时间吗转成 2024-01-01 08:00:00 不就行了。这句话我以前也说过直到被时区坑过、被毫秒时间戳坑过才明白“时间转换”这四个字难点从来不在转换本身而在你知不知道自己在转什么。今天这篇就围绕最常见的“年-月-日 时-分-秒”格式把时间转换这件事讲透。适合所有跟数据打交道的人后端、数据分析、运维、写脚本的、甚至用 Excel 做报表的都能直接拿代码去用。1. 一个“标准格式”的诞生为什么到处是 2025-01-19 14:33:02“2024-01-01 08:00:00”这个写法在国内开发语境里几乎是事实标准MySQL 默认的 datetime 输出长这样日志默认长这样接口文档里也常长这样。它和 ISO 8601 非常接近只是把中间的字母 T 换成了空格。为什么它能成为标准总结下来是三个原因。第一个是定长。两位数月份、两位数日期、两位数小时全部补零所以这个字符串永远不会出现“2024-1-5 8:00”这种长度不一的情况。定长直接带来第二个好处字典序等于时间序。你把“2024-01-01 08:00:00”和“2024-01-01 09:00:00”按字符串比大小得到的结果和真实时间先后完全一致。这在日志分析里非常重要——有时候 grep 一把之后直接 sort字符串顺序就是时间顺序根本不用先把字段解析成时间对象。第三个是零歧义。“2024-01-19”在任何文化里都很难被理解成别的组合月在前还是日在前的问题天然不存在。相比之下“05/01/2024”在美国人眼里是 1 月 5 日在欧洲人眼里是 5 月 1 日“2024.1.19”这种又没法直接排序。所以跨系统交接数据时写“年-月-日 时-分-秒”是最不容易出争议的沟通方式。当然这个格式也有一些变体需要心里有数。比如文件系统里不允许冒号时就会把时间写成“2024-01-19_14-33-02”数据传输紧凑一点会写成“20240119143302”带时区时标准做法是“2024-01-19T14:33:0208:00”。这些本质上都是同一个东西只是换了壳。但说到底“年-月-日 时-分-秒”只是给人看的字符串。机器真正认的是数字于是就有了时间戳程序内部操作的是时间对象于是又有了 DateTime、Date 这些类型。所谓时间转换本质上就是这三态互转可读字符串、时间戳数字、时间对象。下面这张图可以不用记但心里要有正向是“数字 → 对象 → 字符串”反向是“字符串 → 对象 → 数字”时区在每一步都可能插手。2. 先把时间翻译成数字Unix 时间戳到底怎么算出来的要玩转转换先搞清楚时间戳的底细。Unix 时间戳的定义一句话从 1970-01-01 00:00:00 UTC 起经过的秒数。1970-01-01 00:00:00 UTC 那一刻时间戳就是 0。之后的每一秒加一之前的每一秒减一所以 1969-12-31 23:59:59 UTC 的时间戳是 -1。这里说的“经过的秒数”其实是理想化的秒遇到闰秒时绝大多数系统直接忽略日常处理不用管但要知道它不是朴素物理意义下的“真实经过时间”。拿 1704067200 这个数字来拆解一下你就明白它和“2024-01-01”的关系了。1704067200 ÷ 86400 19723刚好整除说明这一天是某个 UTC 日的零点。19723 天是什么概念1970 年到 2024 年相隔 54 年按每年 365 天算一共 19710 天这 54 年里有 13 个闰年加上去正好是 19723 天。所以 1704067200 就是 1970-01-01 起的第 19723 个整天的零点——也就是 2024-01-01 00:00:00 UTC。注意这里用的是 UTC。如果你在北京这个时间戳对应的本地时间是 2024-01-01 08:00:00因为北京时间是 UTC8本地时间 UTC 时间 8 小时。看到没同一个时间戳在不同时区显示出来的“年-月-日 时-分-秒”字符串完全不一样。所以时间转换里最容易被忽略的就是这条先问清楚目标时区。关于时间戳还有两个数字要刻在脑子里。第一当前 Unix 时间戳大概在 17 亿这个量级2024 年附近13 位的那是毫秒不是秒10 位才是秒。13 位转 10 位就是除以 1000但多少人栽在这上面后面细说。第二32 位有符号整数的上限是 2147483647对应 2038-01-19 03:14:07 UTC这就是著名的 2038 年问题。现在新系统基本都是 64 位存储理论上撑到遥远未来但老代码、老表结构里如果还用 int 存时间戳迟早要出大事。时间时间戳说明1970-01-01 00:00:00 UTC0Unix epoch 原点2024-01-01 00:00:00 UTC1704067200上文拆解的例子2024-01-01 08:00:00 UTC81704038400北京时间的同一天零点2038-01-19 03:14:07 UTC214748364732 位有符号整数上限知道了这些再看各种语言的时间转换函数本质都只是一层包装它帮你把时间戳按某个时区拆成年月日时分秒或者反过来把年月日时分秒拼回时间戳。你自己会拆了就不会被函数默认行为带偏。3. 正向转换实操时间戳变“年-月-日 时-分-秒”的五个环境正向转换 把时间戳数字变成“年-月-日 时-分-秒”字符串。下面五个环境我挨个过一遍都是可以直接抄的写法。3.1 Python标准库就够了from datetime import datetime ts 1704067200 # 默认使用系统本地时区 print(datetime.fromtimestamp(ts).strftime(%Y-%m-%d %H:%M:%S)) # 系统时区为 UTC 时2024-01-01 00:00:00 # 系统时区为 Asia/Shanghai 时2024-01-01 08:00:00如果想把结果固定成某个时区不要依赖服务器设置显式指定最稳from datetime import datetime, timezone, timedelta ts 1704067200 # 固定北京时区 dt datetime.fromtimestamp(ts, tztimezone(timedelta(hours8))) print(dt.strftime(%Y-%m-%d %H:%M:%S)) # 2024-01-01 08:00:00Python 3.9 以上还可以用 zoneinfo 按 IANA 时区名写更不容易错ZoneInfo(Asia/Shanghai)。另外一个小技巧格式化也可以写成 f-stringf{dt:%Y-%m-%d %H:%M:%S}效果一样代码更短。批量场景用 pandas后面专门说。3.2 JavaScriptDate 对象加手工补零JavaScript 的时间戳单位是毫秒所以第一步永远是乘 1000// ts 单位是秒 const ts 1704067200; const d new Date(ts * 1000); // Date 内部是毫秒 const pad (n) String(n).padStart(2, 0); const result ${d.getFullYear()}-${pad(d.getMonth() 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}; console.log(result); // 输出取决于当前时区两个新手必踩点getMonth() 返回的是 0~11所以要 1getDate()、getHours() 这些返回 1~9 时不会自动补零所以要 padStart。想要省事可以引入 dayjsdayjs(ts * 1000).format(YYYY-MM-DD HH:mm:ss)一行完事。moment.js 当年也是这么写但它已经进入维护模式新项目我更推荐 dayjs 或 Luxon。3.3 Excel一个公式搞定Excel 的日期本质是“距离 1900-01-01 的天数”1970-01-01 在 Excel 里对应序列值 25569。所以秒转日期时间TEXT(A2/86400 25569 8/24, yyyy-mm-dd hh:mm:ss)其中 A2 放时间戳秒数。A2/86400 是把秒换算成天数25569 是 1970-01-01 的序列值8/24 是把 UTC 时间调成北京时间。如果你要的是 UTC 本身去掉 8/24 就行。这里顺便说个冷知识Excel 把 1900-02-29 当成有效日期其实 1900 年不是闰年所以 1970-01-01 的序列值比“严格天数”大了一天正好是 25569。这个常数已经被全世界公式用了几十年不用纠结对错记住 25569 就行。3.4 SQL数据库自带函数MySQL 直接一行SELECT FROM_UNIXTIME(1704067200, %Y-%m-%d %H:%i:%s);FROM_UNIXTIME 的第二个参数是输出模板%H 是 24 小时制%h 是 12 小时制%i 才是分钟——这个等下还会吐槽。PostgreSQL 对应的写法SELECT to_char(to_timestamp(1704067200), YYYY-MM-DD HH24:MI:SS);注意 to_timestamp 返回的是 timestamptzto_char 输出的是当前会话时区下的字符串。会话时区变了输出也跟着变。3.5 Shell一个 date 命令走天下# Linux 写法 date -d 1704067200 %Y-%m-%d %H:%M:%S # 默认本地时区 TZAsia/Shanghai date -d 1704067200 %Y-%m-%d %H:%M:%S # 固定时区当前时间也是同一个套路date %Y-%m-%d %H:%M:%S。对了macOS 上 date -d 语法不一样要写成date -r 1704067200临时用的时候别对着同一个命令怀疑半天。4. 反向转换实操把“可读串”解析回时间对象反向转换 把“2024-01-01 08:00:00”这种字符串解析回时间对象或时间戳。这个方向坑比正向多因为“解析”本身就有歧义字符串里没有时区信息那一堆年月日时到底代表哪个时区完全依赖实现者的默认值。4.1 Pythonstrptime 与 timezone 的暗坑from datetime import datetime s 2024-01-01 08:00:00 dt datetime.strptime(s, %Y-%m-%d %H:%M:%S) print(dt.timestamp()) # 返回的秒数取决于本机时区这行代码容易让人犯迷糊strptime 解析出来的 dt 是 naive 的没有时区信息当你调用 .timestamp() 时Python 会按照本机时区把它当成本地时间再换算成 UTC 秒数。如果本机是 UTC“2024-01-01 08:00:00”被当成 UTC 时间时间戳是 1704067200 28800 1704096000如果本机是 Asia/Shanghai则返回 1704038400。同一个字符串两个结果差 8 小时。想固定解析为北京时间就要给 dt 挂上时区from datetime import datetime, timezone, timedelta s 2024-01-01 08:00:00 dt datetime.strptime(s, %Y-%m-%d %H:%M:%S).replace(tzinfotimezone(timedelta(hours8))) print(dt.timestamp()) # 1704038400.0这才是“北京时间 2024-01-01 08:00:00”对应的真实时间戳还有一个常见问题Python 3.11 以前datetime.fromisoformat() 只认带 T 分隔符的 ISO 字符串不能直接解析“2024-01-01 08:00:00”要先把空格替换成 T 再调用或者干脆用 strptime。别问我怎么知道的。4.2 JavaScript浏览器差异与安全解析新版 ECMAScript 允许 new Date() 解析 ISO 字符串但对“空格分隔”的格式没有任何统一要求。实测下来老版 Safari 对 new Date(2024-01-01 08:00:00) 直接返回 Invalid Date就算能解析不同版本对它是本地时间还是 UTC 的理解也不完全一致。建议别赌手动解析const s 2024-01-01 08:00:00; const m s.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/); if (!m) throw new Error(格式不对: s); // 注意月份要 -1 const d new Date(m[1], m[2] - 1, m[3], m[4], m[5], m[6]); const ts Math.floor(d.getTime() / 1000); // 按本地时区解释后的 UTC 秒数如果字符串带时区后缀比如“2024-01-01T08:00:0008:00”直接 new Date(s) 是安全的所有主流引擎都认。4.3 Excel 与 SQL 的反向转换Excel 里日期时间转秒数(B2 - 25569) * 86400B2 是日期时间单元格。同样这里得到的是“该显示值所对应的秒数”。如果你的 Excel 显示的是北京时间想拿到真正的 UTC 时间戳要再减 28800否则你只是把“2024-01-01 08:00:00”当成 UTC 去算秒数结果照样差 8 小时。MySQL 反向SELECT UNIX_TIMESTAMP(2024-01-01 08:00:00);注意 MySQL 的 UNIX_TIMESTAMP 会把字符串丢给会话时区去解释所以先检查 session time_zone 再下结论。PostgreSQL 用SELECT extract(epoch from 2024-01-01 08:00:0008:00::timestamptz);这里我建议永远显式带时区后缀否则行为同样依赖会话设置。5. 时间转换最容易翻车的六个细节到了喜闻乐见的环节。下面六个坑我一个不落地踩过写出来给你省时间。5.1 十位秒和十三位毫秒混用最经典的翻车没有之一。有的接口给的是秒 1704067200有的是毫秒 1704067200000前端拿到后直接 new Date(1704067200000 * 1000)多乘一个 1000日期瞬间变成一个夸张的数字。反过来把 1704067200000 当秒传给 date 命令输出的年份能到五位数。凡是时间戳交接第一句话必须是“你这个单位是秒还是毫秒”。我见过多个团队因为这个问题线上出事故真事。5.2 时区是那消失的八小时Python 的 fromtimestamp、JS 的 getHours()、Excel 的显示值、MySQL 的 FROM_UNIXTIME默认全跟“当前时区”走。当前时区是什么取决于你的服务器配置、浏览器的系统设置、Excel 的本机区域设置、数据库会话变量——任何一环不一样同一个时间戳就能输出四个不一样的字符串。我的习惯存储一律用 UTC展示层才转本地时区跨系统交接字符串必须带时区后缀比如 08:00。宁可让字符串难看一点也不要让时间暧昧。提示在团队接口文档里明确写一条“所有时间字段默认 UTC除另有说明外”能挡掉绝大多数时区纠纷。这是成本最低的一行“文档代码”。5.3 JavaScript 月份从零开始new Date(2024, 1, 1) 是 2 月 1 日不是 1 月 1 日getMonth() 返回 0 代表一月。这个设计坑了无数人特别是从 Python datetime 切到 JS 的时候。补零脚本里忘了 1或者反过来多 1出来都差一个月。建议在代码注释里写明“month - 1”并写个单元测试覆盖一月和十二月两个边界。5.4 格式符的大小写太容易混时间格式化用的字母在各语言里并不统一。Python 里 %M 是分钟、%m 是月份、%H 是 24 小时制、%I 是 12 小时制MySQL 里 %i 才是分钟、%m 是月份Excel 里“mm”紧跟冒号时是分钟紧跟横杠时是月份全靠位置猜Java 和 Moment 里更狠大写的 YYYY 表示“基于 ISO 周的年份”小写 yyyy 才是自然年。2021-2022 跨年那阵子不少系统因为把 yyyy 写成 YYYY导致 1 月 1 日前后几天的日期整体错了一年——2022-01-01 用 YYYY 格式化会得到 2021-01-01。所以复制粘贴格式串之前先拿一个已知日期跑一遍验证。5.5 非法日期的处理逻辑千差万别“2024-02-30”这种日期不同环境的反应完全不同Python strptime 直接抛 ValueErrorMySQL STR_TO_DATE 返回 NULL 并给一条 warningExcel 日期公式会把它归一到 3 月 1 日前后的序列值JS 的 new Date(2024, 1, 30) 会悄悄进位到 3 月 1 日。这意味着你最好在上层解析时就截断报错别指望底层帮你拦。数据清洗脚本里先用正则粗校验、再丢给解析函数并对 NULL、Invalid 结果做汇总统计这是我能给出的最稳妥方案。5.6 边界值1970 之前、2038、9999负时间戳不是天方夜谭很多业务系统里有 1969 年的档案数据时间戳是 -1 这种负数。别用“大于 0”过滤时间字段。数据库方面MySQL 的 datetime 范围是 1000-01-01 到 9999-12-31而 timestamp 类型到 2038 年就是极限如果你在设计新表别用 timestamp 存远期排期。查询时还有个高频边界错误查“1 月整月”写成 create_time 2024-01-31这会把 1 月 31 日全天的记录漏掉。正确写法是半开区间create_time 2024-01-01 AND create_time 2024-02-01。6. 批量转换的上岸姿势写给数据导出和日志处理单条转换练熟了真实场景基本是百万行日志、几十 GB 的导出文件。这时候有几条工程经验很值钱。第一优先用向量化工具而不是 for 循环。Python 里强烈建议 pandasimport pandas as pd df pd.read_csv(export.csv) # 时间戳是秒 df[dt] pd.to_datetime(df[ts], units, utcTrue).dt.tz_convert(Asia/Shanghai) df[fmt] df[dt].dt.strftime(%Y-%m-%d %H:%M:%S)如果源数据是字符串在 to_datetime 里显式传 format 参数性能差距能到十倍以上pd.to_datetime(df[s], format%Y-%m-%d %H:%M:%S)。pandas 拿到明确的格式才能走 C 层快速路径靠猜格式时只能老实一个个试。第二流式处理大日志别把几百 MB 一口气读进内存。Linux 下用 gawk 可以一边扫一边转gawk { print strftime(%Y-%m-%d %H:%M:%S, $1), substr($0, index($0, )1) } app.log app_fmt.log第三交接文档比代码更重要。我给团队定过一份“时间字段交接清单”任何接口、任何表只要涉及时间必须写清楚六件事项必须写清示例单位秒 / 毫秒 / 微秒秒基点Unix epoch1970-01-01 00:00:00 UTC时区UTC / 本地 / 偏移Asia/Shanghai (UTC8)格式分隔符、T 还是空格、是否补零yyyy-MM-dd HH:mm:ss精度整数秒 / 小数秒毫秒精度异常值空值、非法日期怎么处理置 NULL有了这张表时间转换的九成问题能提前消灭在需求评审阶段而不是等数据跑完才发现差了八个小时。最后再说个个人习惯我在任何做时间转换的脚本里都会把 0、-1、1704067200、2147483647 这组数写成测试用例先跑一遍比任何口头约定都可靠。日期时间这东西错一次不是差一分钟是差八年、差一天、差一个世纪排查起来血压直接拉满。你提前花五分钟写这四行断言省下的绝对是五小时起步。