Ana içeriğe geç
Kesit Python
Orta · Standart kütüphane

İleri dosya işlemleri: ikili mod, akışlar, arşiv ve atomik yazma

Metin ve ikili modu ayırır; bytes/bytearray, seek/tell, OSError alt sınıfları, standart akışlar, pathlib glob, shutil, zipfile/tarfile, tempfile ve atomik yazmayla gerçek dosya işlerini sağlam yaparsın.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli?

Anadolu Market'in gece çalışan işlerini düşün: her mağaza kasası günün satışlarını bir CSV dosyası olarak gelen/2026-10-27/ klasörüne bırakıyor, ödeme sistemi ikili (binary) bir dekont arşivi gönderiyor, muhasebe sabah 08.00'de rapor.json dosyasını okuyor. Bu işte üç şey ters gidebilir: yanlış modda açılan dosya Türkçe karakterleri bozar, bir klasördeki 500 dosyadan biri bozuk olduğu için tüm iş durur, ya da program rapor yazarken çöker ve muhasebe yarım bir JSON okur.

Temel düzeydeki Dosya işlemleri dersinde open, with, okuma-yazma modlarını ve pathlib'in ilk adımlarını gördün. Bu ders o temelin üstüne kurumsal düzeyi koyar: bayt düzeyinde çalışma, dosya içinde gezinme, hata türlerini ayırma, standart akışlar, klasörleri toplu işleme, arşivler ve hiçbir zaman yarım kalmayan yazma.

Metin modu ve ikili mod

Diskte her şey bayttır (byte). Metin modu ("r", "w") baytları senin için bir kodlamayla (encoding) çözer ve str verir; satır sonlarını da işletim sistemine göre çevirir. İkili mod ("rb", "wb") hiçbir çeviri yapmaz, ham bytes verir. Resim, PDF, zip, şifreli dosya ya da özet (hash) hesabı için her zaman ikili mod kullanırsın.

import tempfile, os
with tempfile.TemporaryDirectory() as d:
    yol = os.path.join(d, "sehir.txt")
    with open(yol, "w", encoding="utf-8") as f:
        yazilan = f.write("İzmir\nÇorum\n")
    print("yazılan karakter:", yazilan)
    with open(yol, "rb") as f:
        ham = f.read()
    print(type(ham).__name__, len(ham), "bayt")
    print(ham)
    print(ham.decode("utf-8").splitlines())

12 karakter yazdın ama dosya 14 bayt: UTF-8'de İ ve Ç ikişer bayt tutar. Metin modunda write yazılan karakter sayısını, ikili modda bayt sayısını döndürür.

ModAnlamıDosya yoksaDosya varsa
r / rbOkuFileNotFoundErrorBaştan okur
w / wbYazOluştururİçeriği SİLER
a / abSona ekleOluştururSona yazar
x / xbYalnız yeni dosyaOluştururFileExistsError
r+ / r+bOku ve yazFileNotFoundErrorSilmeden, imlecin olduğu yere yazar
encoding'i hep açıkça yazMetin modunda encoding vermezsen Python işletim sisteminin varsayılanını kullanır; Windows'ta bu hâlâ cp1254 olabilir. Kendi makinende çalışan betik sunucuda Türkçe karakterleri bozar. Excel'in ürettiği CSV'lerin başındaki BOM işaretini atmak için encoding="utf-8-sig" kullan.

bytes ve bytearray: tampon olarak baytlar

bytes değişmez (immutable) bir bayt dizisidir; bytearray ise değiştirilebilir (mutable) kardeşidir. Bir ağ paketini, sabit genişlikli bir banka kaydını ya da parça parça gelen veriyi biriktirirken bytearray bir tampon (buffer) gibi kullanılır: her eklemede yeni nesne üretmez.

imza = b"PK"           # zip dosyalarının ilk 4 baytı
print(imza[0], imza[:2], len(imza))   # indeks tek bayt -> int verir

tampon = bytearray()
for parca in [b"AB", b"-", b"2026"]:
    tampon.extend(parca)              # yerinde büyür
tampon[0] = ord("X")                  # yerinde değiştir
print(tampon, bytes(tampon))
print("2026".encode("ascii") in tampon)
print(bytes([72, 105]).decode())       # sayılardan bayt

Dikkat: imza[0] bir tam sayı (80) verir, imza[:2] ise bytes. str ile bytes asla karışmaz: b"abc" == "abc" False'tur, b"abc" + "d" TypeError verir. Geçiş her zaman encode (str → bytes) ve decode (bytes → str) ile yapılır.

İmleç: read, readline, seek, tell

Açık bir dosyanın bir imleci (file pointer) vardır. Her okuma imleci ileri taşır. tell() imlecin konumunu, seek(konum, nereden) imleci taşımayı sağlar: nereden 0 = baştan, 1 = bulunduğun yerden, 2 = sondan. İkili modda bunlar bayt cinsindendir; metin modunda seek yalnızca tell'den aldığın değerlerle güvenlidir.

import io
f = io.BytesIO(b"ID;TUTAR\n101;250\n102;975\n")   # bellek içi ikili dosya
print(f.readline())          # ilk satır (başlık)
print(f.tell())              # 9 bayt okundu
print(f.read(3))             # sonraki 3 bayt
f.seek(0)                    # başa dön
print(len(f.readlines()), "satır")
f.seek(-4, 2)                # sondan 4 bayt geri
print(f.read())

io.BytesIO ve io.StringIO diskte dosya açmadan aynı arayüzü verir; testlerde ve küçük denemelerde çok işine yarar. readlines() tüm dosyayı listeye yükler; büyük dosyada bunun yerine for satir in f kullan.

Büyük dosyayı parça parça okumak

5 GB'lık bir işlem arşivinin özetini (hash) hesaplamak ya da satırlarını saymak için dosyayı belleğe almazsın; sabit boyutlu parçalar (chunk) hâlinde okursun. Walrus operatörü (:=) bu döngüyü çok temiz yazar.

import hashlib, io
dosya = io.BytesIO(b"islem;" * 10_000)
ozet = hashlib.sha256()
parca_sayisi = 0
while parca := dosya.read(8192):     # boş bytes gelince döngü biter
    ozet.update(parca)
    parca_sayisi += 1
print(parca_sayisi, "parça")
print(ozet.hexdigest()[:16])
print(ozet.hexdigest() == hashlib.sha256(b"islem;" * 10_000).hexdigest())

Bellek kullanımı dosya boyutundan bağımsızdır: hep en fazla 8 KB. Parça sınırına denk gelen bir satır sonu ya da çok baytlı bir harf ikiye bölünebilir; bu yüzden satır mantığı gerekiyorsa son parçayı bir sonrakine taşırsın ya da doğrudan for satir in f kullanırsın (o da içeride tamponlu okur).

Hata türleri: OSError ailesi ve errno

Dosya ve işletim sistemiyle ilgili tüm hatalar OSError'dan türer. Her birinin bir errno kodu (işletim sisteminin hata numarası), bir strerror açıklaması ve çoğunlukla bir filename alanı vardır. Python, errno koduna bakarak doğru alt sınıfı kendisi seçer.

Alt sınıferrnoNe zaman?
FileNotFoundErrorENOENTDosya ya da klasör yok
FileExistsErrorEEXIST"x" modu, mkdir var olan klasörde
PermissionErrorEACCES, EPERMOkuma/yazma izni yok, dosya başka programda kilitli (Windows)
IsADirectoryErrorEISDIRKlasörü dosya gibi açmaya çalıştın
NotADirectoryErrorENOTDIRDosyayı klasör gibi kullandın
import errno, tempfile
try:
    open("/yok/boyle/bir/dosya.csv", encoding="utf-8")
except FileNotFoundError as e:
    print(type(e).__name__, e.errno == errno.ENOENT, e.filename)

with tempfile.TemporaryDirectory() as d:
    try:
        open(d, encoding="utf-8")
    except IsADirectoryError as e:
        print("Klasör açılamaz:", e.strerror)
    except PermissionError as e:          # bazı sistemler klasör için bunu verir
        print("İzin hatası:", e.strerror)

hata = OSError(errno.EACCES, "Erişim reddedildi")
print(type(hata).__name__, isinstance(hata, OSError))

Son satır önemli: OSError(errno.EACCES, ...) yazdığında Python sana doğrudan PermissionError verir. Bu yüzden ya alt sınıfları yakalarsın (okunur), ya da OSError yakalayıp e.errno ile ayırırsın (daha ayrıntılı). Özelden genele sırala: önce FileNotFoundError, en sonda OSError.

Standart akışlar: stdin, stdout, stderr

Her programın üç hazır akışı vardır: sys.stdin (girdi), sys.stdout (normal çıktı) ve sys.stderr (hata ve uyarı çıktısı). Bunlar da dosya nesnesidir; write, readline, for satir in sys.stdin ile kullanılır. Kural basit: sonuç stdout'a, gürültü stderr'e. Böylece python rapor.py > rapor.csv diye yönlendiren biri dosyasına uyarı mesajı karışmadan yalnız veriyi alır.

import sys, io, contextlib
kayitlar = ["Ankara;250", "Bursa;abc", "İzmir;975"]
cikti, hatalar = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(cikti), contextlib.redirect_stderr(hatalar):
    for k in kayitlar:
        sehir, tutar = k.split(";")
        if tutar.isdigit():
            print(f"{sehir},{tutar}")
        else:
            print(f"UYARI: geçersiz tutar -> {k}", file=sys.stderr)
print("stdout:", cikti.getvalue().splitlines())
print("stderr:", hatalar.getvalue().strip())

redirect_stdout/redirect_stderr akışı geçici olarak başka bir dosya nesnesine çevirir; test yazarken programın ne yazdırdığını yakalamanın standart yolu budur. Gerçek bir komut satırı aracında ise for satir in sys.stdin: ile boru hattından (pipe) gelen veriyi satır satır okursun: cat siparis.csv | python temizle.py.

pathlib ileri: klasörü sorgulamak

from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as d:
    kok = Path(d)
    (kok / "2026" / "10").mkdir(parents=True, exist_ok=True)   # ara klasörler de
    for ad in ["2026/10/ankara.csv", "2026/10/izmir.csv", "2026/notlar.txt", "ozet.csv"]:
        (kok / ad).write_text("tarih,tutar\n", encoding="utf-8")
    print(sorted(p.name for p in kok.iterdir()))          # yalnız bir seviye
    print(sorted(p.name for p in kok.glob("*.csv")))      # bu klasörde
    print(sorted(p.relative_to(kok).as_posix() for p in kok.rglob("*.csv")))  # tüm alt klasörler
    p = kok / "2026/10/ankara.csv"
    print(p.stem, p.suffix, p.with_suffix(".json").name, p.parent.name)
    print(p.stat().st_size, "bayt", p.exists(), p.is_file())

glob("*.csv") yalnızca o klasöre, rglob("*.csv") tüm alt ağaca bakar (glob("**/*.csv") ile aynı). iterdir() ve glob sırası işletim sistemine göre değişir; raporda tekrarlanabilir sonuç için her zaman sırala. stem son uzantı hariç ad, suffix son uzantı, suffixes tüm uzantılardır (.tar.gz için iki tane).

shutil, tempfile, zipfile ve tarfile

shutil dosya düzeyinde üst işlemleri yapar: copy (içerik + izinler), copy2 (tarihler de), copytree (klasör ağacı), move (taşı/yeniden adlandır), rmtree (klasörü içiyle sil, geri dönüşü yok). tempfile güvenli geçici dosya ve klasör verir; TemporaryDirectory blok bitince her şeyi temizler.

import shutil, tempfile, zipfile, tarfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
    kok = Path(d)
    kaynak = kok / "rapor"
    kaynak.mkdir()
    (kaynak / "ciro.csv").write_text("sehir,tutar\nAnkara,250\n", encoding="utf-8")
    (kaynak / "not.txt").write_text("gece işi tamam", encoding="utf-8")
    shutil.copy(kaynak / "ciro.csv", kaynak / "ciro_yedek.csv")

    zip_yolu = kok / "rapor.zip"
    with zipfile.ZipFile(zip_yolu, "w", compression=zipfile.ZIP_DEFLATED) as z:
        for p in sorted(kaynak.rglob("*")):
            z.write(p, arcname=p.relative_to(kaynak).as_posix())
    with zipfile.ZipFile(zip_yolu) as z:
        print(z.namelist())
        print(z.read("ciro.csv").decode("utf-8").splitlines()[1])

    with tarfile.open(kok / "rapor.tar.gz", "w:gz") as t:
        t.add(kaynak, arcname="rapor")
    with tarfile.open(kok / "rapor.tar.gz") as t:
        print(sorted(m.name for m in t.getmembers() if m.isfile()))
    shutil.rmtree(kaynak)
    print(kaynak.exists())
Güvenmediğin arşivi körü körüne açmaBir zip/tar içindeki ../../etc/passwd gibi adlar hedef klasörün dışına yazabilir (path traversal). tarfile için Python 3.12'de t.extractall(hedef, filter="data") kullan; zip'te her adın Path(hedef, ad).resolve() sonucunun hedefin içinde kaldığını kontrol et.

Atomik yazma ve dosya kilidi

Program open("rapor.json", "w") ile dosyayı açtığı anda eski içerik silinir. Yazma sürerken elektrik kesilirse ya da program çökerse geride yarım bir dosya kalır. Çözüm: önce aynı klasörde geçici bir dosyaya yaz, kapat, sonra os.replace ile asıl adın üzerine taşı. os.replace aynı dosya sisteminde atomiktir: okuyan kişi ya eski dosyanın tamamını ya yeni dosyanın tamamını görür, arası yoktur.

import os, json, tempfile
def atomik_json_yaz(yol, veri):
    klasor = os.path.dirname(os.path.abspath(yol))
    fd, gecici = tempfile.mkstemp(dir=klasor, suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump(veri, f, ensure_ascii=False)
            f.flush()
            os.fsync(f.fileno())        # diske gerçekten yazıldığından emin ol
        os.replace(gecici, yol)          # tek hamlede yer değiştir
    except BaseException:
        os.unlink(gecici)                # hata olursa çöp bırakma
        raise

with tempfile.TemporaryDirectory() as d:
    hedef = os.path.join(d, "rapor.json")
    atomik_json_yaz(hedef, {"sehir": "Ankara", "ciro": 1250})
    atomik_json_yaz(hedef, {"sehir": "İzmir", "ciro": 975})
    print(open(hedef, encoding="utf-8").read(), os.listdir(d))

Geçici dosyanın hedefle aynı klasörde olması şarttır; /tmp başka bir disk bölümündeyse os.replace atomik olamaz ya da hata verir.

İki süreç aynı dosyaya aynı anda yazıyorsa kilit (lock) gerekir. En taşınabilir basit yöntem bir kilit dosyasıdır: open("is.lock", "x") dosya zaten varsa FileExistsError verir, yani kilidi yalnızca bir süreç alabilir; iş bitince os.remove ile bırakırsın. İşletim sistemi düzeyinde kilit için Linux/macOS'ta fcntl.flock, Windows'ta msvcrt.locking, ikisini birden saran üçüncü parti filelock paketi vardır.

Kilit dosyası çökmeye dayanıklı olmalıProgram kilidi aldıktan sonra çökerse kilit dosyası kalır ve sonraki çalışma sonsuza dek bekler. Kilidi try/finally içinde bırak, kilit dosyasına süreç numarası (PID) ve zaman yaz ki eski kilitleri tanıyabilesin.

Klasörü toplu işlemek

Gerçek işlerin çoğu şu kalıptır: bir klasördeki dosyaları bul, her birini oku, bozuk olanı atla ama kaydını tut, sonucu tek bir rapora atomik yaz.

import csv, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
    kok = Path(d)
    (kok / "ankara.csv").write_text("sehir,tutar\nAnkara,250\nAnkara,100\n", encoding="utf-8")
    (kok / "izmir.csv").write_text("sehir,tutar\nİzmir,975\nİzmir,yok\n", encoding="utf-8")
    toplam, hatali = {}, []
    for yol in sorted(kok.glob("*.csv")):
        with yol.open(encoding="utf-8", newline="") as f:
            for no, satir in enumerate(csv.DictReader(f), start=2):
                try:
                    tutar = float(satir["tutar"])
                except ValueError:
                    hatali.append(f"{yol.name}:{no}")
                    continue
                toplam[satir["sehir"]] = toplam.get(satir["sehir"], 0) + tutar
    print(toplam)
    print("hatalı satırlar:", hatali)
Gerçek işteBankalarda ve e-ticarette "gece işi" (batch job) dediğin şey çoğunlukla budur: SFTP'ye düşen dosyaları bulmak, işlemek, işleneni arsiv/ klasörüne shutil.move ile taşımak, hatalıyı hatali/ klasörüne ayırmak ve sonucu atomik yazmak. Hatalı satırın dosya adı ve satır numarasıyla loglanması, sabah gelen "rakam tutmuyor" e-postasını beş dakikada çözmeni sağlar. Aynı dosyanın iki kez işlenmemesi için işlenmiş dosyaların özetini (sha256) bir listede tutmak da yaygın bir pratiktir.
Uzmanlaş

Arabellek, flush ve fsync

write veriyi önce Python'un arabelleğine (buffer), sonra işletim sisteminin önbelleğine (cache) yazar; diske ne zaman gideceğine işletim sistemi karar verir. f.flush() Python arabelleğini boşaltır, os.fsync(f.fileno()) işletim sisteminden diske yazmasını ister. Para ve sipariş gibi kaybı kabul edilemeyen veride ikisini birlikte kullanırsın; her satırda yapmak ise çok yavaştır, toplu yaparsın.

os.scandir ve büyük klasörler

Yüz binlerce dosyalı klasörde os.scandir, dosya türü ve boyut bilgisini tek sistem çağrısıyla getirdiği için os.listdir + os.stat'tan belirgin hızlıdır; pathlib'in iterdir'i de içeride benzer çalışır.

mmap ve readinto

mmap modülü dosyayı belleğe eşler; çok büyük ikili dosyada rastgele konumlara seek etmeden dilimle erişirsin. f.readinto(bytearray(n)) ise var olan bir tampona okur, her okumada yeni nesne üretmez; yüksek hacimli ikili protokollerde kullanılır.

Kodlama hataları

Kaynağını bilmediğin metin dosyasında errors="replace" bozuk baytları yerine koyma karakteriyle değiştirir, errors="strict" (varsayılan) hata verir. Veri kaybını görmezden gelmek yerine önce hatayı yakalayıp hangi dosyada olduğunu loglamak daha güvenlidir.

Alıştırmalara geç (18)