
1. 为什么几乎每个程序里都离不开时间函数做开发这些年我见过太多新手在时间处理上翻车。有人写日志发现时间全差8小时有人做定时任务发现程序在跨天时莫名其妙跳过执行还有人用错格式化符号直接把年份写错。归根结底都是在最开始接触时间函数时没有把time、localtime、strftime这套组合吃透。这三个函数是编程语言里最常见的时间处理三件套几乎覆盖了日常开发中90%的时间需求获取当前时间戳、转换成本地时间结构、格式化输出成人类可读的字符串。不管你是写Python、C、PHP还是Shell脚本这套思路都大同小异理解了底层逻辑换任何语言都能快速上手。这篇文章不打算讲枯燥的API文档我会结合自己实际写代码踩过的坑把这三个函数的原理、配合方式、时区问题、格式化套路一次讲清楚。适合刚开始接触时间处理的初学者也适合那些已经写过几个脚本但总在时间上出bug的开发者。看完你能搞清楚时间戳和字符串之间到底是怎么转换的以及为什么不同语言里这些函数长得几乎一样。2. 先搞清楚三个函数各自管哪一段2.1 time拿到的是“绝对时刻”而不是“日期”time()函数在所有主流语言里语义几乎一致返回的是自协调世界时1970年1月1日0点0分0秒起经过的秒数浮点或整数视语言而定。这个数字本身没有任何时区概念是全宇宙统一的一个坐标点。我习惯把它理解成“拍照片的底片”。底片记录的是那一刻全部的光线信息但你最终打印成照片时可能调色、裁切那都是后面的事。时间戳就是这个底片它记录的是一个绝对瞬间和你在东京还是纽约没有任何关系。import time print(time.time()) # 输出类似 1736328000.123456这个数字对普通用户没有意义但它是所有时间运算的基础。比如你要计算一段代码跑了多久就是end - start你要判断一个订单是否超时就是now - create_time 1800。这些操作都基于“时间戳是单调递增的数值”这一特性根本不需要关心日期格式。2.2 localtime把“绝对时刻”翻译成“本地日历”time()得到的是一个单调数值但人类习惯看“2025年1月8日 14:30:00”这种日历表示。localtime()干的事情就是把时间戳转换成当前时区下的日期时间结构体。注意关键词当前时区。同样的时间戳在东京显示的时间比在北京快1小时在纽约则比北京晚13小时。这个转换完全取决于操作系统的时区设置程序本身并不携带时区信息。import time now_ts time.time() local_time time.localtime(now_ts) print(local_time) # time.struct_time(tm_year2025, tm_mon1, tm_col8, tm_hour14, ...)返回的结构体包含年、月、日、时、分、秒、星期几、一年中的第几天、是否夏令时等字段。Python里是struct_timeC语言里是struct tmPHP里是关联数组但字段含义都是一一对应的。这里有个特别容易踩的坑如果你在代码里写死了tm_hour 8来“修正时区”那就是给自己埋雷。因为如果系统时区本来就是东八区localtime()返回的就是北京时间你再加8小时就错了如果系统时区是UTC你加8倒是对了但等到夏令时切换你又得换算法。正确做法是永远不要手动偏移时区让系统去处理。2.3 strftime把结构体“格式化”成你想要的样子strftimestring format time接收一个时间结构体和一个格式模板返回按照模板生成的字符串。比如%Y-%m-%d %H:%M:%S会得到2025-01-08 14:30:00%Y%m%d会得到20250108。格式符看着多实际常用的就这几个。格式符含义示例2025-01-08 14:30:00%Y四位年份2025%m两位月份01%d两位日期08%H24小时制小时14%M分钟30%S秒00%j一年中的第几天008%w星期几0为周日3周三%A完整的星期名称Wednesday我自己的经验是日志文件命名用%Y-%m-%d数据库里存时间戳只有在返给前端展示时才用strftime转成人话。这个分工配合能避免很多麻烦。3. 三者的协作流程一张完整的转换链路3.1 正向链路时间戳 → 结构体 → 字符串绝大多数业务的输出逻辑都是这条正向链路。先获取当前或某个时间点的时间戳转成结构体再格式化成字符串显示或写日志。import time def log_now(): ts time.time() st time.localtime(ts) readable time.strftime(%Y-%m-%d %H:%M:%S, st) return readable print(log_now()) # 2025-01-08 14:30:00这一套组合拳里最容易忽略的一个细节是strftime的第二个参数必须是time.struct_time不能直接传时间戳。很多新手直接time.strftime(%Y-%m-%d, time.time())必然报错。如果你只想从时间戳直接格式化Python里有现成的time.strftime(%Y-%m-%d, time.localtime(ts))但本质上仍然是先转换结构体。3.2 反向链路字符串 → 结构体 → 时间戳有时候用户输入了一个日期字符串比如前端传过来2025-01-08你需要把它存进数据库或者参与时间比较这时需要反向转换。import time date_str 2025-01-08 14:30:00 st time.strptime(date_str, %Y-%m-%d %H:%M:%S) ts time.mktime(st) print(ts) # 输出对应的时间戳strptime就是strftime的逆操作按模板解析字符串得到结构体。唯一的坑是如果要支持中文环境月份和星期的解析可能在某些系统上有兼容问题我建议在解析用户输入时尽量只接收数字格式不要接收中文月份名。3.3 为什么数据库里存时间戳比存字符串更香这是我在实际项目中反复体会出来的一条经验。时间戳是一个数值可以比较大小、计算差值、排序字符串则要经过解析才能参与运算而且格式不统一时排序结果完全错误。举例说明2025-1-8和2025-01-08在字典序下排序结果是不同的但如果你把两个日期都转成时间戳再比较100%准确。所以我现在设计表结构时如果字段代表“某个时刻”一律存整数型时间戳只有展示层才转成字符串。4. 时区问题你写的日志为什么总是差8小时4.1 localtime 与 UTC 的关系一句话说透localtime()本质上是“UTC时间 时区偏移”。系统时区如果是东八区它返回的就是UTC8的日历时间系统时区如果是UTC0返回的就是UTC时间本身。这个机制带来的最大陷阱是同一个时间戳在不同时区的机器上跑同一个程序输出完全不一样。我自己就遇到过开发机在北京测试服务器在香港结果两边打印日志里的时间对不上查了半天才发现是时区配置不一致。后来我给所有服务器统一设置时区才彻底解决这个问题。4.2 跨时区业务怎么处理才稳妥如果你的业务涉及多个时区的用户比如线上会议系统要显示每个参会者本地时间正确做法是数据库里统一存UTC时间戳。前端获取自己所在时区的偏移量。展示时用本地时区规则把时间戳格式化成字符串。不要试图在服务器端“猜”用户时区猜不到的。也不要为了图省事把时间全转成东八区字符串存库这样其他时区的用户看到的时间全是错的。用我在3.3节说的时间戳存储方案这个问题能轻松绕开。对于Python 3.9更好的做法是用内置的zoneinfo模块显式指定时区而不是依赖系统时区。from datetime import datetime from zoneinfo import ZoneInfo dt datetime.now(ZoneInfo(Asia/Shanghai)) print(dt.strftime(%Y-%m-%d %H:%M:%S))当然这段代码用的是datetime而不是time但思路一脉相承显式优于隐式不要让程序行为取决于运行机器的系统设置。5. 格式化实战那几种最容易用错的写法5.1 大小写字母的含义天差地别%Y是全年的四个数字%y是年份后两位%m是数字月份%b是缩写月份名Jan、Feb%B是完整月份名January%H是24小时制%I是12小时制%p是AM/PM。一个大小写之差结果可能让你排查半天。我整理过一个速查表现在仍然贴在工位旁边。格式符含义示例%Y/%y四位/两位年份2025 / 25%m/%b/%B数字月份/缩写/全称01 / Jan / January%H/%I/%p24小时/12小时/上下午14 / 02 / PM%d/%e带前导零/前置空格的日期08 / 85.2 日志文件按天切分的最稳写法很多人用%Y-%m-%d切分日志文件但这里有个隐患如果程序跨过午夜运行比如零点整还在处理上一天的数据按当前时间切分日志会导致上一天的数据出现在今天的文件里。我自己写的日志模块里生成文件名时允许传入一个“归属时间”参数默认用当前时间但业务侧如果处理的是昨天的数据可以显式传入昨天的结构体。这个功能拯救过我不止一次。import time def get_log_filename(tsNone): st time.localtime(ts if ts else time.time()) return time.strftime(app-%Y-%m-%d.log, st) # 处理昨天的数据时 print(get_log_filename(time.time() - 86400))5.3 性能问题频繁格式化开销大不大strftime本身开销不大但在高并发日志场景下每行日志都调用一次time.time()localtime()strftime()积累起来也可观。我在一个服务里做过粗略压测单线程每秒几十万次格式化会让CPU明显上升但如果只是每秒打印几百条日志完全不用担心。优化思路也很简单如果日志的“秒”级别精度够用可以缓存上一个格式化结果的秒数下一秒不变就直接复用字符串或者把日志异步写入队列格式化放在写日志的线程里不阻塞业务主流程。对绝大多数业务来说不改也完全够用我只是给那些真正遇到性能瓶颈的人一个方向。6. 常见问题速查遇到这些报错别慌问题症状原因解法TypeError传入时间戳strftime报参数类型不对第二个参数需要结构体而非时间戳localtime()转一下再传日志时间差8小时时间比当前北京时间少8小时系统时区是UTCtimedatectl set-timezone Asia/Shanghai或代码中显式处理ValueError: Invalid format string格式化模板里写错格式符%后跟了不存在的字符对照速查表检查格式符星期几对不上%w输出0~6但预期1~7不同语言里0代表周日或周一有差异查具体语言的文档别凭感觉跨天日志进错文件零点前后日志归属错乱使用当前时间生成文件名但数据属于昨天日志模块支持传入归属时间结构体locale中文名乱码%A输出中文乱码系统locale未安装或未设置设置LC_TIMEzh_CN.UTF-8或直接不用文字星期32位系统上的2038问题时间戳溢出用32位整数存时间戳用64位整数或改用高精度时间表示这里我最想强调的是“2038年问题”。很多老系统用32位有符号整数存时间戳最多撑到2038年1月19日。虽然看起来还很远但金融、嵌入式这类长生命周期系统现在不注意将来迁移成本极高。我在设计新模块时一律要求用64位类型存储时间戳这属于花几分钟就能规避的长期风险。7. 一个真实场景的完整实现从零写一个“上次运行时间”记录器前面几节都是知识点这一节我们来个完整的小项目程序每次启动时读取上次运行时间打印出来然后更新记录。这个功能常用于定时任务检查是否漏跑、异常退出后的续跑判断。需求拆解需要有一个持久化文件保存上次运行时间戳。程序启动时读取文件时间戳转成结构体格式化成易读字符串。用当前时间戳覆盖文件内容。import time import os STATE_FILE last_run.ts def read_last_run(): if not os.path.exists(STATE_FILE): return None with open(STATE_FILE, r) as f: content f.read().strip() if not content: return None try: return int(content) except ValueError: return None def write_last_run(ts): with open(STATE_FILE, w) as f: f.write(str(int(ts))) def format_ts(ts): return time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(ts)) if __name__ __main__: last_ts read_last_run() current_ts time.time() if last_ts is None: print(首次运行没有历史记录) else: interval current_ts - last_ts print(f上次运行时间{format_ts(last_ts)}) print(f距上次运行已过{interval:.2f} 秒) if interval 86400: print(警告距离上次运行超过24小时可能漏跑了任务) write_last_run(current_ts) print(f本次运行时间已记录{format_ts(current_ts)})这个脚本里涉及到的核心操作就是time.time()、time.localtime()、time.strftime()的组合使用。你可以在任何服务器上跑一下看看输出是否符合预期。踩过的坑说明一下文件读写要处理内容被截断或写坏的情况所以我做了try/except兜底读不了就当成没有历史记录不阻塞主流程。定时任务场景下这种“宁可放过不可卡死”的容错思路很重要否则程序异常退出可能直接导致后续任务全部失败。把这个脚本扩展成“按小时检查任务是否执行”的守护进程也非常简单循环里对比time.time()和预期执行点到时间就触发任务。这是time函数在实际项目中相当经典的一种用法。8. 不同语言里的同款三件套怎么对应虽然称呼不同但几乎所有主流语言都把这套逻辑带了过去。我写了个对照表方便大家参考。语言获取时间戳转本地结构体格式化输出Pythontime.time()time.localtime()time.strftime()Ctime(NULL)localtime()strftime()PHPtime()getdate()date()JavaScriptDate.now()getFullYear()等方法toLocaleString()Shelldate %sdatedate %Y-%m-%dGotime.Now().Unix()time.Now()time.Now().Format()有意思的是C语言里的strftime和 Python 里的strftime格式符规范几乎完全一样这是历史沿革的结果。你只要吃透一套迁移到其他语言是很快的。JavaScript是个例外它没有直接暴露结构体而是用Date对象提供各种 getter 方法但从底层逻辑上看仍然是“时间戳 → 时间对象 → 格式化字符串”的链路。所以理解概念模型比死记API更重要。9. 我踩过的三个真坑希望你一次都不用踩9.1 用%s在strftime里输出时间戳结果在不同平台表现不一有些语言和平台支持%s作为“输出原始时间戳”的格式符但这不是C标准定义的。我在一个脚本里用过%s来生成Unix时间戳换了一台服务器就失效了。后来所有“获取时间戳”的操作我都改用专门的时间戳函数而不是靠格式化去推。9.2 时间戳的精度整秒还是毫秒time.time()默认返回浮点数小数部分在不同精度下表现不同。如果你拿它做跨进程的唯一ID可能会碰撞但做时间差计算绰绰有余。如果业务需要毫秒精度Python里可以乘1000转整但要注意浮点数精度可能导致末尾差1毫秒的问题。我自己的做法是需要毫秒时间戳时用int(time.time() * 1000)这个在绝大多数平台上都能满足需求。更高精度则建议直接用专门的高精度时钟接口而不是用时间戳推算。9.3 不要把localtime存下来的结构体当成“可持久化数据”到处传我在一个项目里为了省事把struct_time对象直接通过序列化存到数据库本意是省掉重复转换。结果换语言读取时发现字段完全对不上只能重构。结构体是进程内的中间表示出了进程边界就走样了该存时间戳就存时间戳。这是我吃过亏之后总结的最重要的一条纪律进程内用结构体存储和传输用时间戳展示用格式化字符串。三者各司其职代码的生命力会强很多。关于这套时间函数的组合拳我目前能分享的就这些。时间处理看起来简单但凡是涉及时区、格式化、持久化的项目都能在这类细节上折腾人。如果你正准备写日志系统、定时任务或者任何和“时间”有关的模块把time、localtime、strftime这三个函数的协作链路先理清楚后面写起来顺很多。