跳到主要内容
AZ Tools

TZif(zoneinfo)时区文件解析器

TZif 文件是时区被编译之后的样子:/usr/share/zoneinfo/Europe/Berlin 里的那个文件、TZ= 指向的那个文件、随 JDK 一起分发或被复制进容器镜像的那个文件。它不可读、内部也不带自己的名字,于是被到处复制却无从核对。这个工具按照 RFC 8536 在浏览器里打开它,把里面真正的内容列出来。头部给出版本和六个计数,并且把两个数据块的计数并排显示,因为版本 2 及以上的文件会先完整放一个 32 位块,再放一个 64 位块。前者是现代解析器会跳过的历史遗留部分,在 slim 文件里已被削减到只剩一个类型、零条转换,真正的数据在后一个块中。类型表给出每个偏移、夏令时标志、缩写,以及标准/挂钟和 UT/本地这两个标志,它们说明生成该类型的规则当初是用哪种时刻写的。转换表逐条列出切换前后的偏移、接管的缩写以及是否进入夏令时;转换时刻是有符号 64 位秒,所以文件里出现 1901 年和 2037 年的日期,或者一两条什么都不改变的哨兵记录,都属于正常。POSIX 结尾字符串不是照抄,而是拆成字段:标准时与夏令时缩写、两个把 POSIX 反向符号还原之后的偏移,以及 M、J 或从 0 起算三种形式的起止规则,并按所显示的参考年份算出具体时刻。对 slim 文件来说,这段结尾正是关键,因为再没有别的东西描述明年春天。只有真正带闰秒的文件(也就是 right/ 目录下的那一套)才会列出闰秒记录。它做不到的是告诉你这是哪个时区:TZif 文件不保存自己的名字,像 Asia/Istanbul 这样的别名与 Europe/Istanbul 逐字节相同;它也无法判断文件是否损坏,因为这个格式没有校验和,中间翻转一个字节只会悄悄挪动一次转换。

使用方法

  1. 把编译好的时区文件拖进方框:可以从 /usr/share/zoneinfo 复制,用 docker cp 从容器里取出,或者从 JDK 里取出。
  2. 先看概要:版本、文件里有多少条转换,以及未来是被逐条列出(fat)、交给 POSIX 规则(slim),还是固定不变。
  3. 对比头部的两列。slim 文件会显示一个几乎空的 32 位块和一个内容完整的 64 位块,这是格式本来的设计,不是损坏。
  4. 查看参考时刻与下一次转换,就知道这个文件今天会给出什么偏移、下一次什么时候改变。
  5. 如果是 slim 文件,请展开 POSIX 结尾字符串一节:最后一次转换之后的所有日期,都只由那里的 M、J 或从 0 起算的规则决定。

常见问题

头部为什么有两套计数?
因为版本 2 及以上的文件确实把数据存了两遍。文件开头是一个完整的版本 1 块,其中的转换时刻是 32 位的;之后才是第二个头部、一个 64 位块,最后是 POSIX 结尾字符串。这样重复是为了让 2004 年之前写的解析器读到能理解的内容而不是直接失败,而 RFC 8536 要求现代解析器直接跳到第二个块。所以两套计数不一致是正常的:在 zic 的 slim 输出里,第一个块被削减到只有一个本地时间类型、零条转换,真正的数据都在第二个块。如果某个工具说这个时区没有任何历史,多半是它错读了那个遗留块,而今天仍有库这么做。
fat 文件和 slim 文件有什么区别?
两者描述的是同一个时区,区别在于把多少未来写了出来。fat 文件把 2037 年之前的所有转换逐条列出,所以 Europe/Berlin 里大约有 140 条,今年十月的切换可以直接从表里读到。slim 文件在最后一条无法由规则推出的转换处停下,之后全部交给 POSIX 结尾字符串,因此体积可能只有十分之一。自 2020 年起 zic 默认输出 slim,但各发行版做法不同:Debian 和 Ubuntu 仍然分发 fat 文件,Alpine 以及不少容器基础镜像分发 slim 文件。忽略结尾字符串的解析器在 fat 文件上一切正常,在 slim 文件上却会悄悄把明年算错——这正是那种只在某个镜像里复现的 bug。
POSIX 结尾字符串怎么读,M、J 和单独的数字分别是什么意思?
这段结尾和 TZ 环境变量能填的字符串是同一种格式:标准时缩写、标准时偏移,然后可选地给出夏令时缩写、夏令时偏移,以及在两者之间切换的两条规则。它的偏移采用 POSIX 的符号约定,格林尼治以西为正,所以 CET-1 表示 UTC+01:00,本工具会把符号还原过来。规则有三种形式。Mm.w.d 是月、周、星期几,周为 5 表示当月最后一周,所以 M3.5.0 是三月的最后一个星期日。Jn 是从 1 起算的年内第 n 天,并且从不计入 2 月 29 日,所以 J60 永远是 3 月 1 日。单独的数字从 0 起算,而且计入 2 月 29 日,所以 59 在平年是 3 月 1 日,在闰年是 2 月 29 日。最后一种形式很少见,也值得避开:各实现的理解并不一致,Python 的 zoneinfo 就比 glibc 晚一天。
为什么 Etc/GMT+5 显示的偏移是 -05:00?
因为 Etc 这一系名字沿用 POSIX 的符号约定,而它与其他所有地方的约定正好相反:在 TZ 字符串里,正号表示格林尼治以西。所以 Etc/GMT+5 是比 UTC 晚五小时的时区,而 ISO 8601 和各种界面都把它写成 -05:00;反过来 Etc/GMT-5 才是快五小时的那一个。tz 数据库保留这些名字正是因为 POSIX 要求这种行为,同时也提醒它们是个陷阱。本工具按 ISO 方式显示偏移,所以名字写着 +5、偏移显示 -05:00 既不矛盾也不是 bug,只是命名约定露了出来。
闰秒记录是什么?为什么 right/ 文件里的转换时刻看着不对?
tz 数据库可以编译成两套:常见的 posix/ 一套,其中时间戳是不含闰秒的秒数;以及 right/ 一套,其中 1972 年以来的闰秒作为记录保存,时间戳统计的是真实流逝的秒数。多数系统只分发前一套,所以这里能看到闰秒表,说明你手上是 right/ 文件,或者是别人用 zic -L 编译出来的。在这类文件里,转换会被累计修正量整体推移:本该是 01:00:00 的切换会显示成 01:00:27,因为 1972 年以来一共插入了 27 个闰秒,修正列也一路加到同一个 27。把 right/ 文件混进期待 posix/ 时间的系统,正是经典的“刚好差 27 秒”的来源。
它能告诉我这是哪个时区,或者文件有没有损坏吗?
两者都不能,而且这两个限制都是格式本身决定的。TZif 文件里不含任何名字:时区的身份完全来自它在数据库中的路径,所以像 Asia/Istanbul 这样的链接要么是 Europe/Istanbul 的逐字节副本,要么是指向它的符号链接,从内容上无法区分。你能做的最多是靠指纹认出它——缩写、偏移和各次转换的日期,这些表格正是为此而设。格式里也没有任何校验和,因此转换表中翻转一个字节并不会让文件失效,只会悄悄挪动一次转换或让它指向另一个类型。唯一可检测的是长度与计数对不上,而本工具正是把这种文件按截断拒绝。

相关工具