Comment rendre un fuseau horaire datetime inconnu en python

508

Ce que je dois faire

J'ai un objet datetime non compatible avec le fuseau horaire, auquel je dois ajouter un fuseau horaire afin de pouvoir le comparer avec d'autres objets datetime sensibles au fuseau horaire. Je ne veux pas convertir l'intégralité de mon application en fuseau horaire sans être au courant de ce cas hérité.

Ce que j'ai essayé

Tout d'abord, pour illustrer le problème:

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

J'ai d'abord essayé l'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
>>>

Il n'est pas très surprenant que cela ait échoué, car il essaie en fait de faire une conversion. Remplacer semblait être un meilleur choix (selon Python: comment obtenir une valeur de datetime.today () qui est "sensible au fuseau horaire"? ):

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

Mais comme vous pouvez le voir, replace semble définir le tzinfo, mais pas rendre l'objet conscient. Je me prépare à retravailler la chaîne d'entrée pour avoir un fuseau horaire avant de l'analyser (j'utilise dateutil pour l'analyse, si cela importe), mais cela semble incroyablement délicat.

De plus, j'ai essayé cela à la fois en python 2.6 et en python 2.7, avec les mêmes résultats.

Le contexte

J'écris un analyseur pour certains fichiers de données. Il y a un ancien format que je dois prendre en charge où la chaîne de date n'a pas d'indicateur de fuseau horaire. J'ai déjà corrigé la source de données, mais je dois toujours prendre en charge le format de données hérité. Une conversion unique des données héritées n'est pas une option pour diverses raisons commerciales BS. Bien qu'en général, je n'aime pas l'idée de coder en dur un fuseau horaire par défaut, dans ce cas, cela semble être la meilleure option. Je sais avec une confiance raisonnable que toutes les anciennes données en question sont en UTC, donc je suis prêt à accepter le risque de ne pas y parvenir dans ce cas.

Mark Tozzi
la source
1
unaware.replace()retournerait Nones'il modifiait l' unawareobjet en place. Le REPL montre que .replace()renvoie un nouvel datetimeobjet ici.
jfs
3
Ce dont j'avais besoin quand je suis venu ici:import datetime; datetime.datetime.now(datetime.timezone.utc)
Martin Thoma
1
@MartinThoma J'utiliserais l' tzargument nommé pour être plus lisible:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus

Réponses:

595

En général, pour rendre un fuseau horaire naïf sensible au fuseau horaire, utilisez la méthode 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

Pour le fuseau horaire UTC, il n'est pas vraiment nécessaire d'utiliser localizecar il n'y a pas de calcul d'heure d'été pour gérer:

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

travaux. ( .replacerenvoie un nouveau datetime; il ne modifie pas unaware.)

unutbu
la source
10
Eh bien, je me sens bête. Replace renvoie une nouvelle date / heure. Cela dit aussi dans les documents, et ça m'a complètement manqué. Merci, c'est exactement ce que je cherchais.
Mark Tozzi
2
"Replace renvoie une nouvelle date / heure." Oui. L'indice que le REPL vous donne est qu'il vous montre la valeur retournée. :)
Karl Knechtel - loin de chez soi
merci, j'ai utilisé de dt_aware à unware dt_unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Sérgio
4
si le fuseau horaire n'est pas UTC, n'utilisez pas directement le constructeur:, aware = datetime(..., tz)utilisez .localize()plutôt.
jfs
1
Il convient de mentionner que l'heure locale peut être ambiguë. tz.localize(..., is_dst=None)affirme que ce n'est pas le cas.
jfs
167

Tous ces exemples utilisent un module externe, mais vous pouvez obtenir le même résultat en utilisant uniquement le module datetime, comme également présenté dans cette réponse 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'

Moins de dépendances et pas de problèmes pytz.

REMARQUE: Si vous souhaitez l'utiliser avec python3 et python2, vous pouvez également l'utiliser pour l'importation de fuseau horaire (codé en dur pour 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
la source
10
Très bonne réponse pour éviter les pytzproblèmes, je suis content d'avoir fait défiler un peu! Je ne voulais pas aborder avec pytzsur mes serveurs distants en effet :)
Tregoreg
7
Notez que cela from datetime import timezonefonctionne dans py3 mais pas py2.7.
7yl4r
11
Vous devez noter que dt.replace(tzinfo=timezone.utc)renvoie une nouvelle date / heure, elle ne se modifie pas dtsur place. (Je vais modifier pour montrer cela).
Blairg23
2
Comment pourriez-vous, au lieu d'utiliser timezone.utc, fournir un fuseau horaire différent sous forme de chaîne (par exemple "America / Chicago")?
bumpkin
2
@bumpkin vaut mieux tard que jamais, je suppose:tz = pytz.timezone('America/Chicago')
Florian
83

J'ai utilisé de dt_aware à dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

et dt_unware à dt_aware

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

mais répondre avant est aussi une bonne solution.

Sérgio
la source
2
vous pouvez utiliser localtz.localize(dt_unware, is_dst=None)pour déclencher une exception s'il dt_unwarereprésente une heure locale inexistante ou ambiguë (remarque: il n'y avait aucun problème de ce genre dans la révision précédente de votre réponse où localtzétait UTC car UTC n'a pas de transition DST
jfs
@JF Sebastian, premier commentaire appliqué
Sérgio
1
Je vous remercie de montrer les deux directions de la conversion.
Christian Long
42

J'utilise cette déclaration dans Django pour convertir une heure inconnue en une heure consciente:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())
Googol
la source
2
J'aime bien cette solution (+1), mais elle dépend de Django, qui n'est pas ce qu'ils cherchaient (-1). =)
mkoistinen
3
Vous n'avez pas vraiment le deuxième argument. L' argument par défaut (Aucun) signifie que le fuseau horaire local est utilisé implicitement. Idem avec le DST (qui est le troisième argument_
Oli
14

Je suis d'accord avec les réponses précédentes, et ça va si vous êtes d'accord pour commencer en UTC. Mais je pense que c'est aussi un scénario courant pour les gens de travailler avec une valeur consciente de tz qui a un datetime qui a un fuseau horaire local non UTC.

Si vous deviez simplement vous rendre par son nom, on en déduirait probablement que replace () sera applicable et produira le bon objet sensible à l'heure. Ce n'est pas le cas.

le remplacement (tzinfo = ...) semble être aléatoire dans son comportement . C'est donc inutile. Ne l'utilisez pas!

localiser est la fonction correcte à utiliser. Exemple:

localdatetime_aware = tz.localize(datetime_nonaware)

Ou un exemple plus complet:

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

me donne une valeur datetime sensible au fuseau horaire de l'heure locale actuelle:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
paolov
la source
3
Cela nécessite plus de votes positifs, essayer de le faire replace(tzinfo=...)sur un fuseau horaire autre que UTC va salir votre datetime. J'ai eu -07:53au lieu de -08:00par exemple. Voir stackoverflow.com/a/13994611/1224827
Blairg23
Pouvez-vous donner un exemple reproductible d' replace(tzinfo=...)un comportement inattendu?
xjcl
11

Utilisez dateutil.tz.tzlocal()pour obtenir le fuseau horaire dans votre utilisation de datetime.datetime.now()et 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())

Notez que datetime.astimezonecela convertira d'abord votre datetimeobjet en UTC puis dans le fuseau horaire, ce qui revient à appeler datetime.replaceavec les informations de fuseau horaire d'origine None.

Ahmet
la source
1
Si vous voulez le faire UTC:.replace(tzinfo=dateutil.tz.UTC)
Martin Thoma
2
Une importation en moins et juste:.replace(tzinfo=datetime.timezone.utc)
kubanczyk
9

Cela codifie les réponses de @ Sérgio et @ unutbu . Il "fonctionnera simplement" avec un pytz.timezoneobjet ou une chaîne de fuseau horaire 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) 

Cela semble être ce que datetime.localize()(ou .inform()ou .awarify()) devrait faire, accepter à la fois les chaînes et les objets de fuseau horaire pour l'argument tz et par défaut à UTC si aucun fuseau horaire n'est spécifié.

plaques de cuisson
la source
1
Merci, cela m'a aidé à "marquer" un objet datetime brut en tant que "UTC" sans que le système suppose d'abord qu'il s'agit de l'heure locale et recalcule ensuite les valeurs!
Nikhil VJ
4

Python 3.9 ajoute le zoneinfomodule donc maintenant seule la bibliothèque standard est nécessaire!

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

Attachez un fuseau horaire:

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

Attachez le fuseau horaire local du système:

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

Par la suite, il est correctement converti en d'autres fuseaux horaires:

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

Liste Wikipedia des fuseaux horaires disponibles


Windows n'a pas de base de données de fuseau horaire système, donc ici un package supplémentaire est nécessaire:

pip install tzdata  

Il existe un backport pour permettre une utilisation en Python 3.6 à 3.8 :

pip install backports.zoneinfo

Alors:

from backports.zoneinfo import ZoneInfo
xjcl
la source
1
sur Windows, vous devez égalementpip install tzdata
MrFuppes
@MrFuppes Merci pour le conseil! Je vais tester cela demain et à ma réponse. Connaissez-vous la situation sur Mac?
xjcl
Non, désolé, je n'ai pas de Mac à essayer. Je suppose que c'est comme sur Linux.
MrFuppes
0

Dans le format de la réponse d'unutbu; J'ai créé un module utilitaire qui gère des choses comme ça, avec une syntaxe plus intuitive. Peut être installé avec 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')
Les tortues sont mignonnes
la source
0

pour ceux qui veulent juste faire un datetime sensible au fuseau horaire

import datetime
import pytz

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

assez nouveau pour Python et j'ai rencontré le même problème. Je trouve cette solution assez simple et pour moi ça marche très bien (Python 3.6):

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

Changer de fuseau horaire

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