İçeriğe geç
academia.sh

Ders 14 / 16

Dosya İşlemleri

Aynı 41 bayt ikili kipte 41 uzunluğunda bir bayt dizisi, metin kipinde utf-8 ile 34 karakterlik bir dizgi verir; yanlış kodlama açma değil okuma anında kırılır ve latin-1 hiç kırılmayıp sessizce 41 yanlış karakter üretir.

İçindekiler

Önceki dersin on iki kaydı bellekte hazır duruyordu. deger alanı zaten bir dizgiydi; tek iş onu sayıya çevirmek ve alanın sınırlarına vurmaktı. Alan kökü işe yaradı çünkü hatanın kaynağı alanın kendisiydi.

Kayıtlar bir dosyadan geldiğinde araya bir katman daha girer. Dosyada duran şey ne dizgidir ne sayı — bayttır. Baytın dizgiye çevrilmesi bir kodlama kararıdır ve o karar yanlışsa hata alan katmanına hiç ulaşmaz; daha okuma sırasında ortaya çıkar. Dosya, kodlama ve bayt kavramları Bilgisayarlar Nasıl Çalışır kursunda kuruldu; orada kavram kuruldu, burada Python’un iki dosya kipinin aynı baytlarda ne verdiği ölçülüyor.

İki Kip, İki Tür

open iki kipte çalışır ve aralarındaki fark bir seçenek değil, dönen nesnenin türüdür.

İkili kip ("rb", "wb") bayt dizisi verir. Dosyada ne varsa odur; hiçbir çeviri yapılmaz. Uzunluk bayt sayısıdır.

Metin kipi ("r", "w") dizgi verir. Araya bir çözücü girer: baytlar bir kodlamaya göre karakterlere çevrilir ve satır sonları çevrilir. Uzunluk karakter sayısıdır.

Metin kipinde encoding verilmezse yorumlayıcı bir öntanımlı seçer ve o öntanımlı çalıştığı ortama bağlıdır. Aynı dosya, aynı program, başka bir ortam — başka sonuç. Bu dersin bütün ölçümleri kodlamayı açıkça yazar; ölçülebilir olmasının koşulu budur.

Aynı Baytlar, Ayrı Kodlamalar

Ölçüm tek bir metinle başlar: Türkçe harfler, bir derece işareti ve bir uzun çizgi içeren bir ölçüm satırı. Bu metin utf-8 ile kodlanır ve elde edilen baytlar beş ayrı kodlamayla çözülmeye çalışılır.

Ölçümün varsayımları:

  • HD19 — Bütün ölçüm tek bir bayt dizisi üzerinde yapılır; dizi metinden kodlanarak üretilir, elle yazılmaz.
  • HD20 — Kâhin kaynak metnin kendisidir: çözülen dizgi kaynakla özdeş mi diye sorulabilir, çünkü kaynağı biz ürettik.
  • HD21 — “Kırılan bayt konumu” istisna nesnesinin kendi alanından okunur; hata iletisi ayrıştırılmaz.
  • HD22 — Kodlama adları dilin kendi çözücü kataloğundan gelir; hiçbiri dış bir araca ait değildir.
  • HD23 — Parça boyu taraması aynı baytları farklı boylarda böler ve her parçayı ayrı ayrı çözer; parçalar arasında durum taşınmaz.
"""Ayni baytlar, ayri kodlamalar: cozulur mu, ozdes mi, nerede kirilir."""

METIN = "kuzey yamaç: 12,5 °C — ölçüm tamam"
HAM = METIN.encode("utf-8")

print(f"karakter {len(METIN)} | utf-8 bayt {len(HAM)} | "
      f"çok baytlı karakter {sum(len(k.encode('utf-8')) > 1 for k in METIN)}")

KODLAMALAR = ("utf-8", "utf-16", "latin-1", "ascii", "cp1254")

print()
print(f"{'kodlama':<12s}{'çözüldü':<10s}{'karakter':>9s}{'özdeş':>8s}"
      f"   kırılan sınıf / bayt konumu")
for ad in KODLAMALAR:
    try:
        cozulen = HAM.decode(ad)
    except UnicodeDecodeError as e:
        print(f"  {ad:<10s}{'hayır':<10s}{'-':>9s}{'-':>8s}"
              f"   {type(e).__name__} / {e.start}")
    else:
        print(f"  {ad:<10s}{'evet':<10s}{len(cozulen):>9d}"
              f"{('evet' if cozulen == METIN else 'hayır'):>8s}   -")

print()
print(f"{'hata kipi':<20s}{'karakter':>9s}{'özdeş':>8s}   ilk yedi karakter")
for kip in ("ignore", "replace", "backslashreplace"):
    c = HAM.decode("ascii", errors=kip)
    print(f"  {kip:<18s}{len(c):>9d}{('evet' if c == METIN else 'hayır'):>8s}"
          f"   {c[:7]!r}")

print()
print(f"{'parça boyu':<12s}{'parça':>7s}{'kırılan parça':>15s}")
for boy in range(4, 25, 2):
    parcalar = [HAM[i:i + boy] for i in range(0, len(HAM), boy)]
    kirilan = 0
    for p in parcalar:
        try:
            p.decode("utf-8")
        except UnicodeDecodeError:
            kirilan += 1
    print(f"  {boy:<10d}{len(parcalar):>7d}{kirilan:>15d}")
butun = 0
try:
    HAM.decode("utf-8")
except UnicodeDecodeError:
    butun = 1
print(f"aynı baytlar bütün olarak çözüldüğünde kırılan {butun}")
karakter 34 | utf-8 bayt 41 | çok baytlı karakter 6

kodlama     çözüldü    karakter   özdeş   kırılan sınıf / bayt konumu
  utf-8     evet             34    evet   -
  utf-16    hayır             -       -   UnicodeDecodeError / 40
  latin-1   evet             41   hayır   -
  ascii     hayır             -       -   UnicodeDecodeError / 10
  cp1254    evet             41   hayır   -

hata kipi            karakter   özdeş   ilk yedi karakter
  ignore                   28   hayır   'kuzey y'
  replace                  41   hayır   'kuzey y'
  backslashreplace         80   hayır   'kuzey y'

parça boyu    parça  kırılan parça
  4              11              4
  6               7              2
  8               6              2
  10              5              2
  12              4              2
  14              3              2
  16              3              0
  18              3              0
  20              3              2
  22              2              0
  24              2              2
aynı baytlar bütün olarak çözüldüğünde kırılan 0

Sessiz Yanlış

İlk satır dersin çekirdeğini veriyor: 34 karakterlik metin 41 bayt tutuyor. Aradaki 7 baytlık fark, çok baytlı 6 karakterden geliyor. Karakter sayısı ile bayt sayısı birbirinden bağımsızdır ve hangisini saydığınız hangi kipte okuduğunuza bağlıdır.

Kodlama tablosu üç ayrı davranış gösteriyor. utf-8 çözüyor, 34 karakter veriyor ve kaynakla özdeş. Doğru kodlamanın tanımı budur.

ascii ve utf-16 çözemiyor ve UnicodeDecodeError fırlatıyor. Konumlar ayrı: ascii 10’uncu baytta kırılıyor — ilk Türkçe harfin ilk baytında. utf-16 ise 40’ıncı baytta kırılıyor, yani neredeyse sonda; çünkü utf-16 baytları ikişerli okur ve 41 tek sayıdır, son bayt eşsiz kalır. İkisi de kırılır ama nerede kırıldıkları kodlamanın kendi kuralından gelir.

Asıl bulgu latin-1 ve cp1254 satırlarıdır. İkisi de çözüyor — hiç istisna fırlatmıyor — ve 41 karakter veriyor. Yani her baytı bir karaktere çeviriyorlar. Ama özdeş değil. Tek baytlı bir kodlama, önüne gelen her baytı geçerli sayar; kırılacak bir şey yoktur. Bu, ölçümün en tehlikeli sonucudur: yanlış kodlama her zaman istisna üretmez. İstisna fırlatan kodlama sizi uyarır; fırlatmayan kodlama sessizce yanlış metin verir ve yanlışlık ancak metni okuyan insan tarafından fark edilir.

Hata kipleri bunun bilinçli hâlidir. ignore çözemediği baytları atıyor ve 28 karakter kalıyor — 6 karakterin baytları düşmüş. replace her kusurlu baytın yerine bir karakter koyuyor ve 41 çıkıyor; sayı bayt sayısına eşitleniyor çünkü değiştirme bayt başına yapılıyor. backslashreplace her kusurlu baytı dört karakterlik bir yazımla anlatıyor ve uzunluk 80’e çıkıyor. Üçünün de özdeş sütunu hayır: hata kipi vermek, veriyi kurtarmaz, yalnız kırılmayı engeller.

Parçanın Sınırı

Son tablo ikili kipte parça parça okumanın bedelini veriyor. Aynı 41 bayt, farklı boylarda bölünüp her parça ayrı ayrı çözülüyor.

Sonuç düzensiz. 4 baytlık parçalarda 11 parçanın 4’ü kırılıyor; 16 ve 18 boyunda hiçbiri kırılmıyor; 20’de yine 2 parça kırılıyor, 22’de 0, 24’te 2. Kırılma sayısı parça boyunun büyüklüğüne bağlı değil, parça sınırının çok baytlı bir karakterin ortasına düşüp düşmediğine bağlı. Aynı baytlar bütün olarak çözüldüğünde kırılan 0.

Bunun doğrudan sonucu şudur: ikili kipte okuyup baytları kendiniz çözüyorsanız, parçalar arasında durum taşımak sizin işinizdir. Metin kipi bu işi zaten yapar — çözücü parça sınırında yarım kalan karakteri bekletir. İkili kip hızlı ve tarafsızdır; çeviriyi üstlenmediği için çevirinin sorunlarını da üstlenmez ve onları çağırana bırakır.

Kip, Tür ve Kırılma Noktası

İkinci ölçüm gerçek bir dosyayla çalışır. Dosya geçici bir dizinde kurulur ve ölçüm bitince dizin silinir; ölçüm hiçbir kalıcı iz bırakmaz.

  • HD24 — Dosya geçici bir dizinde üretilir ve baytları önceki ölçümün metninden gelir; hiçbir yol adı basılmaz, çünkü yol ortama bağlıdır.
  • HD25 — “Uzunluk” ikili kipte bayt, metin kipinde karakter sayısıdır; ikisi aynı adla ölçülür ama aynı şey değildir.
  • HD26 — Kırılma noktası ölçümü açma ile okumayı ayrı adım olarak dener; hangisinin istisna ürettiği koşumdan okunur.
  • HD27 — Satır sonu ölçümünde dosya, biri iki karakterlik biri tek karakterlik olmak üzere iki ayrı satır sonu biçimi taşır; çeviri farkı bundan doğar.
"""Metin kipi ile ikili kip: ayni dosya, ayri tur, ayri uzunluk, ayri kirilma noktasi."""
import tempfile
from pathlib import Path

METIN = "kuzey yamaç: 12,5 °C — ölçüm tamam"
SATIRLI = b"kuzey\r\nyama\xc3\xa7\nolcum\r\n"

with tempfile.TemporaryDirectory() as gecici:
    olcum = Path(gecici) / "olcum.dat"
    olcum.write_bytes(METIN.encode("utf-8"))
    satir = Path(gecici) / "satir.dat"
    satir.write_bytes(SATIRLI)

    print(f"{'okuma kipi':<22s}{'tür':<8s}{'uzunluk':>8s}{'özdeş':>8s}")
    with open(olcum, "rb") as d:
        ikili = d.read()
    print(f"  {'ikili':<20s}{type(ikili).__name__:<8s}{len(ikili):>8d}{'-':>8s}")
    for ad, kodlama, kip in (("metin utf-8", "utf-8", "strict"),
                             ("metin latin-1", "latin-1", "strict"),
                             ("metin ascii replace", "ascii", "replace")):
        with open(olcum, "r", encoding=kodlama, errors=kip) as d:
            metin = d.read()
        print(f"  {ad:<20s}{type(metin).__name__:<8s}{len(metin):>8d}"
              f"{('evet' if metin == METIN else 'hayır'):>8s}")

    print()
    print("adım                sonuç")
    try:
        d = open(olcum, "r", encoding="ascii")
    except UnicodeDecodeError as e:
        print(f"  {'open':<18s}{type(e).__name__} / bayt {e.start}")
    else:
        print(f"  {'open':<18s}başarılı, dosya açıldı")
        try:
            d.read()
        except UnicodeDecodeError as e:
            print(f"  {'read':<18s}{type(e).__name__} / bayt {e.start}")
        finally:
            d.close()

    print()
    print(f"{'okuma biçimi':<22s}{'uzunluk':>8s}{'CR sayısı':>11s}{'satır':>7s}")
    with open(satir, "rb") as d:
        ham = d.read()
    print(f"  {'ikili':<20s}{len(ham):>8d}{ham.count(13):>11d}"
          f"{len(ham.splitlines()):>7d}")
    for ad, yenisatir in (("metin (öntanımlı)", None), ('metin newline=""', "")):
        with open(satir, "r", encoding="utf-8", newline=yenisatir) as d:
            m = d.read()
        with open(satir, "r", encoding="utf-8", newline=yenisatir) as d:
            satirlar = d.readlines()
        print(f"  {ad:<20s}{len(m):>8d}{m.count(chr(13)):>11d}"
              f"{len(satirlar):>7d}")

print("geçici dizin silindi:", not Path(gecici).exists())
okuma kipi            tür      uzunluk   özdeş
  ikili               bytes         41       -
  metin utf-8         str           34    evet
  metin latin-1       str           41   hayır
  metin ascii replace str           41   hayır

adım                sonuç
  open              başarılı, dosya açıldı
  read              UnicodeDecodeError / bayt 10

okuma biçimi           uzunluk  CR sayısı  satır
  ikili                     21          2      3
  metin (öntanımlı)         18          0      3
  metin newline=""          20          2      3
geçici dizin silindi: True

Sayıların Okunması

Birinci tablo iki kipin farkını türle veriyor. İkili kip bytes döndürüyor ve uzunluk 41; metin kipi str döndürüyor ve doğru kodlamayla uzunluk 34. Aynı dosya, aynı içerik, iki ayrı sayı — ve ikisi de doğru. latin-1 yine 41 veriyor ve özdeş sütunu hayır: bellekteki ölçümde görülen sessiz yanlış, dosyadan okurken de aynen tekrarlanıyor.

İkinci tablo dersin en kesin bulgusudur. open çağrısı ascii kodlamasıyla başarılı oluyor — dosya açılıyor, hiçbir istisna çıkmıyor. İstisna read çağrısında, 10’uncu baytta çıkıyor. open yalnız dosyayı hazırlar ve kodlama adını saklar; çözme işi ilk okumada yapılır. Kodlama hatası bir açma hatası değil, okuma hatasıdır ve bunun pratik sonucu şudur: open çağrısını saran bir try bloğu bu hatayı yakalayamaz. Yakalayacak blok okumayı sarmalıdır.

Üçüncü tablo satır sonu çevirisini sayıyor. Dosya 21 bayt ve içinde 2 tane satır başı karakteri var, çünkü iki satır iki karakterlik satır sonuyla, biri tek karakterlik satır sonuyla bitiyor. Metin kipi öntanımlı ayarıyla okuduğunda uzunluk 18’e iniyor ve satır başı sayısı 0 oluyor: çözücü her satır sonu biçimini tek bir karaktere çevirdi. newline="" verildiğinde çeviri kapanıyor ve uzunluk 20, satır başı sayısı 2 oluyor.

Satır sayısı üç okumada da 3. Çeviri satırların nerede bittiğini değiştirmez, yalnız satır sonunun kaç karakterle yazıldığını değiştirir. Metin kipinde okunan bir dosyanın uzunluğunu dosyanın boyutuyla karşılaştırmak bu yüzden yanlış sonuç verir: hem kodlama hem satır sonu çevirisi araya girer.

Yazma Tarafı ve Açma Kipleri

Okuma tarafı kodlamayı varsayar; yazma tarafı onu kurar. Üçüncü ölçüm aynı metni beş kodlamayla yazmayı dener ve her birinde gidiş–dönüş sınaması yapar: kodlanan baytlar aynı kodlamayla geri çözüldüğünde kaynak metin geri gelir mi?

  • HD28 — Gidiş–dönüş sınaması her kodlamada kendi kodlamasıyla yapılır; ölçülen şey kodlamanın metni taşıyıp taşıyamadığıdır.
  • HD29 — Açma kipleri aynı dosya üzerinde sırayla denenir; her adımda dosyanın bayt sayısı dosya sisteminden okunur, hesaplanmaz.
  • HD30 — Dosya yine geçici bir dizinde kurulur; hiçbir yol basılmaz ve ölçüm bitince dizin silinir.
"""Yazma tarafi: kodlanamayan karakter ve dosya acma kipleri."""
import tempfile
from pathlib import Path

METIN = "kuzey yamaç: 12,5 °C — ölçüm tamam"

print(f"{'kodlama':<12s}{'kodlandı':<10s}{'bayt':>6s}"
      f"{'gidiş-dönüş özdeş':>19s}   kırılan sınıf / konum")
for ad in ("utf-8", "utf-16", "latin-1", "ascii", "cp1254"):
    try:
        ham = METIN.encode(ad)
    except UnicodeEncodeError as e:
        print(f"  {ad:<10s}{'hayır':<10s}{'-':>6s}{'-':>19s}"
              f"   {type(e).__name__} / {e.start}")
    else:
        ozdes = "evet" if ham.decode(ad) == METIN else "hayır"
        print(f"  {ad:<10s}{'evet':<10s}{len(ham):>6d}{ozdes:>19s}   -")

print()
with tempfile.TemporaryDirectory() as gecici:
    gunluk = Path(gecici) / "olcum.log"
    print(f"{'kip':<6s}{'çağrı':<8s}{'dosya baytı':>12s}   sonuç")
    for sira, kip in (("1.", "w"), ("2.", "w"), ("3.", "a"), ("4.", "x")):
        try:
            with open(gunluk, kip, encoding="utf-8") as d:
                d.write(METIN + "\n")
        except FileExistsError as e:
            print(f"  {kip:<4s}{sira:<8s}{gunluk.stat().st_size:>12d}"
                  f"   {type(e).__name__}")
        else:
            print(f"  {kip:<4s}{sira:<8s}{gunluk.stat().st_size:>12d}   yazıldı")
    with open(gunluk, "r", encoding="utf-8") as d:
        satirlar = d.readlines()
    print(f"son durumda satır {len(satirlar)}, "
          f"karakter {sum(len(s) for s in satirlar)}, "
          f"bayt {gunluk.stat().st_size}")
kodlama     kodlandı    bayt  gidiş-dönüş özdeş   kırılan sınıf / konum
  utf-8     evet          41               evet   -
  utf-16    evet          70               evet   -
  latin-1   hayır          -                  -   UnicodeEncodeError / 21
  ascii     hayır          -                  -   UnicodeEncodeError / 10
  cp1254    evet          34               evet   -

kip   çağrı    dosya baytı   sonuç
  w   1.                42   yazıldı
  w   2.                42   yazıldı
  a   3.                84   yazıldı
  x   4.                84   FileExistsError
son durumda satır 2, karakter 70, bayt 84

cp1254 satırı okuma tablosunun karşıtıdır ve dersin en öğretici çiftidir. Orada cp1254 utf-8 baytlarını sessizce yanlış çözüyordu; burada aynı kodlama metni 34 bayta yazıyor ve gidiş–dönüş özdeş çıkıyor. Kodlama bir dosyanın özelliği değildir; yazanla okuyan arasındaki anlaşmadır ve dosyanın kendisi o anlaşmayı taşımaz. Yanlış olan cp1254 değildi, utf-8 ile yazılmış baytları cp1254 sanmaktı.

Yazma tarafı okuma tarafından bir noktada ayrılır: latin-1 ve ascii burada kırılıyor. latin-1 21’inci karakterde, uzun çizgide; ascii 10‘uncu karakterde, ilk Türkçe harfte. Yazarken kodlanamayan karakter sessizce geçemez, çünkü karakterin karşılığı kodlamada yoktur. Okurken sessiz yanlış mümkündü, çünkü her baytın bir karşılığı vardı. utf-16 metni 70 bayta yazıyor — utf-8’in 41’ine karşı — ve o da özdeş dönüyor; kodlama seçimi doğruluk kadar boyut kararıdır.

Açma kipleri tablosu üç ayrı davranışı ayırıyor. "w" ilk çağrıda 42 bayt yazıyor; ikinci "w" çağrısından sonra dosya yine 42 — kip dosyayı budayıp baştan yazdı, ikinci kayıt birinciyi sildi. "a" çağrısı 84’e çıkarıyor: ekleme kipi budamaz. "x" çağrısı hiç yazmadan FileExistsError fırlatıyor ve bayt sayısı 84’te kalıyor. Son satır dosyanın 2 satır, 70 karakter ve 84 bayt tuttuğunu söylüyor; karakter ile bayt farkı burada da duruyor.

Özet

  • İkili kip bytes verir ve uzunluk bayt sayısıdır; metin kipi str verir, uzunluk karakter sayısıdır — aynı dosya 41 ve 34 okunur.
  • Yanlış kodlama her zaman kırılmaz: ascii 10’uncu baytta, utf-16 40’ıncı baytta kırılırken latin-1 ve cp1254 hiç kırılmaz ve sessizce 41 yanlış karakter üretir.
  • Hata kipleri kırılmayı engeller, veriyi kurtarmaz: ignore 28, replace 41, backslashreplace 80 karakter verir ve hiçbiri kaynakla özdeş değildir.
  • Baytları parça parça çözmek parça sınırına bağlıdır: aynı 41 bayt boya göre 0, 2 ya da 4 parçada kırılır, bütün olarak çözüldüğünde 0.
  • Kodlama hatası open çağrısında değil read çağrısında çıkar; satır sonu çevirisi uzunluğu 21’den 18’e indirir ama satır sayısını 3 olarak korur.
  • Kodlama dosyanın değil, yazanla okuyanın özelliğidir: cp1254 yazarken 34 baytla özdeş gidiş–dönüş verirken utf-8 baytlarını okurken sessizce bozar; yazarken kodlanamayan karakter latin-1’de 21’inci, ascii’de 10’uncu karakterde kırılır.

Sonraki Adım

Bu dersin bütün dosya örnekleri with bloğu içinde açıldı ve blok bitince dosya kapandı — ölçüm sonunda geçici dizin bile silinebildi, çünkü açık kalan bir tutamak kalmamıştı. with şimdiye kadar bir alışkanlık olarak kullanıldı; oysa o da bir sözdizimdir ve altında bir protokol vardır. Sonraki ders o protokolü ölçer: with girerken hangi özel yöntemi çağırır, çıkarken hangisini, ve blok içinde bir istisna oluştuğunda ne değişir. Ortak tanımın with n + istisna satırının üç çağrısı orada ödenir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat