Tüm yazılar
Araçlar 6 dk okuma

Otomasyonda tarih ve saat tuzakları: saat dilimi, UTC ve Türkçe tarih ayrıştırma

Python otomasyonlarında saat dilimi, UTC depolama, Türkçe tarih ve Excel seri numarası ayrıştırma ve dönem sınırı hesabı: sessiz rapor hatalarını önleyen dört kural.

#python #tarih-saat #saat-dilimi #veri-isleme #otomasyon

Otomasyonda tarih ve saat tuzakları, en sessiz hata kaynaklarından biridir: betik çalışır, hata vermez, rapor üretilir; ama "dün" dediğiniz aralık sunucunun saatine göre üç saat kaymıştır, Excel'den gelen tarih bir gün geri gitmiştir ya da "3 Ekim 2026" yazan hücre ayrıştırılamadığı için satır sessizce atlanmıştır. Bu hatalar test ortamında görünmez, çünkü geliştirici bilgisayarı ile sunucu çoğu zaman farklı saat dilimindedir.

Küçük ve orta ölçekli işletmelerde veri, tedarikçi e-tablolarından, muhasebe programı dışa aktarımlarından, API yanıtlarından ve sunucu loglarından gelir. Her kaynağın tarih biçimi farklıdır ve çoğu saat dilimi bilgisi taşımaz. Bu yazı, Python standart kütüphanesiyle bu kaynakları tek bir doğru temsile indirgeyen dört kuralı anlatır: farkında nesne kullan, UTC ve ISO 8601 ile sakla, Türkçe ve Excel biçimlerini bilinçli ayrıştır, dönem sınırlarını yerel saatte hesaplayıp UTC'ye çevir.

Örnekler Python 3.9 ve üzeri sürümlerde dış bağımlılık olmadan çalışır; saat dilimi veri tabanı için standart kütüphanedeki zoneinfo modülü yeterlidir. Her örneği yazının sonunda anlatılan yöntemle kendi verinizde deneyebilirsiniz.

Naif ve farkında tarih: ilk kural

Naif datetime, saat dilimi bilgisi taşımayan bir tarih-saat nesnesidir; farkında (aware) datetime ise hangi dilimde olduğunu bilir. Naif bir değer "09:30" der ama hangi 09:30 olduğunu söylemez: İstanbul'daki mi, sunucunun kurulu olduğu Frankfurt'taki mi, yoksa UTC mi. Otomasyonun ilk kuralı, sisteme giren her tarih-saati en kısa yoldan farkında hale getirmek ve naif değerleri hiçbir hesaba sokmamaktır.

Python bu konuda yardımcıdır: naif ile farkında değeri karşılaştırmaya kalkarsanız TypeError alırsınız. Bu hata bir engel değil, yanlış varsayımı erken yakalayan bir güvenlik ağıdır. Aşağıdaki örnek iki tür nesneyi, aralarındaki dönüşümü ve karşılaştırma hatasını gösterir.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

ISTANBUL = ZoneInfo("Europe/Istanbul")

naif = datetime(2026, 10, 3, 9, 30)                 # saat dilimi yok: belirsiz
farkinda = datetime(2026, 10, 3, 9, 30, tzinfo=ISTANBUL)

print(naif.isoformat())        # 2026-10-03T09:30:00
print(farkinda.isoformat())    # 2026-10-03T09:30:00+03:00

# Depolama icin UTC'ye cevir, gosterim icin geri al
utc = farkinda.astimezone(timezone.utc)
print(utc.isoformat())         # 2026-10-03T06:30:00+00:00
print(utc.astimezone(ISTANBUL).strftime("%d.%m.%Y %H:%M"))

# Naif ile farkindayi karsilastirmak hata verir; bu iyi bir seydir
try:
    print(naif < farkinda)
except TypeError as hata:
    print("TypeError:", hata)

# "Simdi" her zaman farkinda alinir
simdi = datetime.now(timezone.utc)
print(simdi.tzinfo)

Türkiye için işi kolaylaştıran bir ayrıntı var: 2016 tarihli Bakanlar Kurulu kararına göre ülke yaz saati uygulamasını bırakıp kalıcı olarak UTC+3'te kaldı. Yine de sabit "+3 saat" ekleyerek dönüşüm yapmayın; Europe/Istanbul dilimini kullanın. Geçmiş tarihli kayıtlarda yaz saati farkı vardır ve karar bir gün değişirse kodunuz işletim sisteminin saat dilimi güncellemesiyle kendiliğinden düzelir.

UTC ve ISO 8601: depolama ve değişim biçimi

UTC ile depolama, her tarih-saati veri tabanına ve dosyalara saat dilimi farkından arındırılmış tek bir referansla yazmak demektir; ISO 8601 ise bu değerin metin biçimidir. Kural basittir: içeride UTC, kullanıcıya gösterirken yerel saat. Böylece sunucu taşınsa, konteyner UTC'de çalışsa ya da ikinci bir ülkede şube açılsa kayıtların anlamı değişmez.

ISO 8601 biçimini seçmenin pratik gerekçesi sıralanabilirliktir: 2026-10-03T06:30:00+00:00 gibi metinler alfabetik sıralandığında kronolojik sıralanır. SQLite'ın yerel tarih türü olmadığından bu özellik doğrudan işe yarar; SQLite ile küçük veri boru hatları yazısında anlattığım yapıda tarih sütunları tam bu biçimde tutulur ve aralık sorguları metin karşılaştırmasıyla doğru çalışır.

Depolama için şu kuralları öneriyorum:

  • Sütun adına birimi yazın: olusturma_utc. Okuyan kişi dönüşüm gerekip gerekmediğini sütun adından anlar.
  • Sadece tarih taşıyan alanları (fatura tarihi, doğum tarihi) saat dilimine çevirmeyin; 2026-10-03 olarak bırakın. Takvim tarihi bir an değil, bir gündür.
  • Dışarıya veri verirken her zaman sapmayı yazın (+03:00 ya da Z); sapma olmayan ISO metni alıcı için naif demektir.
  • Python sürümüne dikkat edin: datetime.fromisoformat sondaki Z harfini 3.11 ve sonrasında kabul eder; daha eski sürümlerde Z yerine +00:00 yazmanız gerekir.
  • Unix zaman damgası (epoch) ile çalışıyorsanız datetime.fromtimestamp(x, tz=timezone.utc) kullanın; dilim vermezseniz sonuç sunucunun yerel saatine göre naif gelir.

Aynı kural log satırları için de geçerlidir. Betik UTC'de, veri tabanı yerel saatte, zamanlayıcı başka bir dilimde yazarsa bir hatanın sırasını kurmak saatler alır. Log biçimlendiricisine sapma içeren ISO 8601 zaman damgası verin; böylece farklı makinelerden gelen satırlar tek bir zaman çizgisine dizilebilir.

Türkçe tarihleri ve Excel seri numaralarını ayrıştırmak

Türkçe tarih ayrıştırma, "3 Ekim 2026" ya da "03.10.2026" gibi yerel biçimleri standart bir tarih nesnesine çevirme işidir ve iki ayrı tuzak taşır. İlki ay adlarıdır: strptime ay adını işletim sisteminin yerel ayarına göre bekler, sunucuda Türkçe yerel ayar kurulu değilse "Ekim" tanınmaz. İkincisi belirsizliktir: "03.10.2026" Türkiye'de 3 Ekim, ABD biçiminde 10 Mart demektir.

Yerel ayara bağımlı olmayan çözüm, ay adlarını kendi sözlüğünüzle eşlemek ve kabul ettiğiniz biçimleri açıkça sıralamaktır. Belirsiz biçimi "kaynak hangi ülkeden geliyorsa o" kuralıyla çözün ve bu kuralı kaynak başına ayara bağlayın; betik tahmin etmesin.

Excel'den gelen tarihler ayrı bir dünyadır. Excel tarihleri 1899-12-30 başlangıçlı gün sayısı olarak tutar; 46298.5 değeri 3 Ekim 2026 öğle demektir; ondalık kısım günün kesrini, yani saati taşır. openpyxl hücre biçimi tarihse bunu kendiliğinden datetime'a çevirir, ama hücre "sayı" biçimindeyse ham seri numarası gelir ve CSV dışa aktarımında da çoğu zaman bu sayı görünür. Her iki durumu da ele alan örnek aşağıdadır.

from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo

AYLAR = {
    "ocak": 1, "şubat": 2, "subat": 2, "mart": 3, "nisan": 4, "mayıs": 5, "mayis": 5,
    "haziran": 6, "temmuz": 7, "ağustos": 8, "agustos": 8, "eylül": 9, "eylul": 9,
    "ekim": 10, "kasım": 11, "kasim": 11, "aralık": 12, "aralik": 12,
}
EXCEL_BASLANGIC = datetime(1899, 12, 30)


def turkce_tarih(metin):
    """'3 Ekim 2026', '03.10.2026' ve '2026-10-03' biçimlerini date'e çevirir."""
    metin = metin.strip().lower()
    parcalar = metin.replace(",", " ").split()
    if len(parcalar) == 3 and parcalar[1] in AYLAR:
        return date(int(parcalar[2]), AYLAR[parcalar[1]], int(parcalar[0]))
    for bicim in ("%d.%m.%Y", "%d/%m/%Y", "%Y-%m-%d"):
        try:
            return datetime.strptime(metin, bicim).date()
        except ValueError:
            continue
    raise ValueError("Tanınmayan tarih biçimi: %r" % metin)


def excel_seri(sayi, saat_dilimi="Europe/Istanbul"):
    """Excel'in 46298.5 gibi seri numarasını saat dilimi bilen datetime'a çevirir."""
    yerel = EXCEL_BASLANGIC + timedelta(days=float(sayi))
    return yerel.replace(tzinfo=ZoneInfo(saat_dilimi))


if __name__ == "__main__":
    for ornek in ("3 Ekim 2026", "03.10.2026", "2026-10-03", "12 Kasım 2025"):
        print(ornek, "->", turkce_tarih(ornek))
    print(excel_seri(46298.5).isoformat())

Tanınmayan biçimde hata fırlatmak bilinçli bir tercihtir. Sessizce None döndüren ayrıştırıcı, bozuk satırı raporun dışına atar ve kimse fark etmez. Boru hattının kapısında şema doğrulaması yapıyorsanız, bu hatayı doğrulama katmanında yakalayıp reddedilen satırları ayrı bir dosyaya yazdırın.

Zamanlayıcı, süre ölçümü ve dönem sınırları

Dönem sınırı hesabı, "dünün raporu" ya da "geçen ayın satışları" gibi ifadelerin hangi UTC aralığına denk geldiğini belirleme işidir ve saat dilimi hatalarının en çok biriktiği yerdir. Doğru sıra şudur: dönemi yerel takvimde tanımla, sınırlarını farkında nesnelerle kur, sonra UTC'ye çevirip sorguya ver. Sunucu UTC'de çalışıyorsa "dün" gece yarısı Türkiye'de 03:00'tür; yerelde hesaplamazsanız raporunuz üç saatlik kayıtla eksik ya da fazla çıkar.

Zamanlayıcının kendisi de bir tuzak kaynağıdır. cron, işletim sisteminin saat dilimine göre çalışır ve Docker konteynerleri varsayılan olarak UTC'dedir; "her gün 08:00" yazdığınız iş İstanbul'da 11:00'de koşabilir. Çözüm, sistem saat dilimine güvenmek yerine TZ=Europe/Istanbul ortam değişkenini crontab başına ya da konteyner tanımına açıkça yazmaktır. Kilit ve yeniden çalıştırma tasarımını zamanlanmış iş tasarımı yazısında ayrıntılı anlattım.

from datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo


def donem_siniri(gun, saat_dilimi="Europe/Istanbul"):
    """Verilen yerel günün [başlangıç, bitiş) sınırlarını UTC olarak döndürür."""
    dilim = ZoneInfo(saat_dilimi)
    baslangic = datetime.combine(gun, time.min, tzinfo=dilim)
    bitis = baslangic + timedelta(days=1)
    return baslangic.astimezone(timezone.utc), bitis.astimezone(timezone.utc)


def ay_sonu(gun):
    """Ayın son gününü döndürür; 31 Ocak + 1 ay tuzağına düşmez."""
    sonraki_ay = (gun.replace(day=1) + timedelta(days=32)).replace(day=1)
    return sonraki_ay - timedelta(days=1)


if __name__ == "__main__":
    dun = (datetime.now(ZoneInfo("Europe/Istanbul")) - timedelta(days=1)).date()
    bas, bit = donem_siniri(dun)
    print("Dünün UTC aralığı:", bas.isoformat(), "->", bit.isoformat())
    print("SQL:", "WHERE olusturma_utc >= ? AND olusturma_utc < ?", (bas.isoformat(), bit.isoformat()))
    print("Ocak sonu:", ay_sonu(datetime(2026, 1, 31).date()))
    print("Şubat sonu:", ay_sonu(datetime(2026, 2, 10).date()))

Üç ayrıntı daha hata biriktirir:

  • Aralığın sonunu kapalı değil açık tutun: >= başlangıç ve < bitiş. "23:59:59" ile biten aralıklar milisaniyeli kayıtları kaçırır.
  • Süre ölçerken duvar saati değil time.monotonic() kullanın. Sistem saati NTP ile düzeltildiğinde duvar saatine dayalı süre eksi çıkabilir.
  • Ay eklemeyi elle yapmayın; "31 Ocak + 1 ay" ifadesinin doğal karşılığı yoktur. Yukarıdaki ay_sonu gibi takvim kurallarını açıkça yazan küçük yardımcılar kullanın.

Sonuç: dört kuralı tek denetim listesine indirmek

Tarih ve saat tuzaklarından korunmanın yolu, her veri kaynağının girişinde aynı dört soruyu sormaktır: değer farkında mı, UTC ve ISO 8601 ile mi saklanıyor, yerel biçimler bilinçli bir sözlükle mi ayrıştırılıyor, dönem sınırları yerel takvimde kurulup UTC'ye mi çevriliyor. Bu sorulara "evet" diyemediğiniz her nokta bir gün sessiz bir rapor hatasına dönüşür.

İlk adım olarak mevcut betiklerinizde datetime.now() ve datetime.utcnow() çağrılarını arayın; her ikisi de naif değer üretir ve bu yazıdaki datetime.now(timezone.utc) biçimiyle değiştirilmelidir. Ardından veri tabanındaki tarih sütunlarının adına birimi ekleyin ve ayrıştırıcıları tanımadığı biçimde hata fırlatacak hale getirin.

Veri akışınızda birden çok kaynak ve saat dilimi varsa ve hangi kuralın nereye uygulanacağını birlikte çıkarmak isterseniz iletişim bölümünden yazabilirsiniz.