Jak ustawić nieświadomą strefę czasową datetime w Pythonie

506

Co muszę zrobić

Mam obiekt datetime nieświadomy strefy czasowej, do którego muszę dodać strefę czasową, aby móc ją porównać z innymi obiektami datetime rozpoznającymi strefę czasową. Nie chcę przekonwertować całej aplikacji na strefę czasową, nieświadoma tego jednego starszego przypadku.

Co próbowałem

Po pierwsze, aby zademonstrować problem:

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

Najpierw spróbowałem astimezonu:

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

Nic dziwnego, że to się nie powiodło, ponieważ tak naprawdę próbuje dokonać konwersji. Zastąpienie wydawało się lepszym wyborem (jak w Pythonie: jak uzyskać wartość datetime.today (), która jest „świadoma strefy czasowej”? ):

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

Ale jak widać, zamień wydaje się ustawiać tzinfo, ale nie uświadamia obiektu. Przygotowuję się do powrotu do sprawdzania łańcucha wejściowego, aby mieć strefę czasową przed analizowaniem (używam dateutil do analizowania, jeśli to ma znaczenie), ale to wydaje się niesamowicie niewyraźne.

Próbowałem tego również w Pythonie 2.6 i Pythonie 2.7, z tymi samymi wynikami.

Kontekst

Piszę parser dla niektórych plików danych. Jest stary format, który muszę obsługiwać, gdy ciąg daty nie ma wskaźnika strefy czasowej. Naprawiłem już źródło danych, ale nadal muszę obsługiwać starszy format danych. Jednorazowa konwersja starszych danych nie jest opcją z różnych powodów biznesowych BS. Chociaż ogólnie nie podoba mi się pomysł zaprogramowania domyślnej strefy czasowej, w tym przypadku wydaje się to najlepsza opcja. Wiem z wystarczającą pewnością, że wszystkie starsze dane, o których mowa, znajdują się w UTC, więc jestem gotów zaakceptować ryzyko niedotrzymania tego w tym przypadku.

Mark Tozzi
źródło
1
unaware.replace()powróciłby, Nonegdyby modyfikował unawareobiekt w miejscu. REPL pokazuje, że .replace()zwraca tutaj nowy datetimeobiekt.
jfs
3
Czego potrzebowałem, kiedy tu przybyłem:import datetime; datetime.datetime.now(datetime.timezone.utc)
Martin Thoma,
1
@MartinThoma Chciałbym użyć nazwanego tzarg, aby był bardziej czytelny:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus

Odpowiedzi:

593

Ogólnie rzecz biorąc, aby naiwna strefa czasowa uwzględniała strefę czasową, skorzystaj z metody 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

W strefie czasowej UTC nie jest tak naprawdę konieczne, localizeponieważ nie ma obliczenia czasu letniego do obsługi:

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

Pracuje. ( .replacezwraca nowy czas danych; nie zmienia się unaware).

unutbu
źródło
10
Cóż, czuję się głupio. Zastąp zwraca nową datę / godzinę. Mówi to również tutaj, w dokumentach, i zupełnie mi tego brakowało. Dzięki, właśnie tego szukałem.
Mark Tozzi
2
„Zastąp zwraca nową datę / godzinę”. Tak. Wskazówka, którą daje REPL, polega na tym, że pokazuje ona zwróconą wartość. :)
Karl Knechtel - poza domem
dzięki, użyłem od dt_aware do unware dt_unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Sérgio
4
jeśli strefą czasową nie jest UTC, nie używaj konstruktora bezpośrednio: aware = datetime(..., tz)użyj .localize()zamiast tego.
jfs
1
Warto wspomnieć, że czas lokalny może być niejednoznaczny. tz.localize(..., is_dst=None)twierdzi, że tak nie jest.
jfs
166

Wszystkie te przykłady wykorzystują moduł zewnętrzny, ale możesz osiągnąć ten sam wynik, korzystając tylko z modułu datetime, jak również przedstawiono w tej odpowiedzi 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'

Mniej zależności i brak problemów z pytz.

UWAGA: Jeśli chcesz używać tego z python3 i python2, możesz również użyć tego do importu strefy czasowej (na stałe dla 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
źródło
10
Bardzo dobra odpowiedź na zapobieganie pytzproblemom, cieszę się, że przewinąłem trochę w dół! Naprawdę nie chciałem się zmagać z pytzmoimi zdalnymi serwerami :)
Tregoreg
7
Pamiętaj, że from datetime import timezonedziała w py3, ale nie py2.7.
7yl4r
11
Należy pamiętać, że dt.replace(tzinfo=timezone.utc)zwraca nową datę i godzinę, nie zmienia się dtw miejscu. (Będę edytować, aby to pokazać).
Blairg23,
2
W jaki sposób możesz zamiast timezone.utc podać inną strefę czasową jako ciąg (np. „America / Chicago”)?
bumpkin
2
@bumpkin lepiej późno niż wcale, chyba:tz = pytz.timezone('America/Chicago')
Florian
82

Korzystałem z dt_aware do dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

i dt_unware do dt_aware

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

ale odpowiedź wcześniej jest również dobrym rozwiązaniem.

Sérgio
źródło
2
możesz użyć, localtz.localize(dt_unware, is_dst=None)aby zgłosić wyjątek, jeśli dt_unwarereprezentuje nieistniejący lub niejednoznaczny czas lokalny (uwaga: w poprzedniej wersji Twojej odpowiedzi nie było takiego problemu, gdzie localtzbył UTC, ponieważ UTC nie ma przejść DST
jfs
@JF Sebastian, zastosowano pierwszy komentarz
Sérgio
1
Doceniam pokazanie obu kierunków konwersji.
Christian Long
41

Używam tego oświadczenia w Django do konwersji nieświadomego czasu na świadomą:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())
Googol
źródło
2
Podoba mi się to rozwiązanie (+1), ale zależy ono od Django, którego nie szukali (-1). =)
mkoistinen,
3
W rzeczywistości nie jest to drugi argument. Domyślnym argumentem (None) będzie oznaczać lokalna strefa czasowa jest stosowana w sposób dorozumiany. To samo z DST (który jest trzecim argumentem
Oli
14

Zgadzam się z poprzednimi odpowiedziami i jest w porządku, jeśli możesz zacząć od UTC. Ale myślę, że jest to również częsty scenariusz dla ludzi pracujących z wartością świadomą tz, która ma datę / godzinę, która ma lokalną strefę czasową inną niż UTC.

Gdybyś po prostu używał nazwy, prawdopodobnie można by wywnioskować, że funkcja replace () będzie miała zastosowanie i utworzy odpowiedni obiekt uwzględniający datę i godzinę. Nie o to chodzi.

replace (tzinfo = ...) wydaje się zachowywać losowo . Jest więc bezużyteczny. Nie używaj tego!

localize to właściwa funkcja do użycia. Przykład:

localdatetime_aware = tz.localize(datetime_nonaware)

Lub bardziej kompletny przykład:

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

daje mi wartość strefy czasowej dla aktualnego czasu lokalnego:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
paolov
źródło
3
To wymaga więcej pozytywnych opinii, próba zrobienia replace(tzinfo=...)w strefie czasowej innej niż UTC zepsuje twoją datę i godzinę . Mam na przykład -07:53zamiast -08:00. Zobacz stackoverflow.com/a/13994611/1224827
Blairg23
Czy możesz podać powtarzalny przykład replace(tzinfo=...)nieoczekiwanego zachowania?
xjcl
11

Użyj, dateutil.tz.tzlocal()aby uzyskać strefę czasową w użyciu datetime.datetime.now()i 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())

Zauważ, że datetime.astimezonenajpierw przekształci Twój datetimeobiekt w UTC, a następnie w strefę czasową, co jest równoznaczne datetime.replacez wywołaniem z oryginalną informacją o strefie czasowej None.

Ahmet
źródło
1
Jeśli chcesz zrobić to UTC:.replace(tzinfo=dateutil.tz.UTC)
Martin Thoma
2
Jeden import mniej i tylko:.replace(tzinfo=datetime.timezone.utc)
kubańczyk
9

Ten ujednolica @ Sergio i @ unutbu za odpowiedzi . Będzie „po prostu działał” z pytz.timezoneobiektem lub ciągiem strefy czasowej 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) 

Wygląda na to, co należy zrobić datetime.localize()(lub .inform()lub .awarify()), zaakceptować zarówno łańcuchy, jak i obiekty strefy czasowej dla argumentu tz i ustawić domyślnie na UTC, jeśli nie określono strefy czasowej.

płyty grzewcze
źródło
1
Dzięki, pomogło mi to „oznaczyć” surowy obiekt daty / godziny jako „UTC” bez uprzedniego przyjmowania przez system czasu lokalnego, a następnie ponownego obliczania wartości!
Nikhil VJ,
2

Python 3.9 dodaje zoneinfomoduł, więc teraz potrzebna jest tylko standardowa biblioteka!

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

Dołącz strefę czasową:

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

Dołącz lokalną strefę czasową systemu:

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

Następnie jest poprawnie konwertowany na inne strefy czasowe:

>>> 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 dostępnych stref czasowych w Wikipedii


Istnieje backport pozwalający na użycie w Pythonie od 3.6 do 3.8 :

sudo pip install backports.zoneinfo

Następnie:

from backports.zoneinfo import ZoneInfo
xjcl
źródło
0

W formie odpowiedzi Unutbu; Stworzyłem moduł narzędziowy, który obsługuje takie rzeczy, z bardziej intuicyjną składnią. Może być instalowany z pipem.

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')
Żółwie są słodkie
źródło
0

dla tych, którzy chcą po prostu utworzyć strefę czasową uwzględniającą strefę czasową

import datetime
import pytz

datetime.datetime(2019, 12, 7, tzinfo=pytz.UTC)
Harry Moreno
źródło
0

całkiem nowy w Pythonie i napotkałem ten sam problem. Uważam to rozwiązanie za dość proste i dla mnie działa dobrze (Python 3.6):

unaware=parser.parse("2020-05-01 0:00:00")
aware=unaware.replace(tzinfo=tz.tzlocal()).astimezone(tz.tzlocal())
ilmatte
źródło
0

Przełączanie między strefami czasowymi

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
źródło