Skip to content

上海时区偏差6分钟问题

约 1120 字大约 4 分钟

Python

2025-01-17

今天写一个计算当天还剩余多少时间的函数时,发现了一个问题:上海时区偏差6分钟

我需要用这个函数返回的时间来设置Redis的缓存时间,当我去Redis中查询记录的有效时间时,发现这个时间和当前时间差了几分钟,于是我开始在本地调试,最后发现是上海时区偏差6分钟

问题

出现问题的的完整测试代码如下:

#!/Users/matias/matias/MT/MTGithub/bingwallpaper/venv/bin/python3
# coding=utf-8
'''
Author: matiastang
Date: 2025-01-17 11:26:02
LastEditors: matiastang
LastEditTime: 2025-01-17 12:32:30
FilePath: /bingwallpaper/app/utils/time_util.py
Description: 时间
'''
import pytz

from datetime import datetime
from enum import Enum


class TimezoneName(Enum):
    """时区名称"""

    UTC = 'UTC'

    SHANGHAI = 'Asia/Shanghai'
    """上海"""


def get_remaining_seconds_in_day(timezone_name: TimezoneName = TimezoneName.UTC) -> int:
    """
    获取指定时区当天剩余的时间(秒数)

    Args:
        timezone_name (str): 时区名称(如 "Asia/Shanghai", "UTC")

    Raises:
        ValueError: 未知时区

    Returns:
        int: 当天剩余时间的秒数
    """
    try:
        # 获取指定时区
        timezone = pytz.timezone(timezone_name.value)

        # 当前时区时间
        now = datetime.now(timezone)

        # 当天结束时间(午夜)
        end_of_day = datetime(now.year, now.month, now.day, 23, 59, 59, tzinfo=timezone)

        # 计算剩余时间
        print(end_of_day, now) # 2025-01-17 23:59:59+08:06 2025-01-17 12:21:27.589801+08:00
        remaining_time = end_of_day - now
        return int(remaining_time.total_seconds())
    except pytz.UnknownTimeZoneError:
        raise ValueError(f"未知时区: {timezone_name}")


if __name__ == "__main__":
    """
    测试
    python3 -m app.utils.time_util
    """
    print(get_remaining_seconds_in_day(TimezoneName.SHANGHAI))

运行测试之后,打印的信息如下:

$ python3 -m app.utils.time_util
2025-01-17 23:59:59+08:06 2025-01-17 12:21:27.589801+08:00
41551

可以看到时区这里偏差了6分钟。

分析

那这个偏差,到底是怎么回事呢?

原来这是上海的历史时区规则的问题。在现代时区(中国标准时间 CST)中,北京时间是 UTC+8,这已成为标准。虽然上海和北京是不同的城市,但它们同属于中国的标准时区(中国标准时间,CST),并且在时区数据库中共享 Asia/Shanghai 的时区名称。在 20 世纪初,中国各地时区标准未统一,有多个地方时间。上海曾经使用东八区 +8:06:00 的时间偏移(1900 年代早期)。一些时区数据库会保留这样的历史信息。

Pythonpytz 或内置的 zoneinfo 模块会基于 IANAIANA Time Zone Database)时区数据库解释时间。具体表现为:

  • 如果你创建的时间是一个未来的时间(例如 2025-01-17),它会使用现代标准,即 +08:00
  • 如果你创建的时间是在历史时间(例如 20 世纪初),则可能会显示历史偏移,如 +08:06

虽然我们使用了pytz库,但是我们只用它来获取时区,生成时间使用的是datetime。所以我们可以怀疑是datetime的问题:指定了时区为Asia/Shanghai后生成的时间为+8:06:00 的时间偏移。

我们使用下面的几种获取时间的方式测试一下

timezone = pytz.timezone(timezone_name.value)
end_of_day = datetime(now.year, now.month, now.day, 23, 59, 59)
end_of_day_test = datetime(now.year, now.month, now.day, 23, 59, 59, tzinfo=timezone)
end_of_day_time_tz = timezone.localize(end_of_day)

print(end_of_day, end_of_day_test, end_of_day_time_tz)
# 2025-01-17 23:59:59 2025-01-17 23:59:59+08:06 2025-01-17 23:59:59+08:00

可以看出,确实是datetime指定时区生成时,还是使用的老的时区+8:06:00

修改

知道原因后我们将函数进行修改如下:

def get_remaining_seconds_in_day(timezone_name: TimezoneName = TimezoneName.UTC) -> int:
    """
    获取指定时区当天剩余的时间(秒数)

    Args:
        timezone_name (str): 时区名称(如 "Asia/Shanghai", "UTC")

    Raises:
        ValueError: 未知时区

    Returns:
        int: 当天剩余时间的秒数
    """
    try:
        # 获取指定时区
        timezone = pytz.timezone(timezone_name.value)

        # 当前时区时间
        now = datetime.now()

        # 当天结束时间(午夜)
        end_of_day = datetime(now.year, now.month, now.day, 23, 59, 59)

        # 转换时间
        now_time_tz = timezone.localize(now)
        end_of_day_time_tz = timezone.localize(end_of_day)

        # 计算剩余时间
        print(end_of_day_time_tz, now_time_tz) # 2025-01-17 23:59:59+08:00 2025-01-17 12:33:35.750785+08:00
        remaining_time = end_of_day_time_tz - now_time_tz
        return int(remaining_time.total_seconds())
    except pytz.UnknownTimeZoneError:
        raise ValueError(f"未知时区: {timezone_name}")

这次我们使用timezone.localize来将标准时间,转换为Asia/Shanghai时区的时间。 再次测试,结果如下:

$ python3 -m app.utils.time_util
2025-01-17 23:59:59+08:00 2025-01-17 12:33:35.750785+08:00
41183

思考

既然是时区标准的问题,那可能其他语言或库可能也会有类似的问题。后面用到其他语言或库时,可以测试一下,看对这个问题有兼容没。