Как сделать незнакомый часовой пояс даты и времени в Python

508

Что я должен сделать

У меня есть объект datetime, не поддерживающий часовой пояс, к которому мне нужно добавить часовой пояс, чтобы иметь возможность сравнивать его с другими объектами datetime с учетом часового пояса. Я не хочу преобразовывать все мое приложение в часовой пояс, не подозревая об этом унаследованном случае.

Что я пробовал

Во-первых, чтобы продемонстрировать проблему:

Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> import pytz
>>> unaware = datetime.datetime(2011,8,15,8,15,12,0)
>>> unaware
datetime.datetime(2011, 8, 15, 8, 15, 12)
>>> aware = datetime.datetime(2011,8,15,8,15,12,0,pytz.UTC)
>>> aware
datetime.datetime(2011, 8, 15, 8, 15, 12, tzinfo=<UTC>)
>>> aware == unaware
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes

Сначала я попробовал astimezone:

>>> unaware.astimezone(pytz.UTC)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: astimezone() cannot be applied to a naive datetime
>>>

Неудивительно, что это не удалось, так как он пытается конвертировать. Заменить казалось лучшим выбором (согласно Python: как получить значение datetime.today (), которое «учитывает часовой пояс»? ):

>>> unaware.replace(tzinfo=pytz.UTC)
datetime.datetime(2011, 8, 15, 8, 15, 12, tzinfo=<UTC>)
>>> unaware == aware
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes
>>> 

Но, как вы можете видеть, замена, кажется, устанавливает tzinfo, но не делает объект осведомленным. Я собираюсь вернуться к лечению входной строки, чтобы иметь часовой пояс перед его синтаксическим анализом (я использую dateutil для анализа, если это имеет значение), но это кажется невероятно грязным.

Кроме того, я пробовал это и в Python 2.6 и Python 2.7, с одинаковыми результатами.

контекст

Я пишу парсер для некоторых файлов данных. Существует старый формат, который мне нужно поддерживать, когда строка даты не имеет индикатора часового пояса. Я уже исправил источник данных, но мне все еще нужно поддерживать устаревший формат данных. Однократное преобразование устаревших данных не подходит по различным причинам бизнес-BS. Хотя в целом мне не нравится идея жесткого кодирования часового пояса по умолчанию, в этом случае он кажется наилучшим вариантом. Я с достаточной уверенностью знаю, что все унаследованные данные находятся в UTC, поэтому я готов принять на себя риск невыполнения этого требования в этом случае.

Марк Тоцци
источник
1
unaware.replace()вернулся Noneбы, если бы он изменял unawareобъект на месте. REPL показывает, что .replace()возвращает новый datetimeобъект здесь.
JFS
3
Что мне было нужно, когда я приехал сюда:import datetime; datetime.datetime.now(datetime.timezone.utc)
Мартин Тома
1
@MartinThoma Я хотел бы использовать названный tzаргумент, чтобы быть более читабельным:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus

Ответы:

596

В общем, чтобы сделать наивный точный часовой пояс даты и времени, используйте метод локализации :

import datetime
import pytz

unaware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0)
aware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0, pytz.UTC)

now_aware = pytz.utc.localize(unaware)
assert aware == now_aware

Для часового пояса UTC использование на самом деле не является необходимым, localizeтак как нет никакого расчета летнего времени для обработки:

now_aware = unaware.replace(tzinfo=pytz.UTC)

работает. ( .replaceвозвращает новую дату и время; она не изменяется unaware.)

unutbu
источник
10
Ну, я чувствую себя глупо. Заменить возвращает новую дату и время. Там тоже написано в документах, и я полностью пропустил это. Спасибо, это именно то, что я искал.
Марк Тоцци
2
«Заменить возвращает новую дату и время». Ага. Намек, который дает вам REPL, заключается в том, что он показывает возвращаемое значение. :)
Карл Кнехтель - далеко от дома
спасибо, я использовал от dt_aware, чтобы не знать dt_unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Сержио
4
если часовой пояс не UTC, не используйте конструктор напрямую: aware = datetime(..., tz)используйте .localize()вместо этого.
Jfs
1
Стоит отметить, что местное время может быть неоднозначным. tz.localize(..., is_dst=None)утверждает, что это не так.
Jfs
167

Во всех этих примерах используется внешний модуль, но вы можете достичь того же результата, используя только модуль datetime, как также представлено в этом ответе SO :

from datetime import datetime
from datetime import timezone

dt = datetime.now()
dt.replace(tzinfo=timezone.utc)

print(dt.replace(tzinfo=timezone.utc).isoformat())
'2017-01-12T22:11:31+00:00'

Меньше зависимостей и нет проблем с Pytz.

ПРИМЕЧАНИЕ: Если вы хотите использовать это с python3 и python2, вы можете использовать это также для импорта часового пояса (жестко задано для UTC):

try:
    from datetime import timezone
    utc = timezone.utc
except ImportError:
    #Hi there python2 user
    class UTC(tzinfo):
        def utcoffset(self, dt):
            return timedelta(0)
        def tzname(self, dt):
            return "UTC"
        def dst(self, dt):
            return timedelta(0)
    utc = UTC()
кан
источник
10
Очень хороший ответ для предотвращения pytzпроблем, я рад, что прокрутил немного вниз! Не хотел заниматься pytzна моих удаленных серверах действительно :)
Tregoreg
7
Обратите внимание, что from datetime import timezoneработает в py3, но не py2.7.
7yl4r
11
Следует отметить, что dt.replace(tzinfo=timezone.utc)возвращает новое время и дату, оно не изменяется dtна месте. (Я буду редактировать, чтобы показать это).
Blairg23
2
Как бы вы вместо использования timezone.utc указали другой часовой пояс в виде строки (например, «Америка / Чикаго»)?
шишка
2
@ bumpkin лучше поздно, чем никогда, я думаю:tz = pytz.timezone('America/Chicago')
Флориан
83

Я использовал от dt_aware до dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

и dt_unware для dt_aware

from pytz import timezone
localtz = timezone('Europe/Lisbon')
dt_aware = localtz.localize(dt_unware)

но ответ раньше тоже хорошее решение.

Сержио
источник
2
Вы могли бы использовать, localtz.localize(dt_unware, is_dst=None)чтобы вызвать исключение, если оно dt_unwareпредставляет собой несуществующее или неоднозначное местное время (примечание: в предыдущей редакции вашего ответа такой проблемы не localtzбыло, когда было UTC, потому что в UTC нет переходов DST
jfs
@JF Себастьян, первый комментарий применен
Сержио
1
Я ценю, что вы показываете оба направления конверсии.
Кристиан Лонг
42

Я использую это утверждение в Django, чтобы преобразовать неосведомленное время в осведомленное:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())
Googol
источник
2
Мне действительно нравится это решение (+1), но оно зависит от Django, а это не то, что они искали (-1). =)
mkoistinen
3
Вы на самом деле не второй аргумент. По умолчанию аргумент (None) будет означать , что локальная временная зона используется неявно. То же самое с DST (который является третьим аргументом_
Оли
14

Я согласен с предыдущими ответами, и это нормально, если вы можете начать в UTC. Но я думаю, что это также распространенный сценарий, когда люди работают со значением, учитывающим tz, которое имеет дату и время, а не местный часовой пояс UTC.

Если бы вы просто пошли по имени, можно было бы предположить, что replace () будет применим и создаст правильный объект с указанием даты и времени. Это не вариант.

замена (tzinfo = ...) кажется случайной в своем поведении . Поэтому бесполезно. Не используйте это!

localize - это правильная функция для использования. Пример:

localdatetime_aware = tz.localize(datetime_nonaware)

Или более полный пример:

import pytz
from datetime import datetime
pytz.timezone('Australia/Melbourne').localize(datetime.now())

дает мне текущее местное время с учетом даты и времени:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
paolov
источник
3
Для этого нужно больше голосов, попытка сделать replace(tzinfo=...)это в часовом поясе, отличном от UTC, испортит ваше время. Я получил -07:53вместо, -08:00например. См stackoverflow.com/a/13994611/1224827
Blairg23
Можете ли вы привести воспроизводимый пример replace(tzinfo=...)неожиданного поведения?
xjcl
11

Используйте, dateutil.tz.tzlocal()чтобы получить часовой пояс в вашем использовании datetime.datetime.now()и datetime.datetime.astimezone():

from datetime import datetime
from dateutil import tz

unlocalisedDatetime = datetime.now()

localisedDatetime1 = datetime.now(tz = tz.tzlocal())
localisedDatetime2 = datetime(2017, 6, 24, 12, 24, 36, tz.tzlocal())
localisedDatetime3 = unlocalisedDatetime.astimezone(tz = tz.tzlocal())
localisedDatetime4 = unlocalisedDatetime.replace(tzinfo = tz.tzlocal())

Обратите внимание, что datetime.astimezoneсначала будет преобразован ваш datetimeобъект в UTC, а затем в часовой пояс, который аналогичен вызову datetime.replaceс исходной информацией часового пояса None.

Ахмет
источник
1
Если вы хотите сделать это UTC:.replace(tzinfo=dateutil.tz.UTC)
Мартин Тома
2
На один импорт меньше и всего:.replace(tzinfo=datetime.timezone.utc)
kubanczyk
9

Это систематизирует ответы @ Sérgio и @ unutbu . Он будет «просто работать» с pytz.timezoneобъектом или строкой часового пояса IANA .

def make_tz_aware(dt, tz='UTC', is_dst=None):
    """Add timezone information to a datetime object, only if it is naive."""
    tz = dt.tzinfo or tz
    try:
        tz = pytz.timezone(tz)
    except AttributeError:
        pass
    return tz.localize(dt, is_dst=is_dst) 

Это похоже на то, что datetime.localize()(или .inform()или .awarify()) должно делать, принимать как строки, так и объекты часового пояса для аргумента tz и по умолчанию UTC, если часовой пояс не указан.

варочные панели
источник
1
Спасибо, это помогло мне «пометить» необработанный объект даты и времени как «UTC», при этом система сначала не приняла местное время, а затем пересчитала значения!
Nikhil VJ
4

Python 3.9 добавляет zoneinfoмодуль, поэтому теперь нужна только стандартная библиотека!

from zoneinfo import ZoneInfo
from datetime import datetime
unaware = datetime(2020, 10, 31, 12)

Прикрепить часовой пояс:

>>> unaware.replace(tzinfo=ZoneInfo('Asia/Tokyo'))
datetime.datetime(2020, 10, 31, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))
>>> str(_)
'2020-10-31 12:00:00+09:00'

Присоедините местный часовой пояс системы:

>>> unaware.replace(tzinfo=ZoneInfo('localtime'))
datetime.datetime(2020, 10, 31, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='localtime'))
>>> str(_)
'2020-10-31 12:00:00+01:00'

Впоследствии он правильно конвертируется в другие часовые пояса:

>>> unaware.replace(tzinfo=ZoneInfo('localtime')).astimezone(ZoneInfo('Asia/Tokyo'))
datetime.datetime(2020, 10, 31, 20, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='Asia/Tokyo'))
>>> str(_)
'2020-10-31 20:00:00+09:00'

Википедия список доступных часовых поясов


Windows не имеет базы данных системного часового пояса, поэтому здесь необходим дополнительный пакет:

pip install tzdata  

Существует бэкпорт, позволяющий использовать в Python 3.6 до 3.8 :

pip install backports.zoneinfo

Затем:

from backports.zoneinfo import ZoneInfo
xjcl
источник
1
на винде тоже надоpip install tzdata
MrFuppes
@MrFuppes Спасибо за совет! Я проверю это завтра и на мой ответ. Знаете ли вы, какова ситуация на Mac?
xjcl
Нет, прости, у меня нет Mac, чтобы попробовать. Я думаю, что это похоже на Linux.
MrFuppes
0

В формате ответа unutbu; Я сделал служебный модуль, который обрабатывает подобные вещи с более интуитивным синтаксисом. Может быть установлен с помощью пипа.

import datetime
import saturn

unaware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0)
now_aware = saturn.fix_naive(unaware)

now_aware_madrid = saturn.fix_naive(unaware, 'Europe/Madrid')
Милые черепахи
источник
0

для тех, кто просто хочет узнать дату и время

import datetime
import pytz

datetime.datetime(2019, 12, 7, tzinfo=pytz.UTC)
Гарри Морено
источник
0

совершенно новый для Python, и я столкнулся с той же проблемой. Я нахожу это решение довольно простым, и для меня оно отлично работает (Python 3.6):

unaware=parser.parse("2020-05-01 0:00:00")
aware=unaware.replace(tzinfo=tz.tzlocal()).astimezone(tz.tzlocal())
ilmatte
источник
0

Переключение между часовыми поясами

import pytz
from datetime import datetime

other_tz = pytz.timezone('Europe/Madrid')

# From random aware datetime...
aware_datetime = datetime.utcnow().astimezone(other_tz)
>> 2020-05-21 08:28:26.984948+02:00

# 1. Change aware datetime to UTC and remove tzinfo to obtain an unaware datetime
unaware_datetime = aware_datetime.astimezone(pytz.UTC).replace(tzinfo=None)
>> 2020-05-21 06:28:26.984948

# 2. Set tzinfo to UTC directly on an unaware datetime to obtain an utc aware datetime
aware_datetime_utc = unaware_datetime.replace(tzinfo=pytz.UTC)
>> 2020-05-21 06:28:26.984948+00:00

# 3. Convert the aware utc datetime into another timezone
reconverted_aware_datetime = aware_datetime_utc.astimezone(other_tz)
>> 2020-05-21 08:28:26.984948+02:00

# Initial Aware Datetime and Reconverted Aware Datetime are equal
print(aware_datetime1 == aware_datetime2)
>> True
Shide
источник