Como conscientizar um fuso horário de data / hora inconsciente em python

508

O que eu preciso fazer

Eu tenho um objeto de data e hora sem reconhecimento de fuso horário, ao qual preciso adicionar um fuso horário para poder compará-lo com outros objetos de data e hora com reconhecimento de fuso horário. Não quero converter todo o meu aplicativo em fuso horário sem conhecer este caso legado.

O que eu tentei

Primeiro, para demonstrar o problema:

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

Primeiro, tentei o 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
>>>

Não é de surpreender que isso tenha falhado, pois está realmente tentando fazer uma conversão. Substituir parecia uma escolha melhor (como em Python: como obter um valor de datetime.today () que é "consciente do fuso horário"? ):

>>> 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
>>> 

Mas, como você pode ver, substituir parece definir o tzinfo, mas não conscientiza o objeto. Estou me preparando para voltar a medicar a string de entrada para ter um fuso horário antes de analisá-la (estou usando o dateutil para análise, se isso importa), mas isso parece incrivelmente desagradável.

Além disso, eu tentei isso no python 2.6 e python 2.7, com os mesmos resultados.

Contexto

Estou escrevendo um analisador para alguns arquivos de dados. Há um formato antigo que preciso oferecer suporte onde a sequência de datas não possui um indicador de fuso horário. Eu já corrigi a fonte de dados, mas ainda preciso oferecer suporte ao formato de dados herdado. Uma conversão única dos dados herdados não é uma opção por vários motivos de negócios da BS. Embora, em geral, eu não goste da ideia de codificar um fuso horário padrão, neste caso, parece a melhor opção. Sei com confiança razoável que todos os dados herdados em questão estão no UTC, portanto, estou preparado para aceitar o risco de não cumprir isso neste caso.

Mark Tozzi
fonte
1
unaware.replace()retornaria Nonese estivesse modificando o unawareobjeto no local. O REPL mostra que .replace()retorna um novo datetimeobjeto aqui.
JFS
3
O que eu precisava quando cheguei aqui:import datetime; datetime.datetime.now(datetime.timezone.utc)
Martin Thoma 16/01
1
@MartinThoma Eu usaria o tzargumento arg para ser mais legível:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus 14/10/19

Respostas:

594

Em geral, para fazer um fuso horário-aware datetime ingênuo, use o método Localize :

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

Para o fuso horário UTC, não é realmente necessário usar, localizepois não há cálculo de horário de verão para lidar com:

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

trabalho. ( .replaceretorna uma nova data e hora; não modifica unaware.)

unutbu
fonte
10
Bem, eu me sinto boba. Substituir retorna uma nova data e hora. Diz isso também nos documentos, e eu perdi completamente isso. Obrigado, é exatamente o que eu estava procurando.
Mark Tozzi
2
"Substituir retorna uma nova data e hora." Sim. A dica que o REPL fornece é que ele está mostrando o valor retornado. :)
Karl Knechtel - fora de casa
graças, I teve utilização de dt_aware para dt_unware unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Sérgio
4
se o fuso horário não for UTC, não use o construtor diretamente: aware = datetime(..., tz)use em .localize()vez disso.
JFS
1
Vale ressaltar que a hora local pode ser ambígua. tz.localize(..., is_dst=None)afirma que não é.
JFS
167

Todos esses exemplos usam um módulo externo, mas você pode obter o mesmo resultado usando apenas o módulo datetime, como também apresentado nesta resposta do 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'

Menos dependências e sem problemas de pytz.

NOTA: Se você deseja usá-lo com python3 e python2, também pode usá-lo para a importação de fuso horário (codificado permanentemente para 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()
kang
fonte
10
Resposta muito boa para evitar pytzproblemas, fico feliz por ter rolado um pouco! Não queria enfrentar com pytzos meus servidores remotos na verdade :)
Tregoreg
7
Observe que from datetime import timezonefunciona em py3, mas não py2.7.
7yl4r
11
Você deve observar que dt.replace(tzinfo=timezone.utc)retorna um novo datetime, que não é modificado dtno local. (Vou editar para mostrar isso).
Blairg23
2
Como você, em vez de usar o timezone.utc, fornece um fuso horário diferente como uma string (por exemplo, "America / Chicago")?
precisa saber é o seguinte
2
@bumpkin antes tarde do que nunca, eu acho:tz = pytz.timezone('America/Chicago')
Florian
82

Eu usei de dt_aware para dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

e dt_unware para dt_aware

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

mas responder antes também é uma boa solução.

Sérgio
fonte
2
você poderia usar localtz.localize(dt_unware, is_dst=None)para levantar uma exceção se dt_unwarerepresenta não-existente ou hora local ambígua (nota: não houvesse tal questão na revisão anterior de sua resposta, onde localtzfoi UTC porque UTC não tem transições DST
jfs
@JF Sebastian, primeiro comentário aplicado
Sérgio
1
Agradeço por mostrar as duas direções da conversão.
Christian Long
42

Eu uso esta declaração no Django para converter um tempo inconsciente em um ciente:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())
Googol
fonte
2
Eu gosto dessa solução (+1), mas depende do Django, que não é o que eles estavam procurando (-1). =)
mkoistinen
3
Na verdade, você não entende o segundo argumento. O argumento padrão (Nenhum) significará que o fuso horário local é usado implicitamente. O mesmo com o horário de verão (que é o terceiro argumento_
Oli 12/09
14

Eu concordo com as respostas anteriores, e tudo bem se você estiver ok para começar no UTC. Mas acho que também é um cenário comum para as pessoas trabalharem com um valor ciente de tz que possui uma data e hora que possui um fuso horário local não UTC.

Se você fosse apenas usar o nome, provavelmente seria inferido que replace () será aplicável e produziria o objeto com reconhecimento de data e hora correto. Este não é o caso.

a substituição (tzinfo = ...) parece ser aleatória em seu comportamento . Portanto, é inútil. Não use isso!

localizar é a função correta a ser usada. Exemplo:

localdatetime_aware = tz.localize(datetime_nonaware)

Ou um exemplo mais completo:

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

fornece um valor de data e hora com fuso horário da hora local atual:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
paolov
fonte
3
Isso precisa de mais votos, tentar fazer replace(tzinfo=...)em um fuso horário diferente do UTC estraga sua data e hora. Eu peguei em -07:53vez de -08:00por exemplo. Veja stackoverflow.com/a/13994611/1224827
Blairg23
Você pode dar um exemplo reproduzível de replace(tzinfo=...)comportamento inesperado?
xjcl 01/06
11

Use dateutil.tz.tzlocal()para obter o fuso horário no seu uso datetime.datetime.now()e 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())

Observe que datetime.astimezoneprimeiro o datetimeobjeto será convertido para UTC e depois no fuso horário, o mesmo que chamar datetime.replacecom as informações originais do fuso horário None.

Ahmet
fonte
1
Se você quiser torná-lo UTC:.replace(tzinfo=dateutil.tz.UTC)
Martin Thoma
2
Uma importação a menos e apenas:.replace(tzinfo=datetime.timezone.utc)
kubanczyk
9

Isso codifica as respostas de @ Sérgio e @ unutbu . Ele "funcionará" apenas com um pytz.timezoneobjeto ou uma string de fuso horário da 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) 

Parece o que datetime.localize()(ou .inform()ou .awarify()) deve fazer, aceitar objetos de strings e fuso horário para o argumento tz e usar como padrão UTC se nenhum fuso horário for especificado.

fogão
fonte
1
Obrigado, isso me ajudou a "marcar" um objeto de data e hora bruto como "UTC", sem que o sistema assumisse primeiro a hora local e depois recalculasse os valores!
9607 Nikhil VJ #
4

O Python 3.9 adiciona o zoneinfomódulo e agora apenas a biblioteca padrão é necessária!

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

Anexe um fuso horário:

>>> 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'

Anexe o fuso horário local do sistema:

>>> 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'

Posteriormente, é convertido corretamente em outros fusos horários:

>>> 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'

Lista na Wikipedia de fusos horários disponíveis


Há um backport para permitir o uso no Python 3.6 a 3.8 :

sudo pip install backports.zoneinfo

Então:

from backports.zoneinfo import ZoneInfo
xjcl
fonte
1
no Windows, você também precisapip install tzdata
MrFuppes
@MrFuppes Obrigado pela dica! Vou testar isso amanhã e responder à minha resposta. Você sabe qual é a situação nos Macs?
xjcl
não, desculpe, não tem Mac por perto para tentar. Meu palpite seria que é como no Linux.
MrFuppes
0

No formato da resposta de unutbu; Eu criei um módulo utilitário que lida com coisas assim, com sintaxe mais intuitiva. Pode ser instalado com pip.

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')
As tartarugas são bonitos
fonte
0

para aqueles que só querem informar um fuso horário sobre o horário

import datetime
import pytz

datetime.datetime(2019, 12, 7, tzinfo=pytz.UTC)
Harry Moreno
fonte
0

bastante novo para Python e eu encontrei o mesmo problema. Acho esta solução bastante simples e, para mim, funciona bem (Python 3.6):

unaware=parser.parse("2020-05-01 0:00:00")
aware=unaware.replace(tzinfo=tz.tzlocal()).astimezone(tz.tzlocal())
ilmatte
fonte
0

Alterando entre fusos horários

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
fonte