在Python/Django中如何从城市获取时区

20

使用 pytz 我可以这样获取时区列表:

>>> from pytz import country_timezones
>>> print(' '.join(country_timezones('ch')))
Europe/Zurich
>>> print(' '.join(country_timezones('CH')))
Europe/Zurich

如果我从用户那里获取了国家和城市字段,我该如何确定该城市的时区?


你的意思是你想要搜索特定城市的时区数据库并获取其时区?还是只想获取(在这种情况下)Europe/Zurich的时区? - robertklep
我想在时区数据库中搜索特定城市,并获取其时区。 - super9
如何使用纬度和经度坐标从位置获取时区? - jfs
8个回答

33

pytz是围绕IANA时区数据库(Olson数据库)的包装器。它不包含将世界上任意城市映射到所在时区的数据。

您可能需要一个地理编码器,例如geopy,它可以使用各种Web服务将地点(例如城市名称)转换为其坐标(纬度,经度):

from geopy import geocoders # pip install geopy

g = geocoders.GoogleV3()
place, (lat, lng) = g.geocode('Singapore')
# -> (u'Singapore', (1.352083, 103.819836))

根据城市的纬度和经度,可以使用tz_world找到其所在的时区。例如,可以通过PostGIS时区数据库或者pytzwhere来实现。这些都是关于世界各个时区形状文件的e-fele.net/tz地图。请注意:本文中保留原有的HTML标签,请勿删除。
import tzwhere

w = tzwhere()
print w.tzNameAt(1.352083, 103.819836)
# -> Asia/Singapore

还有一些网络服务可以将(纬度,经度)转换为时区,例如askgeogeonames,请参见从纬度经度查找时区

正如@dashesy在评论中指出的那样geopy也可以找到时区(自1.2版本以来):

timezone = g.timezone((lat, lng)) # return pytz timezone object
# -> <DstTzInfo 'Asia/Singapore' LMT+6:55:00 STD>

GeoNames还提供离线数据,可以直接通过城市名称获取所在时区,例如:

#!/usr/bin/env python
import os
from collections import defaultdict
from datetime import datetime
from urllib   import urlretrieve
from urlparse import urljoin
from zipfile  import ZipFile

import pytz # pip install pytz

geonames_url = 'http://download.geonames.org/export/dump/'
basename = 'cities15000' # all cities with a population > 15000 or capitals
filename = basename + '.zip'

# get file
if not os.path.exists(filename):
    urlretrieve(urljoin(geonames_url, filename), filename)

# parse it
city2tz = defaultdict(set)
with ZipFile(filename) as zf, zf.open(basename + '.txt') as file:
    for line in file:
        fields = line.split(b'\t')
        if fields: # geoname table http://download.geonames.org/export/dump/
            name, asciiname, alternatenames = fields[1:4]
            timezone = fields[-2].decode('utf-8').strip()
            if timezone:
                for city in [name, asciiname] + alternatenames.split(b','):
                    city = city.decode('utf-8').strip()
                    if city:
                        city2tz[city].add(timezone)

print("Number of available city names (with aliases): %d" % len(city2tz))

#
n = sum((len(timezones) > 1) for city, timezones in city2tz.iteritems())
print("")
print("Find number of ambigious city names\n "
      "(that have more than one associated timezone): %d" % n)

#
fmt = '%Y-%m-%d %H:%M:%S %Z%z'
city = "Zurich"
for tzname in city2tz[city]:
    now = datetime.now(pytz.timezone(tzname))
    print("")
    print("%s is in %s timezone" % (city, tzname))
    print("Current time in %s is %s" % (city, now.strftime(fmt)))

输出

Number of available city names (with aliases): 112682

Find number of ambigious city names
 (that have more than one associated timezone): 2318

Zurich is in Europe/Zurich timezone
Current time in Zurich is 2013-05-13 11:36:33 CEST+0200

7
我认为时间过去了一些,现在似乎geopy本身可以找到时区:g.timezone(g.geocode('新加坡').point) - dashesy
@dashesy 是的,现在它就像答案中提到的其他在线服务一样。.timezone() 方法已经可用6个月了 - jfs
离线数据的唯一缺点是需要大量的存储空间,对于使用爱好者开发或亚马逊免费层级的用户来说可能不起作用,即使在Redis Cloud上可用的最大Redis内存也仅为30MB。 - Ciasto piekarz
这是一个完全基于Python 3的版本,可以在Colab上使用。https://colab.research.google.com/drive/1-QL_qQVfFGGZ4_0xk6912VusOvgqw4_c - Harry Moreno
这个现在不能直接使用了。谷歌现在要求你在使用API之前先创建一个账户。 - Nicolas Fonteyne
现在这已经不再可以简单使用了。谷歌现在要求你在能够使用API之前先创建一个账户。 - Nicolas Fonteyne

3

这里提出了许多可能的解决方案,但都有一些繁琐的设置。

为了让下一个遇到这个问题的人更快地解决,我采用了Will Charlton提供的方案,并快速制作了一个Python库:https://pypi.python.org/pypi/whenareyou

from whenareyou import whenareyou
tz = whenareyou('Hamburg')
tz.localize(datetime(2002, 10, 27, 6, 0, 0))

获取的是 datetime.datetime(2002, 10, 27, 6, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)

这将为您提供一个 pytz 对象(在示例中为 tz),以便您可以使用它的 Pythonic 方法。

  • 它使用 Google API
  • 将夏令时计算留给 pytz,每个城市只需一次调用,其余操作离线完成
  • LRU 缓存请求,因此您不应轻易达到 API 限制
  • 还应适用于任何地址或 Google 地图理解的任何内容

1
这个模块看起来非常有趣,但是今天我尝试使用它时出现了错误。我只是使用了你在 README 文件中的示例。 50 latlong = cached_json_get( 51 LONG_LAT_URL.format(quote_plus(address)) ---> 52 )['results'][0]['geometry']['location'] 53 54 return get_tz(latlong['lat'], latlong['lng'])IndexError: 列表索引超出范围 - Ruxi Zhang
看起来我需要购买谷歌高级计划才能访问gmap数据! - Ruxi Zhang
@LasseSchuirmann,即使按照您的示例操作,我仍然遇到了错误“ModuleNotFoundError: No module named 'dateutil'”。如果我安装了“python-dateutil”,它会给我返回“UnicodeDecodeError: 'charmap' codec can't decode byte 0x8d in position 2098: character maps to <undefined>”。我真的很喜欢您的程序想法,但不幸的是似乎无法正常工作。 - BenjaminK
@BenjaminK,抱歉这是几年前的事情了:/ 我不明白你的错误,但现在谷歌API似乎不再允许匿名请求。如果你想要解决问题,请查看https://github.com/aerupt/whenareyou,我会审核任何更改并在需要时进行发布。 - Lasse Schuirmann
@BenjaminK 请尝试从git获取最新版本,我刚合并了一些可能有帮助的内容。(我还没有进行全面测试,这些天我没有太多时间用于开源项目 :/) - Lasse Schuirmann
我收到了错误信息:ImportError: 无法从'requests.packages.urllib3.util.url'导入'_normalize_host'名称 - Ricky Levi

3
这段内容的翻译如下:

通过geopy的地理编码器之一,查找给定城市(或州)的纬度/经度坐标,并从地理编码器获取适当的时区。

from datetime import datetime, timezone
from geopy import geocoders

# get the location by using one of the geocoders.
# GeoNames has a free option.
gn = geopy.geocoders.GeoNames(username='your-account-name')    
loc = gn.geocode("California, USA")

# some geocoders can obtain the time zone directly.
# note: the geopy.timezone object contains a pytz timezone.
loc_tz = gn.reverse_timezone(loc.point)

# EXAMPLE: localize a datetime object
dt_UTC = datetime(2020, 11, 27, 12, 0, 0, tzinfo=timezone.utc)
dt_tz = dt_UTC.astimezone(loc_tz.pytz_timezone)
print(dt_tz, repr(dt_tz))   
# 2020-11-27 04:00:00-08:00 
# datetime.datetime(2020, 11, 27, 4, 0, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)

如果地理编码器没有生成时区信息,您可以使用timezonefinder来为给定的纬度/经度坐标确定一个时区。

据我所知,timezonefinder 只能使用纬度和经度。而且 geopy 目前无法使用(截至印度标准时间 7 月 1 日下午 2 点),因此我无法注册用户名。有没有其他替代方案?谢谢。 - Naveen Reddy Marthala
@NaveenKumar:就我所知,“GeoNames”网络服务正在工作,但其他地理编码器还不确定。 - FObersteiner
是的,它可能正在工作,但我没有用户名可用。当我尝试使用一些虚拟用户名时,我会收到HTTP错误,并且Geopy的网站对我来说已经无法访问以创建用户名。 - Naveen Reddy Marthala
@NaveenKumar:geopy只是一个包;请参阅Geopy不是服务。您需要为某个地理编码服务创建一个帐户。 - FObersteiner

2

我认为你需要手动搜索时区数据库以查找你所需要的城市:

from pytz import country_timezones, timezone

def find_city(query):
    for country, cities in country_timezones.items():
        for city in cities:
            if query in city:
                yield timezone(city)

for tz in find_city('Zurich'):
    print(tz)

(这只是一个快速粗糙的解决方案,例如它并没有尝试仅匹配时区中的城市部分 - 尝试搜索 Europe ,它进行了子字符串匹配,不区分大小写等。)

2
它对大多数城市都不适用。城市数量远远超过时区数量。 - jfs
@J.F.Sebastian OP写道:“我想在时区数据库中搜索特定城市”,因此我将我的解决方案限制为pytz提供的时区数据库中的城市。 - robertklep
OP写道:“考虑到我从用户那里获取了国家和城市字段,我该如何确定该城市的时区?”你认为用户提供的城市名称与时区名称中的某个部分相同的可能性有多大? - jfs
@J.F.Sebastian,因此我在评论中提出了问题:“您的意思是要搜索时区数据库吗?” - robertklep

1

1
很遗憾,没有简单的方法来做到这一点。Geonames记录了每个城市以及其时区名称的列表。这可能是一个好选择,但是您需要解析并构建自己的数据库,以便在任何时候轻松找到国家/城市对应的时区。

0
这不是最新的。到目前为止,我找到的最好的答案是:
from geopy.geocoders import Nominatim
from timezonefinder import TimezoneFinder

geolocator = Nominatim(user_agent="anyName")
tf = TimezoneFinder()

coords = geolocator.gecode("Dallas, Texas")
tf = TimezoneFinder()

timezone = tf.timezone_at(lng=coords.longitude, lat=coords.latitude) 更多细节请点击这里


-1

@robertklep的方法可能有效。

但是这里有另一种可能的方法,使用astral - https://pypi.python.org/pypi/astral/0.5

>>> import datetime
>>> from astral import Astral

>>> city_name = 'London'  # assuming we retrieve this from user input

>>> a = Astral()
>>> a.solar_depression = 'civil'

>>> city = a[city_name]   # creates the city object given the city name above

>>> print('Information for %s/%s\n' % (city_name, city.country))
Information for London/England

>>> timezone = city.timezone
>>> print('Timezone: %s' % timezone)
Timezone: Europe/London

它应该能够工作,但支持的城市列表非常有限。 - jfs
很多城市在那里找不到...伦敦很容易,但其他城市根本没有列出来... - Ricky Levi

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