获取本地时区的Olson TZ名称?

53

如何获取与C语言的localtime调用给定值对应的Olson时区名称(例如Australia/Sydney)?

这个值可以通过TZ覆盖,通过符号链接/etc/localtime或在与时间相关的系统配置文件中设置TIMEZONE变量来进行设置。


2
所以,明确一下,您想要一种在Linux上确定当前用户语言环境的Olson时区名称(即America/New_York)的方法? - wberry
1
@wberry:是的,确切地说。 - Matt Joiner
2
在那种情况下,我会收藏这个因为我也想知道 :-) - wberry
1
类似于https://dev59.com/N0rSa4cB1Zd3GeqPU0f6,可以获得一些想法。 - A.H.
我认为我的答案正确地列出了可能的匹配项?你认为它在某些情况下会给出错误的结果吗? - Anurag Uniyal
@MattJoiner 我修改了代码片段,使其更加可靠。希望这正是你所需要的。 - Patrick Perini
11个回答

19

我知道这有点不光彩,但是从'/etc/localtime'获取不行吗?像下面这样:

>>>  import os
>>> '/'.join(os.readlink('/etc/localtime').split('/')[-2:])
'Australia/Sydney'

希望有所帮助。

编辑: 我喜欢@A.H.的想法,在'/etc/localtime'不是符号链接的情况下。把它翻译成Python代码:

#!/usr/bin/env python

from hashlib import sha224
import os

def get_current_olsonname():
    tzfile = open('/etc/localtime')
    tzfile_digest = sha224(tzfile.read()).hexdigest()
    tzfile.close()

    for root, dirs, filenames in os.walk("/usr/share/zoneinfo/"):
        for filename in filenames:
            fullname = os.path.join(root, filename)
            f = open(fullname)
            digest = sha224(f.read()).hexdigest()
            if digest == tzfile_digest:
                return '/'.join((fullname.split('/'))[-2:])
            f.close()
        return None

if __name__ == '__main__':
    print get_current_olsonname()

1
当/etc/localtime是符号链接时,它可以工作。但大多数情况下并非如此。 - Matt Joiner
1
这个效果出奇的好。我想知道如果zoneinfo被更新,但是/etc/localtime没有被更新会发生什么。此外,我建议您立即切换到4个空格缩进而不是制表符(或8个空格)。 - Matt Joiner
好的,它之间是8个空格... 关于zoneinfo,我认为你的系统将没有时区,没有/etc/locatime,对吧? - Thiago Curvelo
1
不,我的意思是如果zoneinfo文件被更新,而/etc/localtime不是符号链接。你可能会有一个本地时间与任何zoneinfo文件不再匹配的情况。 - Matt Joiner
4
很遗憾,在我的Ubuntu系统上,这会返回zoneinfo/localtime。 - Matt Joiner
@MattJoiner:在我的Ubuntu系统上,它产生了与Anurag的答案相同的结果。我使用了这段代码 - jfs

17

我认为最好的方法是遍历所有的pytz时区,并检查哪个与本地时区匹配,每个pytz时区对象都包含有关utcoffset和tzname(如CDT、EST)的信息,可以从time.timezone/altzonetime.tzname获取有关本地时间的相同信息,我认为这足以正确匹配pytz数据库中的本地时区。

import time
import pytz
import datetime

local_names = []
if time.daylight:
    local_offset = time.altzone
    localtz = time.tzname[1]
else:
    local_offset = time.timezone
    localtz = time.tzname[0]

local_offset = datetime.timedelta(seconds=-local_offset)

for name in pytz.all_timezones:
    timezone = pytz.timezone(name)
    if not hasattr(timezone, '_tzinfos'):
        continue#skip, if some timezone doesn't have info
    # go thru tzinfo and see if short name like EDT and offset matches
    for (utcoffset, daylight, tzname), _ in timezone._tzinfos.iteritems():
        if utcoffset == local_offset and tzname == localtz:
            local_names.append(name)

print local_names

输出:

['America/Atikokan', 'America/Bahia_Banderas', 'America/Bahia_Banderas', 'America/Belize', 'America/Cambridge_Bay', 'America/Cancun', 'America/Chicago', 'America/Chihuahua', 'America/Coral_Harbour', 'America/Costa_Rica', 'America/El_Salvador', 'America/Fort_Wayne', 'America/Guatemala', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Indianapolis', 'America/Iqaluit', 'America/Kentucky/Louisville', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Knox_IN', 'America/Louisville', 'America/Louisville', 'America/Managua', 'America/Matamoros', 'America/Menominee', 'America/Merida', 'America/Mexico_City', 'America/Monterrey', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Ojinaga', 'America/Pangnirtung', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Resolute', 'America/Resolute', 'America/Tegucigalpa', 'America/Winnipeg', 'CST6CDT', 'Canada/Central', 'Mexico/General', 'US/Central', 'US/East-Indiana', 'US/Indiana-Starke']

在生产环境中,您可以预先创建这样的映射并保存它,而不是总是迭代。

更改时区后的测试脚本:

$ export TZ='Australia/Sydney'
$ python get_tz_names.py
['Antarctica/Macquarie', 'Australia/ACT', 'Australia/Brisbane', 'Australia/Canberra', 'Australia/Currie', 'Australia/Hobart', 'Australia/Lindeman', 'Australia/Melbourne', 'Australia/NSW', 'Australia/Queensland', 'Australia/Sydney', 'Australia/Tasmania', 'Australia/Victoria']


这个算法到目前为止给出了最好的结果,但它似乎不如 @pcperini 的答案干净。 - Matt Joiner
@Matt Joiner,我认为将偏移量与国家名称进行比较,代码更加简洁和准确。 - Anurag Uniyal
@MattJoiner,我认为从URL获取的国家代码不会因为系统的TZ设置而改变。 - Anurag Uniyal
3
要查看夏令时是否生效,请使用以下代码:if time.daylight and time.localtime().tm_isdst > 0。注意:time.daylight 只是告诉您本地时区是否采用了夏令时,它并没有提供任何关于当前状态的信息。 - jfs
这会丢失使用奥尔森时区名称所代表的历史信息 - 如果我使用其中一个结果将记录在UTC中的历史日期转换为相应的当地时间,可能会得到错误的转换结果:因为许多名称/时区的原因之一是不同地方以不同的方式切换其UTC偏移量。 - chichak

12

一个问题是存在多个“美丽的名称”,比如“澳大利亚/悉尼”,它们指向同一个时区(例如CST)。

因此,您需要获取本地时区的所有可能名称,然后选择您喜欢的名称。

例如:对于澳大利亚,有5个时区,但更多的时区标识符:

     "Australia/Lord_Howe", "Australia/Hobart", "Australia/Currie", 
     "Australia/Melbourne", "Australia/Sydney", "Australia/Broken_Hill", 
     "Australia/Brisbane", "Australia/Lindeman", "Australia/Adelaide", 
     "Australia/Darwin", "Australia/Perth", "Australia/Eucla"

你应该检查是否有一个包装TZinfo的库来处理时区API。
例如:对于Python,请检查pytz库。

http://pytz.sourceforge.net/

并且

http://pypi.python.org/pypi/pytz/

在Python中,你可以做到:
from pytz import timezone
import pytz

In [56]: pytz.country_timezones('AU')
Out[56]: 
[u'Australia/Lord_Howe',
 u'Australia/Hobart',
 u'Australia/Currie',
 u'Australia/Melbourne',
 u'Australia/Sydney',
 u'Australia/Broken_Hill',
 u'Australia/Brisbane',
 u'Australia/Lindeman',
 u'Australia/Adelaide',
 u'Australia/Darwin',
 u'Australia/Perth',
 u'Australia/Eucla']

但是Python的API似乎相当有限,例如它似乎没有像Ruby的all_linked_zone_names这样的调用——可以找到给定时区的所有同义词名称。


我同意,我认为这些旨在让用户更轻松地设置他们的时区(例如,不知道他们的UTC偏移量)。 - Mike
@Tilo:该赏金要求编写一个生成时区名称列表算法。 - Matt Joiner
在Ruby中很容易实现(我在下面添加了一个Ruby的答案)...请查看pytz API以了解如何在Python中执行相同操作。 - Tilo
对于澳大利亚的时区,这可能是正确的(我不确定),但许多现在具有相同时间的不同时区并不总是拥有相同的时间 - 尤其是在欧洲。欧洲/苏黎世和欧洲/柏林在夏令时观察和非观察时期上有差异。 - chichak

8
如果您认为评估 /etc/localtime 是可以接受的,那么下面的技巧可能有效 - 在将其翻译为Python后:
> md5sum /etc/localtime
abcdefabcdefabcdefabcdefabcdefab /etc/localtime
> find /usr/share/zoneinfo -type f |xargs md5sum | grep abcdefabcdefabcdefabcdefabcdefab
abcdefabcdefabcdefabcdefabcdefab /usr/share/zoneinfo/Europe/London
abcdefabcdefabcdefabcdefabcdefab /usr/share/zoneinfo/posix/Europe/London
...

重复项可以使用官方地区名称“欧洲”,“美洲”等进行过滤...如果仍有重复项,则可以选择最短的名称 :-)

7
安装 pytz

安装 pytz

import pytz
import time
#import locale
import urllib2

yourOlsonTZ = None
#yourCountryCode = locale.getdefaultlocale()[0].split('_')[1]
yourCountryCode = urllib2.urlopen('http://api.hostip.info/country.php').read()

for olsonTZ in [pytz.timezone(olsonTZ) for olsonTZ in pytz.all_timezones]:
    if (olsonTZ._tzname in time.tzname) and (str(olsonTZ) in pytz.country_timezones[yourCountryCode]):
        yourOlsonTZ = olsonTZ
        break

print yourOlsonTZ

该代码将根据您的时区名称(根据Python的time模块)和您的国家代码(根据hostip.info项目,引用您的IP地址并相应地确定您的位置),尽力猜测您的Olson时区。例如,仅匹配时区名称可能会得出EST(GMT-5)的America/MonctonAmerica/MontrealAmerica/New_York。然而,如果您的国家是美国,它将限制答案为America/New_York。但是,如果您的国家是加拿大,则脚本将简单地默认为最顶部的加拿大结果(America/Moncton)。如果有更进一步完善的方法,请随时在评论中提出建议。

你能否重新调整国家代码的确定方式,从更可靠的地方获取该值?在澳大利亚,尽管存在en_AU,但拥有en_US并不罕见。此外,我的时区名称是“EST”,因此您的算法会给出“America/New_York”,但我实际上在“悉尼/澳大利亚”。 - Matt Joiner
正在处理中。我可以假设有互联网访问吗? - Patrick Perini

5
Python的tzlocal模块旨在解决这个问题。它可以在Linux和Windows下产生一致的结果,并使用CLDR映射将Windows时区ID正确转换为Olson。

1

这将根据TZ变量中的内容或未设置时的本地时间文件获取时区名称:

#! /usr/bin/env python

import time

time.tzset
print time.tzname

1
不幸的是,它返回了由三个字母时区代码组成的元组,而不是完整的时区名称。 - Matt Joiner
3
即使为此构建查找表,也会存在歧义。"EST"可以是America/New_York或者Australia/Sydney。"BST"可以是Europe/London或者Asia/Dhaka - wberry
2
他是正确的。系统唯一知道的时区信息是GMT偏移量和DST设置。在Linux上,这些设置可以通过TZ环境变量(通常未设置)或/etc/localtime文件进行设置,该文件可以是/usr/share/zoneinfo中找到的时区定义之一的链接或其中一个的副本。除非您确定运行脚本的系统使用符号链接或环境变量进行配置,否则无法通用地获取此值。然后,您可能可以从Python内部运行系统命令来获取它。 - Yanick Girouard
@MattJoiner:如果你想获取环境变量的内容,你可以使用os.getenv("TZ"),或者解析ls -l /etc/localtime的返回结果并从中提取链接路径。这里有一个关于如何做到这一点的完整线程:https://dev59.com/SnVD5IYBdhLWcg3wGXlI(调用ls而不是解析路径) - Yanick Girouard

1

这里还有另一种可能性,可以使用PyICU代替;这对我的目的来说是有效的:

>>> from PyICU import ICUtzinfo
>>> from datetime import datetime
>>> datetime(2012, 1, 1, 12, 30, 18).replace(tzinfo=ICUtzinfo.getDefault()).isoformat()
'2012-01-01T12:30:18-05:00'
>>> datetime(2012, 6, 1, 12, 30, 18).replace(tzinfo=ICUtzinfo.getDefault()).isoformat()
'2012-06-01T12:30:18-04:00'

这里是将本地时区中的naive日期时间(如数据库查询返回的)进行解释。


ICU有什么问题,导致人们不喜欢这个答案吗? - saeedgnu

0

我更喜欢遵循稍微好一点的方式,而不是四处摸索 _xxx 值。

import time, pytz, os

cur_name=time.tzname
cur_TZ=os.environ.get("TZ")

def is_current(name):
   os.environ["TZ"]=name
   time.tzset()
   return time.tzname==cur_name

print "Possible choices:", filter(is_current, pytz.all_timezones)

# optional tz restore
if cur_TZ is None: del os.environ["TZ"]
else: os.environ["TZ"]=cur_TZ
time.tzset()

1
使用 TZ 环境变量仅适用于 Linux(或 Unix),不适用于 Windows。 - saeedgnu
另外一件事是,它在我的当前Fedora(F20)上根本不起作用,没有定义这样的变量TZ。 - Vinzenz

0

我修改了tcurvelo的脚本,以找到正确的时区形式(大陆/.../城市),在大多数情况下,但如果失败则返回所有时区形式

#!/usr/bin/env python

from hashlib import sha224
import os
from os import listdir
from os.path import join, isfile, isdir

infoDir = '/usr/share/zoneinfo/'

def get_current_olsonname():
    result = []
    tzfile_digest = sha224(open('/etc/localtime').read()).hexdigest()

    test_match = lambda filepath: sha224(open(filepath).read()).hexdigest() == tzfile_digest

    def walk_over(dirpath):
        for root, dirs, filenames in os.walk(dirpath):
            for fname in filenames:
                fpath = join(root, fname)
                if test_match(fpath):
                    result.append(tuple(root.split('/')[4:]+[fname]))

    for dname in listdir(infoDir):
        if dname in ('posix', 'right', 'SystemV', 'Etc'):
            continue
        dpath = join(infoDir, dname)
        if not isdir(dpath):
            continue
        walk_over(dpath)

    if not result:
        walk_over(join(infoDir))

    return result


if __name__ == '__main__':
    print get_current_olsonname()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接