Cara membuat zona waktu datetime yang tidak disadari sadar dengan python

508

Apa yang harus saya lakukan

Saya memiliki objek waktu-waktu tidak sadar zona waktu, yang saya perlu menambahkan zona waktu agar dapat membandingkannya dengan objek waktu-waktu sadar-waktu-zona waktu lainnya. Saya tidak ingin mengonversi seluruh aplikasi saya ke zona waktu tanpa mengetahui untuk kasus warisan yang satu ini.

Apa yang saya coba

Pertama, untuk menunjukkan masalahnya:

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

Pertama, saya mencoba 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
>>>

Tidak terlalu mengejutkan bahwa ini gagal, karena sebenarnya mencoba melakukan konversi. Ganti sepertinya pilihan yang lebih baik (sesuai Python: Cara mendapatkan nilai datetime.today () yaitu "timezone aware"? ):

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

Tapi seperti yang Anda lihat, ganti tampaknya mengatur tzinfo, tetapi tidak membuat objek sadar. Saya sedang bersiap-siap untuk kembali ke doctoring string input untuk memiliki zona waktu sebelum parsing (saya menggunakan dateutil untuk parsing, jika itu penting), tapi itu tampaknya sangat muram.

Juga, saya sudah mencoba ini di kedua python 2.6 dan python 2.7, dengan hasil yang sama.

Konteks

Saya menulis parser untuk beberapa file data. Ada format lama yang saya perlukan untuk mendukung di mana string tanggal tidak memiliki indikator zona waktu. Saya sudah memperbaiki sumber data, tetapi saya masih perlu mendukung format data lawas. Konversi satu kali data legacy bukan pilihan untuk berbagai alasan BS bisnis. Sementara secara umum, saya tidak suka gagasan hard-coding zona waktu default, dalam hal ini sepertinya pilihan terbaik. Saya tahu dengan keyakinan yang masuk akal bahwa semua data lawas yang dimaksud adalah dalam UTC, jadi saya siap untuk menerima risiko gagal bayar itu dalam hal ini.

Mark Tozzi
sumber
1
unaware.replace()akan kembali Nonejika itu memodifikasi unawareobjek di tempat. REPL menunjukkan bahwa .replace()mengembalikan datetimeobjek baru di sini.
jfs
3
Apa yang saya butuhkan ketika saya datang ke sini:import datetime; datetime.datetime.now(datetime.timezone.utc)
Martin Thoma
1
@ MartinThoma Saya akan menggunakan nama tzarg agar lebih mudah dibaca:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus

Jawaban:

595

Secara umum, untuk membuat zona waktu sadar-naif yang naif, gunakan metode pelokalan :

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

Untuk zona waktu UTC, itu tidak perlu digunakan localizekarena tidak ada perhitungan waktu musim panas untuk menangani:

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

bekerja. ( .replacemengembalikan datetime baru; tidak diubah unaware.)

unutbu
sumber
10
Yah, saya merasa konyol. Ganti mengembalikan datetime baru. Ia mengatakan itu di dokumen juga, dan aku benar-benar ketinggalan. Terima kasih, itulah tepatnya yang saya cari.
Mark Tozzi
2
"Ganti mengembalikan datetime baru." Ya. Petunjuk bahwa REPL memberi Anda adalah itu menunjukkan Anda nilai yang dikembalikan. :)
Karl Knechtel - jauh dari rumah
terima kasih, saya telah menggunakan dari dt_aware ke unware dt_unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Sérgio
4
jika zona waktu bukan UTC maka jangan gunakan konstruktor secara langsung :, aware = datetime(..., tz)gunakan .localize()saja.
jfs
1
Perlu disebutkan bahwa waktu setempat mungkin ambigu. tz.localize(..., is_dst=None)menegaskan bahwa itu bukan.
jfs
167

Semua contoh ini menggunakan modul eksternal, tetapi Anda dapat mencapai hasil yang sama hanya menggunakan modul datetime, seperti juga disajikan dalam jawaban SO ini :

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'

Lebih sedikit ketergantungan dan tidak ada masalah pytz.

CATATAN: Jika Anda ingin menggunakan ini dengan python3 dan python2, Anda dapat menggunakan ini juga untuk impor zona waktu (hardcode untuk 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
sumber
10
Jawaban yang sangat bagus untuk mencegah pytzmasalah, saya senang saya gulir ke bawah sedikit! Memang tidak ingin menangani dengan pytzserver jauh saya :)
Tregoreg
7
Perhatikan bahwa from datetime import timezonebekerja di py3 tetapi tidak py2.7.
7yl4r
11
Anda harus mencatat bahwa dt.replace(tzinfo=timezone.utc)mengembalikan datetime baru, itu tidak berubah dtpada tempatnya. (Saya akan mengedit untuk menunjukkan ini).
Blairg23
2
Bagaimana mungkin Anda, alih-alih menggunakan timezone.utc, memberikan zona waktu yang berbeda sebagai string (mis. "Amerika / Chicago")?
bumpkin
2
@bumpkin lebih baik terlambat daripada tidak, kurasa:tz = pytz.timezone('America/Chicago')
Florian
83

Saya telah menggunakan dari dt_aware ke dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

dan dt_unware ke dt_aware

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

tetapi jawaban sebelumnya juga merupakan solusi yang baik.

Sérgio
sumber
2
Anda dapat menggunakan localtz.localize(dt_unware, is_dst=None)untuk mengajukan pengecualian jika dt_unwaremewakili waktu lokal yang tidak ada atau ambigu (catatan: tidak ada masalah seperti itu dalam revisi sebelumnya atas jawaban Anda di mana localtzUTC karena UTC tidak memiliki transisi DST
jfs
@JF Sebastian, komentar pertama diterapkan
Sérgio
1
Saya menghargai Anda menunjukkan kedua arah konversi.
Christian Long
42

Saya menggunakan pernyataan ini di Django untuk mengubah waktu yang tidak disadari menjadi sadar:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())
Googol
sumber
2
Saya memang suka solusi ini (+1), tetapi ini tergantung pada Django, yang bukan yang mereka cari (-1). =)
mkoistinen
3
Anda sebenarnya bukan argumen kedua. The argumen default (Tidak) akan berarti zona waktu lokal digunakan secara implisit. Sama dengan DST (yang merupakan argumen
ketiga_
14

Saya setuju dengan jawaban sebelumnya, dan tidak masalah jika Anda baik-baik saja memulai di UTC. Tetapi saya pikir ini juga merupakan skenario umum bagi orang untuk bekerja dengan nilai sadar yang memiliki waktu yang memiliki zona waktu lokal non UTC.

Jika Anda hanya pergi dengan nama, seseorang mungkin akan menyimpulkan ganti () akan berlaku dan menghasilkan objek sadar datetime yang tepat. Ini bukan kasusnya.

ganti (tzinfo = ...) tampaknya acak dalam perilakunya . Karena itu tidak berguna. Jangan gunakan ini!

pelokalan adalah fungsi yang benar untuk digunakan. Contoh:

localdatetime_aware = tz.localize(datetime_nonaware)

Atau contoh yang lebih lengkap:

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

memberi saya nilai data waktu sadar zona waktu saat ini:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
paolov
sumber
3
Ini membutuhkan lebih banyak upvote, coba lakukan replace(tzinfo=...)di zona waktu selain UTC akan merusak waktu Anda. Saya mendapatkan -07:53bukannya -08:00misalnya. Lihat stackoverflow.com/a/13994611/1224827
Blairg23
Bisakah Anda memberikan contoh yang dapat direproduksi tentang replace(tzinfo=...)perilaku yang tidak terduga?
xjcl
11

Gunakan dateutil.tz.tzlocal()untuk mendapatkan zona waktu dalam penggunaan datetime.datetime.now()dan 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())

Perhatikan bahwa datetime.astimezonepertama-tama akan mengonversi datetimeobjek Anda ke UTC kemudian menjadi zona waktu, yang sama dengan menelepon datetime.replacedengan informasi zona waktu yang asli None.

Ahmet
sumber
1
Jika Anda ingin membuatnya UTC:.replace(tzinfo=dateutil.tz.UTC)
Martin Thoma
2
Satu impor lebih sedikit dan hanya:.replace(tzinfo=datetime.timezone.utc)
kubanczyk
9

Ini mengkodifikasikan jawaban @ Sérgio dan @ unutbu . Ini akan "hanya bekerja" dengan pytz.timezoneobjek atau string Zona Waktu 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) 

Ini terlihat seperti apa yang datetime.localize()(atau .inform()atau .awarify()) harus dilakukan, menerima objek string dan zona waktu untuk argumen tz dan default ke UTC jika tidak ada zona waktu yang ditentukan.

hobs
sumber
1
Terima kasih, ini membantu saya "merek" objek datetime mentah sebagai "UTC" tanpa sistem pertama-tama menganggapnya sebagai waktu lokal dan kemudian menghitung ulang nilai-nilai!
Nikhil VJ
4

Python 3.9 menambahkan zoneinfomodul jadi sekarang hanya perpustakaan standar yang diperlukan!

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

Lampirkan zona waktu:

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

Pasang zona waktu lokal sistem:

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

Selanjutnya dikonversi dengan benar ke zona waktu lain:

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

Daftar zona waktu Wikipedia yang tersedia


Ada backport yang memungkinkan penggunaan dalam Python 3.6 hingga 3.8 :

sudo pip install backports.zoneinfo

Kemudian:

from backports.zoneinfo import ZoneInfo
xjcl
sumber
1
pada Windows, Anda juga perlupip install tzdata
MrFuppes
@ McFuppes Terima kasih atas tipnya! Saya akan menguji ini besok dan itu untuk jawaban saya. Apakah Anda tahu apa situasinya pada Mac?
xjcl
tidak, maaf, tidak ada Mac di sekitar untuk mencoba. Dugaan saya adalah seperti di Linux.
MrFuppes
0

Dalam format jawaban unutbu; Saya membuat modul utilitas yang menangani hal-hal seperti ini, dengan sintaks yang lebih intuitif. Dapat diinstal dengan 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')
Turtles Are Cute
sumber
0

bagi mereka yang hanya ingin membuat waktu sadar zona waktu

import datetime
import pytz

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

cukup baru untuk Python dan saya mengalami masalah yang sama. Saya menemukan solusi ini cukup sederhana dan bagi saya itu berfungsi dengan baik (Python 3.6):

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

Berganti di antara zona waktu

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
sumber