Python ile veri işleme betikleri yazmak
Tekrarlayan dönüştürme işlerini Python'a devretmenin pratik gerekçesini ve somut betik örneklerini paylaşıyorum.
Her projede bir yerde şu görev çıkar: bir formattan alıp başka formata dönüştür. CSV’den JSON’a, Excel’den düz metne, bir API yanıtından veritabanı kaydına. Küçükse elle yapılıyor. Büyükse ve tekrarlanıyorsa bir şeyler yazmak kaçınılmaz.
Bu görevleri Python’a devretmeye karar verdiğimde Laravel projesindeyim ve PHP yazabiliyorum. Aynı betikleri PHP’de de yazabilirim. Neden Python? Bu sorunun pratikte test edilmiş bir cevabı var artık.
Neden PHP değil?
PHP web isteklerine cevap vermek için tasarlanmış. CLI desteği var, php artisan komutları Laravel projelerinde gayet işe yarıyor. Ama bir dosyayı okuyup dönüştürüp yazmak için composer.json açmak, bir sınıf hiyerarşisi kurmak istemiyorum. Bu görevler için ceremony fazla.
Python’da aynı görev:
#!/usr/bin/env python3
import csv
import json
def csv_to_json(input_path: str, output_path: str) -> None:
rows = []
with open(input_path, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
rows.append(dict(row))
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(rows, f, ensure_ascii=False, indent=2)
print(f"{len(rows)} kayıt dönüştürüldü.")
if __name__ == '__main__':
csv_to_json('input.csv', 'output.json')
Dış bağımlılık sıfır. csv ve json standart kütüphanede. Dosya kaydet, python3 convert.py çalıştır, bitti.
Gerçek bir ihtiyaç: müşteri verisi temizleme
Geçen ay şöyle bir görev geldi: bin satırlık bir Excel dosyası, farklı formatlarda telefon numaraları (05xx xxx xx xx, +905xxxxxxxxx, 0 5xx-xxx-xxxx), bunları normalize et ve CSV olarak kaydet.
import re
import csv
import openpyxl
def normalize_phone(raw: str) -> str | None:
"""Türkiye telefon numarasını 05XXXXXXXXX formatına çevirir."""
digits = re.sub(r'\D', '', raw)
if digits.startswith('90') and len(digits) == 12:
digits = '0' + digits[2:]
if len(digits) == 10 and digits.startswith('5'):
digits = '0' + digits
if len(digits) == 11 and digits.startswith('05'):
return digits
return None # geçersiz
def process_excel(input_path: str, output_path: str) -> None:
wb = openpyxl.load_workbook(input_path)
ws = wb.active
valid = []
invalid = []
for row in ws.iter_rows(min_row=2, values_only=True):
name, phone_raw = row[0], str(row[1] or '')
normalized = normalize_phone(phone_raw)
if normalized:
valid.append({'name': name, 'phone': normalized})
else:
invalid.append({'name': name, 'phone': phone_raw})
with open(output_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['name', 'phone'])
writer.writeheader()
writer.writerows(valid)
print(f"Geçerli: {len(valid)}, Geçersiz: {len(invalid)}")
process_excel('musteriler.xlsx', 'temiz.csv')
openpyxl tek dış bağımlılık. Bu betiği PHP’de de yazabilirdim — ama re yerine PCRE, openpyxl yerine PhpSpreadsheet, genel sadelik yerine daha fazla boilerplate. Aynı sonuç, daha fazla sürtünme.
Bir detay daha var: bu betik geçersiz kayıtları da ayrı bir listeye alıyor. Başlangıçta bu adımı atlamıştım, yalnızca geçerli olanları yazmıştım. Müşteri şunu sordu: “Toplam 1000 satır göndermiştim, 847’si geldi, 153’üne ne oldu?” Geçersiz kayıtları ayrı bir dosyaya yazmak bu soruyu yanıtlamanın en temiz yolu oldu.
Tekrarlayan görevleri otomatize etmek
Bazı betikler tek seferlik. Bazıları ise haftalık çalıştırılıyor: FTP’den indir, dönüştür, veritabanına yükle. Bu rutinler için Python betiği + cron kombinasyonu gayet yeterli.
# weekly_import.py
import ftplib
import csv
import psycopg2 # ya da mysql-connector-python
def download_from_ftp(remote_path: str, local_path: str) -> None:
with ftplib.FTP('ftp.supplier.com') as ftp:
ftp.login(user='user', passwd='pass')
with open(local_path, 'wb') as f:
ftp.retrbinary(f'RETR {remote_path}', f.write)
def import_products(csv_path: str) -> int:
conn = psycopg2.connect(dsn="...")
cur = conn.cursor()
count = 0
with open(csv_path, newline='', encoding='utf-8') as f:
for row in csv.DictReader(f):
cur.execute(
"INSERT INTO products (sku, name, price) VALUES (%s, %s, %s) "
"ON CONFLICT (sku) DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price",
(row['SKU'], row['Name'], float(row['Price']))
)
count += 1
conn.commit()
cur.close()
conn.close()
return count
Bu kod web uygulamamın dışında yaşıyor, Laravel’i ilgilendirmiyor. Bağımsız bir araç, bağımsız bir süreç.
Betik büyüyünce dikkat edilmesi gerekenler
Tek dosyalık betikler küçük kalırsa sorun yok. Ama zamanla şunlar oluyor: önce bir fonksiyon, sonra birkaç fonksiyon, sonra “bu fonksiyonu şu betik de kullanabilir” düşüncesi ve kopyalama dönemi yeniden başlıyor. Bu noktada bir utils.py ya da küçük bir paket yapısı oluşturmak daha sağlıklı. Python’da pip install -e . ile yerel geliştirmede editable kurulum yapılabiliyor; betikler bu yerel paketten import edebiliyor.
Bir de hata yakalama meselesi var. Tek seferlik betiklerde kabaca çalışan bir şey yeterli olabilir. Ama haftalık otomatik çalışan bir betikte süreci gösteren net bir çıktı ve beklenmedik hata anında anlamlı bir mesaj önemli. try/except bloklarını üretim betiklerine eklemeyi ertelemek sonradan pişman oldunan bir karar.
Dil seçimi kararı
Veri işleme betikleri için şu kararı verdim: iş kütüphanesi gerektiriyorsa (Excel, SFTP, karmaşık regex) Python. Laravel projesiyle entegre çalışıyorsa (modeli okuyup başka modeli oluşturmak) php artisan komutu. Ölçek küçükse ve hız önemliyse Go bile deneyebilirim.
Bu ayrımı açık tutmak, her şeyi aynı dile sıkıştırmaktan daha iyi sonuç veriyor.
Python’un veri işleme betikleri için tercih olmaya başlaması bende bir dil değişikliği değil, doğru aracı seçme alışkanlığının olgunlaşması. PHP hâlâ ana dil; Python bu konudaki boşluğu dolduruyor.