从 UTC 日期时间字符串获取 Unix 时间戳:C 和 C++ 中的挑战与解决方案

在编程世界里,从 UTC 日期时间字符串获取 Unix 时间戳,看似简单,实则暗藏玄机。你以为输入一个像 “Fri, 17 Jan 2025 06:07:07” 这样的 UTC 时间,然后轻松得到 1737094027(从 1970 年 1 月 1 日 00:00:00 UTC 开始经过的秒数)就万事大吉了?事实可没这么简单,这背后涉及到一系列复杂的时间处理问题,还会让你发现 POSIX 时间处理函数在不同 C 库及相关语言中的各种 “意外特性”。今天,咱们就来深入探讨一下这个让人又爱又恨的话题。

一、时间处理的复杂性

时间本身就是一个复杂的概念,要是再把闰秒和相对论这些因素考虑进去,那就更让人头疼了。而人类对时间的记录方式,从模糊不清到精确无比,各不相同。就拿阿姆斯特丹的时间来说,由于夏令时的存在,“2025 年 3 月 30 日 02:20” 这个时间点在当地是不存在的,时间会直接从 01:59:59 跳到 03:00:00。但 “2024 年 10 月 27 日 02:30” 就更让人困惑了,因为夏令时结束时,02:59:59 的下一秒又回到了 02:00:00,这就导致有两个 “02:00” 的时间点。从下面的命令行示例就能看出工具在处理这种情况时的随意性:

$ TZ=Europe/Amsterdam date -d '20241027 01:59:59' +"%Y-%m-%d %H:%M:%S %s %z"
2024-10-27 01:59:59 1729987199 +0200
$ TZ=Europe/Amsterdam date -d '20241027 02:00:00' +"%Y-%m-%d %H:%M:%S %s %z"
2024-10-27 02:00:00 1729990800 +0100

你看,当要求解释 02:00:00 这个时间时,GNU date 工具选择了第二个出现的时间点。而且据我观察,这还和执行命令的时间有关,如果在四月执行,可能就会选择第一个 02:00:00 实例,是不是很让人摸不着头脑?

二、POSIX 时间概念与 struct tm

在 POSIX/Unix 系统中,指定时间的有效方式是用相对于某个 “纪元”(epoch)的秒数。POSIX/Unix 的纪元是 1970 年 1 月 1 日 00:00:00 UTC,GPS 的纪元是 1980 年 1 月 6 日 00:00:00 UTC,伽利略(欧盟的 GPS 系统)的纪元是 1999 年 8 月 21 日 23:59:47 UTC,北斗系统的纪元是 2006 年 1 月 1 日 00:00:00 UTC。其中,GPS、伽利略和北斗系统都明智地忽略了闰秒,把这些麻烦事留给人类去处理。而我们常用的 POSIX/Unix 的 “time_t” 时间戳,除了在闰秒期间可能会有歧义(不过闰秒以后可能也不会再有了),其他时候还是很可靠的。

为了在时间戳和人类可读的时间格式之间进行转换,UNIX 提供了 struct tm 结构体,它包含了年、月、日、时、分、秒等时间信息:

struct tm {int  tm_sec;    /* 秒 [0, 60] */int  tm_min;    /* 分 [0, 59] */int  tm_hour;   /* 时 [0, 23] */int  tm_mday;   /* 一个月中的第几天 [1, 31] */int  tm_mon;    /* 月份 [0, 11] (一月是 0) */int  tm_year;   /* 年份减去 1900 */int  tm_wday;   /* 一周中的第几天 [0, 6] (周日是 0) */int  tm_yday;   /* 一年中的第几天 [0, 365] (1 月 1 日是 0) */int  tm_isdst;  /* 夏令时标志 */long tm_gmtoff; /* 相对于 UTC 的秒数 */const char *tm_zone;   /* 时区缩写 */
};

不过,这个结构体的设计其实有点冗余,像一周中的第几天和一年中的第几天,通过其他字段就能推算出来。而且,tm_gmtofftm_zone 和 tm_isdst 这几个字段的含义不仅定义得不太清晰,理解起来也有难度,并且它们的作用还会根据结构体的使用方式而变化。

struct tm 的一个重要作用是作为 mktime() 函数的输入,mktime() 会把 “根据本地时区拆分的时间” 转换为 Unix 时间戳。但它的功能可不止这一个,它还会对传入的 struct tm 进行标准化处理。比如说,如果你想把当前时间往后调一周,你可能会直接给 time_t 时间戳加上 604800 秒(一周的秒数),但如果这个调整跨越了夏令时的边界,你原本下午 2 点的约会可能就会变成下周下午 1 点或 3 点,这可不是我们想要的结果。而 mktime() 就能帮你处理这种情况,即使你传入像 “3 月 35 日” 这样不合理的日期,它也能帮你修正。不过,使用 mktime() 时也有一些需要注意的地方,下面我们就来详细说说。

三、mktime() 的使用与注意事项

先来看个例子:

struct tm tm = {.tm_hour=14, .tm_mday = 28,
.tm_mon = 2, .tm_year = 2025 - 1900,
.tm_isdst = -1};  // <- 注意这里的 -1
time_t t = mktime(&tm);
cout << "original:         "<< ctime(&t);
tm.tm_mday += 7;
t = mktime(&tm);
cout << "mktime adjusted:  "<< ctime(&t);

在欧洲 / 阿姆斯特丹时区,这段代码的输出结果是:

original:        Fri Mar 28 14:00:00 2025
mktime adjusted: Fri Apr  4 15:00:00 2025

为什么我们的约会时间会偏移一个小时呢?问题就出在 tm.tm_isdst 这个字段上。mktime() 要求你明确指定时间是否处于夏令时,或者让它自己去判断(我们一开始把 tm.tm_isdst 设置为 -1 就是让它自己判断)。当我们第一次调用 mktime() 时,它发现初始时间不在夏令时,就把 tm_isdst 设置为 0。第二次调用时,这个设置没有改变,但新的时间其实是处于夏令时的,所以就出现了时间偏移的情况。解决办法就是在第二次调用 mktime() 之前,把 tm_isdst 重新设置为 -1。

另外,使用 mktime() 处理 UTC 时间时也有个大坑。mktime() 会把传入的时间当作 “本地时间” 来处理,所以如果你要处理 UTC 时间,就需要在调用 mktime() 之前把时区设置为 UTC。但如果你的程序有其他线程在运行,修改整个应用程序的时区可能会产生副作用。不过,多线程程序本来就不能随意更改环境变量,所以这个方法也行不通。

还好,有一个非标准但广泛可用的函数 timegm() 能很好地解决 UTC 时间的处理问题。从 IEEE Std 1003.1 - 2024 标准中可知,“未来的标准版本预计会添加一个 timegm() 函数,它与 mktime() 类似,但 timeptr 指向的 tm 结构体包含的是协调世界时(UTC)的拆分时间”。在 Windows 系统上,timegm() 对应的函数是 mkgmtime()。如果你的系统是 AIX,没有 timegm() 函数,也可以在特定的地方找到独立的实现。

总结一下使用 mktime() 和 timegm()(或 mkgmtime())的要点:

  • 使用 mktime() 处理本地时间时,把 tm_isdst 设置为 -1,这通常符合人们的预期,但在夏令时切换时,可能会随机得到两个 “02:30”(或类似时间点)中的一个。

  • 在填充 struct tm 之前,最好先把其他字段清零,以防万一。

  • 要知道 mktime() 会修改传入的 struct tm,可能会产生副作用,所以在重复使用 struct tm 之前,至少要重置 tm_isdst

  • 不管你怎么设置 tm_gmtoffset 或 tm_zonemktime() 都会使用当前时区。如果你想让它把 struct tm 当作 UTC 时间处理,就需要设置 TZ 环境变量为 UTC,但这会影响其他做时间操作的线程。所以,能使用 timegm() 或 mkgmtime() 就尽量用它们。

四、解析 UTC 时间字符串

我们都希望能把像 “Fri, 17 Jan 2025 06:07:07 GMT” 这样的时间字符串直接传入 strptime() 函数,然后得到一个合理的 struct tm 结构体。但 Linux glibc 的 strptime() 手册中关于 %z 和 %Z 这两个时区格式说明符的描述含糊不清,让人摸不着头脑。很多人可能会期望用 strptime() 结合 %Z(用于解析 “GMT”),再配合 mktime() 就能把 UTC 时间字符串转换为 Unix 时间戳,但实际上 mktime() 根本不会看 tm_gmtoffset 和 tm_zone 这两个字段,所以即使 strptime() 做对了,也无法实现我们的目标,而且它还真就做不对。

截至 2024 年,虽然 Open Group 对 strptime() 有了更详细的规范,但里面也都是些让人无奈的消息。strptime() 对 %z 没有明确的行为定义,这个原本应该用来处理 “+0200” 这种偏移标识符的符号,现在根本靠不住。对于 %Z,虽然有一些说明,但作用也非常有限。只有在特定的本地化设置下,它才可能设置 tm_isdst 的正确值,而且像 “EST” 这样的字符串,由于没有明确的定义,也很难通过 %Z 来解析。

不过,既然我们知道了 timegm() 这个好帮手,就可以忽略 %z 和 %Z 了。

五、strptime() 与本地化问题

在很多情况下,我们需要解析包含英文日期和月份名称的时间字符串,并且希望 strptime() 能正确处理。但 IEEE/Open Group 标准规定,strptime() 的转换操作是由当前本地化设置的 LC_TIME 类别决定的。这里有个容易忽略的点,C 和 C++ 程序默认使用的是 “C” 本地化,这实际上就是美式英语。这对于解析数据中的时间字符串来说通常是好事,因为这些字符串大多是英文的。

但如果你的程序调用了 setlocale() 函数,设置了非 “C” 的本地化,那你的程序可能就只能处理特定语言(比如荷兰语)的时间字符串了,这可就麻烦了。你可能会想在调用 strptime() 之前把本地化设置为 “C”,用完再改回来,但 setlocale() 在多线程程序中调用并不安全(除了在线程启动之前),而且即使安全调用,也可能会影响其他线程的输出。

所以,一般来说,如果你需要解析特定的时间字符串并且想用 strptime(),一定要确保程序处于你期望的本地化环境中。虽然有 strftime_l() 函数可以指定格式化时间时使用的本地化,但并没有官方可用的 strptime_l() 函数。

当然,你也可以自己解析像 “17 Jan 2025 06:07:07” 这样的字符串,填充 struct tm 结构体,然后让 mktime() 来计算 Unix 时间戳,这也是一种可行的办法。

六、用 C++ 解决本地化问题及 C++20 的强大时间处理功能

C++ 的输入输出流(iostreams)在处理本地化方面比 C/POSIX 做得更好。在 C++ 中,你可以为每个输入输出流设置本地化。下面是一个 C++ 辅助函数,如果你在设置了本地化的 C 程序中需要解析任意 UTC 时间字符串,可以调用这个函数:

extern "C"
int utcstr2epoch(const char* timestr, const char* fmtstr, struct tm* output)
{std::tm t = {}; // tm_isdst = 0, 不用考虑夏令时,这是 UTC 时间std::istringstream ss(timestr);ss.imbue(std::locale()); // "LANG=C", 但本地化设置是本地的ss >> std::get_time(&t, fmtstr);if (ss.fail())return -1;// 修正星期几、一年中的第几天等字段t.tm_isdst = 0; // 不用考虑夏令时t.tm_wday = -1;if(mktime(&t) == -1 && t.tm_wday == -1) // "真正的错误"return -1;*output = t;return 0;
}

这个函数还展示了如何处理 mktime() 的错误。当 mktime() 处理 1969 年 12 月 31 日 23:59 这样的时间时,会返回 -1 作为错误代码。我们可以用 tm_wday 作为标志来判断是否有数据被处理,以此确定是否发生了错误。

另外,还有一个基于 C 的小示例程序,它可以解析英文的 UTC 时间戳,并使用调用环境的本地化设置来打印时间:

$ LC_TIME="nl_NL.utf-8" ./utcparse "1 Jan 1970 00:00:00" "%d %b %Y %H:%M:%S"
UTC Time: donderdag,  1 januari 1970 00:00:00, day of year 001
time_t:   0

到了 C++20 及更高版本,更是引入了强大的时区数据库。虽然这个功能还没有在所有编译器上都可用,但预标准化版本可以单独使用。比如下面这个超酷的例子:

auto meet_nyc = make_zoned("America/New_York",
date::local_days{Monday[1]/May/2016} + 9h);
auto meet_lon = make_zoned("Europe/London",    meet_nyc);
auto meet_syd = make_zoned("Australia/Sydney", meet_nyc);
cout << "The New York meeting is " << meet_nyc << '\n';
cout << "The London   meeting is " << meet_lon << '\n';
cout << "The Sydney   meeting is " << meet_syd << '\n';

这段代码选择了 “2016 年 5 月的第一个星期一,纽约当地时间上午 9 点”,然后轻松地将其转换为另外两个时区的时间:

The New York meeting is 2016-05-02 09:00:00 EDT
The London   meeting is 2016-05-02 14:00:00 BST
The Sydney   meeting is 2016-05-02 23:00:00 AEST

更厉害的是,这个时区库不仅可以使用操作系统的时区数据库(可能缺少关键的闰秒细节),还能直接从 IANA tzdb 获取数据。这意味着你可以精确计算 1978 年一次航班飞行的实际时长,即使这次飞行跨越了夏令时变化和闰秒,也不在话下。

在 C 和 C++ 中从 UTC 日期时间字符串获取 Unix 时间戳确实充满挑战,但只要掌握了正确的方法,也能轻松应对。希望今天的分享能让你在处理时间相关的编程问题时更加得心应手。

科技脉搏,每日跳动。

与敖行客 Allthinker一起,创造属于开发者的多彩世界。

图片

- 智慧链接 思想协作 -

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/9946.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ESP32-CAM实验集(WebServer)

WebServer 效果图 已连接 web端 platformio.ini ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library stor…

DRF开发避坑指南01

在当今快速发展的Web开发领域&#xff0c;Django REST Framework&#xff08;DRF&#xff09;以其强大的功能和灵活性成为了众多开发者的首选。然而&#xff0c;错误的使用方法不仅会导致项目进度延误&#xff0c;还可能影响性能和安全性。本文将从我个人本身遇到的相关坑来给大…

qt-C++笔记之QLine、QRect、QPainterPath、和自定义QGraphicsPathItem、QGraphicsRectItem的区别

qt-C笔记之QLine、QRect、QPainterPath、和自定义QGraphicsPathItem、QGraphicsRectItem的区别 code review! 参考笔记 1.qt-C笔记之重写QGraphicsItem的paint方法(自定义QGraphicsItem) 文章目录 qt-C笔记之QLine、QRect、QPainterPath、和自定义QGraphicsPathItem、QGraphic…

C动态库的生成与在Python和QT中的调用方法

目录 一、动态库生成 1&#xff09;C语言生成动态库 2&#xff09;c类生成动态库 二、动态库调用 1&#xff09;Python调用DLL 2&#xff09;QT调用DLL 三、存在的一些问题 1&#xff09;python调用封装了类的DLL可能调用不成功 2&#xff09;DLL格式不匹配的问题 四、…

.NET MAUI进行UDP通信(二)

上篇文章有写过一个简单的demo&#xff0c;本次对项目进行进一步的扩展&#xff0c;添加tabbar功能。 1.修改AppShell.xaml文件&#xff0c;如下所示&#xff1a; <?xml version"1.0" encoding"UTF-8" ?> <Shellx:Class"mauiDemo.AppShel…

什么是Maxscript?为什么要学习Maxscript?

MAXScript是Autodesk 3ds Max的内置脚本语言,它是一种与3dsMax对话并使3dsMax执行某些操作的编程语言。它是一种脚本语言,这意味着您不需要编译代码即可运行。通过使用一系列基于文本的命令而不是使用UI操作,您可以完成许多使用UI操作无法完成的任务。 Maxscript是一种专有…

适配器模式

目录 一、概念 1、定义 2、涉及到的角色 二、类适配器 1、类图 2、代码示例 &#xff08;1&#xff09;水饺&#xff08;源角色&#xff09; &#xff08;2&#xff09;烹饪&#xff08;目的角色&#xff09; &#xff08;3&#xff09;食品适配器&#xff08;适配器角…

YOLO11/ultralytics:环境搭建

前言 人工智能物体识别行业应该已经饱和了吧&#xff1f;或许现在并不是一个好的入行时候。 最近看到了各种各样相关的扩展应用&#xff0c;为了理解它&#xff0c;我不得不去尝试了解一下。 我选择了git里非常受欢迎的yolo系列&#xff0c;并尝试了最新版本YOLO11或者叫它ultr…

SQL注入漏洞之绕过[前端 服务端 waf]限制 以及 防御手法 一篇文章给你搞定

目录 绕过手法 前端代码绕过 后端代码绕过 各种字段进行验证 union 大小写绕过 双写逃过 强制类型判断 引号特殊编码处理。 内联注释绕过 注释符绕过 or/and绕过 空格绕过 防御SQL注入的方法 使用预编译语句 使用存储过程 检查数据类型 绕过手法 前端代码绕过…

使用冒泡排序模拟实现qsort函数

1.冒泡排序 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h>int main() {int arr[] { 0,2,5,3,4,8,9,7,6,1 };int sz sizeof(arr) / sizeof(arr[0]);//冒泡排序一共排序 sz-1 趟for (int i 0; i < sz - 1; i){//标志位&#xff0c;如果有序&#xff0c;直接…

【Linux】线程互斥与同步

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;Linux系统编程 这里将会不定期更新有关Linux的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 文章目…

【数据结构】二叉树

二叉树 1. 树型结构&#xff08;了解&#xff09;1.1 概念1.2 概念&#xff08;重要&#xff09;1.3 树的表示形式&#xff08;了解&#xff09;1.4 树的应用 2. 二叉树&#xff08;重点&#xff09;2.1 概念2.2 两种特殊的二叉树2.3 二叉树的性质2.4 二叉树的存储2.5 二叉树的…

1.五子棋对弈python解法——2024年省赛蓝桥杯真题

问题描述 原题传送门&#xff1a;1.五子棋对弈 - 蓝桥云课 "在五子棋的对弈中&#xff0c;友谊的小船说翻就翻&#xff1f;" 不&#xff01;对小蓝和小桥来说&#xff0c;五子棋不仅是棋盘上的较量&#xff0c;更是心与心之间的沟通。这两位挚友秉承着"友谊第…

Origami Agents:AI驱动的销售研究工具,助力B2B销售团队高效增长

在竞争激烈的B2B市场中,销售团队面临着巨大的挑战——如何高效地发现潜在客户并进行精准的外展活动。Origami Agents通过其创新的AI驱动研究工具,正在彻底改变这一过程。本文将深入探讨Origami Agents的产品特性、技术架构及其快速增长背后的成功因素。 一、一句话定位 Ori…

Java---猜数字游戏

本篇文章所实现的是Java经典的猜数字游戏 , 运用简单代码来实现基本功能 目录 一.题目要求 二.游戏准备 三.代码实现 一.题目要求 随机生成一个1-100之间的整数(可以自己设置区间&#xff09;&#xff0c;提示用户猜测&#xff0c;猜大提示"猜大了"&#xff0c;…

NLP深度学习 DAY5:Seq2Seq 模型详解

Seq2Seq&#xff08;Sequence-to-Sequence&#xff09;模型是一种用于处理输入和输出均为序列任务的深度学习模型。它最初被设计用于机器翻译&#xff0c;但后来广泛应用于其他任务&#xff0c;如文本摘要、对话系统、语音识别、问答系统等。 核心思想 Seq2Seq 模型的目标是将…

数据结构 队列

目录 前言 一&#xff0c;队列的基本知识 二&#xff0c;用数组实现队列 三&#xff0c;用链表实现队列 总结 前言 接下来我们将学习队列的知识&#xff0c;这会让我们了解队列的基本概念和基本的功能 一&#xff0c;队列的基本知识 (Queue) 我们先来研究队列的ADT&#xff0c…

Git 版本控制:基础介绍与常用操作

目录 Git 的基本概念 Git 安装与配置 Git 常用命令与操作 1. 初始化本地仓库 2. 版本控制工作流程 3. 分支管理 4. 解决冲突 5. 回退和撤销 6. 查看提交日志 前言 在软件开发过程中&#xff0c;开发者常常需要在现有程序的基础上进行修改和扩展。但如果不加以管理&am…

Java 大视界 -- Java 大数据在量子通信安全中的应用探索(69)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

国产碳化硅(SiC)MOSFET模块在电镀电源中全面取代进口IGBT模块

国产碳化硅&#xff08;SiC&#xff09;MOSFET模块在电镀电源中全面取代进口IGBT模块&#xff0c;倾佳电子杨茜分析以下几方面的技术、经济和政策优势&#xff1a; 倾佳电子杨茜致力于推动SiC碳化硅模块在电力电子应用中全面取代IGBT模块&#xff0c;助力电力电子行业自主可控…