Tüm yazılar
Veri Otomasyonu 6 dk okuma

API'si olmayan portaldan veri çekmek: Playwright ile oturum, indirme ve dayanıklı otomasyon

Playwright ile API'si olmayan portaldan veri çekme: oturum saklama, dosya indirme ve doğrulama, dayanıklı seçiciler, yeniden deneme ve KVKK sınırı. Python ile.

#playwright #python #tarayici-otomasyonu #veri-toplama

Playwright ile tarayıcı otomasyonu, API sunmayan bir tedarikçi portalından, banka ekstresi ekranından ya da kargo panelinden veriyi elle indirmek yerine bir betiğin düzenli olarak almasını sağlar. Küçük ve orta ölçekli işletmelerde veri çoğu zaman tam da böyle yerlerde durur: giriş yapılan bir panel, dönem seçilen bir açılır menü ve tıklanan bir "CSV indir" düğmesi. Her ayın başında bir kişi aynı işlemi on farklı portal için tekrar eder ve raporlama o kişinin takvimine bağımlı hale gelir.

API olmadığında ilk akla gelen çözüm, sayfanın HTML'ini requests ile çekip ayrıştırmaktır. Ancak modern portallar oturum çerezi, JavaScript ile yüklenen tablolar ve tek kullanımlık form belirteçleri kullanır; ham HTTP isteği çoğu zaman giriş ekranında takılır. Gerçek bir tarayıcıyı programla yönetmek, bu katmanları tek tek taklit etmek yerine kullanıcının yaptığı adımları aynen yapar.

Bu yazıda API'si olmayan bir portaldan dosya indiren bir Playwright hattı kuruyorum: oturumun nasıl açılıp saklandığı, indirilen dosyanın nasıl doğrulandığı, arayüz değiştiğinde betiğin sessizce bozulmak yerine nasıl anlaşılır biçimde durduğu ve işin hukuki sınırı. Örnekler Python ile yazıldı ve yalnız playwright paketini gerektirir; tarayıcı tek komutla kurulur.

Tarayıcı otomasyonu ne zaman doğru seçimdir

Tarayıcı otomasyonu, veriye ulaşmanın başka bir yolu kalmadığında başvurulan son seçenektir; resmi API, dışa aktarma e-postası ya da SFTP erişimi varsa onlar her zaman daha ucuz ve daha dayanıklıdır. Arayüz üzerinden gitmek, portal sahibinin her tasarım değişikliğini sizin bakım yükünüz haline getirir. Yine de API sunmayan ve rapor e-postası göndermeyen sistemler az değildir; böyle durumlarda doğru kurulmuş bir tarayıcı betiği elle yapılan işten daha güvenilirdir.

Betiği yazmaya başlamadan önce şu üç kontrolü yapmak, çoğu zaman tarayıcı otomasyonuna hiç gerek olmadığını gösterir:

  • Portalın ayarlar menüsünde "dışa aktar", "zamanlanmış rapor" ya da "API anahtarı" seçeneği var mı diye bakın; bu menüler çoğu kullanıcı tarafından hiç açılmamıştır.
  • Sağlayıcıya yazılı olarak sorun; küçük sağlayıcılar bile çoğu zaman bir dosya paylaşım yolu ya da günlük e-posta açabilir.
  • Veriyi hangi sıklıkla ve hangi hacimde çekeceğinizi önceden belirleyin; günde bir indirme ile dakikada bir sorgu, hem teknik hem hukuki açıdan aynı şey değildir.

Playwright'ı bu iş için tercih etmemin nedeni, beklemeyi kendisinin yönetmesidir. Bir düğmeye tıklama komutu, düğme görünür ve tıklanabilir olana kadar bekler; indirme komutu dosya diske yazılana kadar döner. Böylece betiğe serpiştirilen sabit bekleme çağrıları ve onların yarattığı rastgele hatalar ortadan kalkar.

Oturum açma ve oturumu saklama

Oturum saklama, her çalıştırmada giriş formunu yeniden doldurmak yerine tarayıcının çerezlerini ve yerel depolamasını bir dosyaya yazıp sonraki çalıştırmada aynı dosyadan yüklemektir. Playwright bu özelliği storage_state adıyla sunar. Portal her girişte doğrulama kodu gönderse bile bu adım yalnız oturum düştüğünde tekrarlanır; giriş denemesi sayısı azalır ve hesabın şüpheli etkinlik nedeniyle kilitlenme olasılığı düşer.

import os
from pathlib import Path
from playwright.sync_api import sync_playwright

PORTAL = "https://portal.ornek-tedarikci.com"
DURUM_DOSYASI = Path("oturum/portal.json")


def oturum_ac(context, kullanici, parola):
    page = context.new_page()
    page.goto(PORTAL + "/giris")
    page.get_by_label("E-posta").fill(kullanici)
    page.get_by_label("Parola").fill(parola)
    page.get_by_role("button", name="Giriş yap").click()
    page.wait_for_url("**/panel")
    DURUM_DOSYASI.parent.mkdir(exist_ok=True)
    context.storage_state(path=str(DURUM_DOSYASI))
    page.close()


def baglan(p):
    browser = p.chromium.launch(headless=True)
    if DURUM_DOSYASI.exists():
        context = browser.new_context(storage_state=str(DURUM_DOSYASI))
    else:
        context = browser.new_context()
        oturum_ac(context, os.environ["PORTAL_KULLANICI"], os.environ["PORTAL_PAROLA"])
    return browser, context


with sync_playwright() as p:
    browser, context = baglan(p)
    page = context.new_page()
    page.goto(PORTAL + "/panel")
    if "/giris" in page.url:
        DURUM_DOSYASI.unlink(missing_ok=True)
        oturum_ac(context, os.environ["PORTAL_KULLANICI"], os.environ["PORTAL_PAROLA"])
    print("oturum hazır")
    browser.close()

Betik önce kayıtlı durumu dener; panel yerine giriş sayfasına yönlendirilirse oturumun düştüğünü anlar, durum dosyasını siler ve yeniden giriş yapar. Kullanıcı adı ve parola koda değil ortam değişkenine yazılır. Seçiciler, kullanıcının ekranda gördüğü etiketlere bağlıdır; form alanlarının kimlikleri değişse bile etiket metni aynı kaldığı sürece betik çalışmaya devam eder.

İki faktörlü doğrulama isteyen portallarda ilk girişi görünür tarayıcı modunda, bir kişinin kodu girmesiyle yapın ve durum dosyasını o oturumdan üretin. Sonraki çalıştırmalar bu dosyayı kullanır. Durum dosyası bir oturum anahtarı taşıdığı için parola kadar hassastır: sürüm kontrolüne eklenmez, yalnız betiği çalıştıran kullanıcının okuyabileceği izinle saklanır ve işten ayrılan bir çalışanın hesabıyla üretilmişse yeniden oluşturulur.

Dosya indirme ve indirilen dosyayı doğrulama

İndirme doğrulaması, portaldan gelen dosyanın gerçekten beklenen içerik olduğunu veri hattına girmeden önce kontrol etmektir. Bir indirme düğmesi, oturum düşmüşse giriş sayfasının HTML'ini, sunucu hata vermişse boş bir dosyayı aynı adla verebilir. Dosya boyutu, ilk satırdaki başlıklar ve satır sayısı gibi basit kontroller, bozuk bir dosyanın raporun altına sessizce girmesini engeller.

import csv
import time
from pathlib import Path

PORTAL = "https://portal.ornek-tedarikci.com"
HEDEF = Path("gelen/ekstre")
BEKLENEN_BASLIK = ["Tarih", "Belge No", "Tutar", "Para Birimi"]


def indir(page, donem):
    page.goto(PORTAL + "/raporlar")
    page.get_by_label("Dönem").select_option(donem)
    with page.expect_download(timeout=60000) as bilgi:
        page.get_by_role("button", name="CSV indir").click()
    gecici = HEDEF / (donem + ".indiriliyor")
    bilgi.value.save_as(gecici)
    return gecici


def dogrula(dosya):
    if dosya.stat().st_size == 0:
        raise ValueError("boş dosya")
    with open(dosya, encoding="utf-8-sig", newline="") as f:
        baslik = next(csv.reader(f, delimiter=";"))
    if baslik != BEKLENEN_BASLIK:
        raise ValueError("beklenmeyen başlık: %r" % baslik)


def guvenli_indir(page, donem, deneme=3):
    HEDEF.mkdir(parents=True, exist_ok=True)
    for i in range(1, deneme + 1):
        try:
            gecici = indir(page, donem)
            dogrula(gecici)
            kalici = HEDEF / (donem + ".csv")
            gecici.replace(kalici)
            return kalici
        except Exception as hata:
            page.screenshot(path="hata-%s-%d.png" % (donem, i))
            print("deneme %d başarısız: %s" % (i, hata))
            time.sleep(5 * i)
    raise RuntimeError("%s indirilemedi" % donem)

Dosya önce geçici bir adla kaydedilir, doğrulamadan geçince kalıcı adına taşınır. Böylece hattın sonraki adımı yarım ya da bozuk bir dosyayı asla görmez. Her başarısız denemede ekran görüntüsü alınır; arayüz değiştiğinde hatanın nedeni log satırından değil o görüntüden anlaşılır. Ekran görüntüsünde kişisel veri görünebileceği için bu dosyaların da saklama süresi ve erişim izni belirlenmelidir.

Yeniden deneme sayısı ve bekleme süresi bilinçli olarak küçük tutuldu. Portal gerçekten çalışmıyorsa on kez üst üste denemek yalnız sunucuyu yorar ve hesabınızı dikkat çekici hale getirir. Yeniden deneme ve geri çekilme stratejisinin genel ilkelerini API entegrasyon desenleri yazısında ayrıntılı anlattım; aynı ilkeler tarayıcı üzerinden yapılan isteklerde de geçerlidir.

Kırılganlığı azaltmak: seçici, bekleme ve değişiklik alarmı

Kırılganlık, tarayıcı otomasyonunun en büyük maliyetidir: portal bir düğmenin adını değiştirdiğinde ya da tabloyu yeni bir bileşenle yeniden yazdığında betik durur. Tamamen önlenemez, ancak seçicilerin nasıl yazıldığı ve hatanın nasıl raporlandığı, bir değişikliğin bir saatlik mi yoksa bir günlük mü iş yaratacağını belirler.

  • Seçiciyi kullanıcının gördüğü şeye bağlayın: rol ve etiket tabanlı seçiciler, iç içe geçmiş etiket yollarına bağlı seçicilerden çok daha uzun yaşar.
  • Birden fazla eşleşme olduğunda hata alın, ilkini sessizce seçmeyin; Playwright'ın katı eşleşme kuralı bunu varsayılan olarak yapar.
  • Sayfa yüklenmesini URL değişimi ya da bir başlığın görünmesiyle bekleyin; sabit saniye beklemek yavaş ağda hataya, hızlı ağda zaman kaybına yol açar.
  • Portalın gösterdiği "şartlar güncellendi" ya da "yeni arayüzü deneyin" gibi ara pencereleri açıkça ele alın; aksi halde tıklama onların arkasında kalır.
  • Betiği tek bir dönemi indiren küçük bir komut olarak tasarlayın; dönem listesini dışarıdan verin, böylece eksik bir ay elle tamamlanabilir.

Betik durduğunda sessiz kalmaması için çıkış kodunu ve ekran görüntüsünü bir bildirim kanalına bağlayın. Elle yapılan işin yerini alan bir otomasyonun en kötü durumu çalışmaması değil, çalışıyor gibi görünüp eski dosyayı bırakmasıdır. İndirilen dosyanın tarih damgasını ve satır sayısını her çalıştırmada loga yazmak, bu durumu yakalamanın en ucuz yoludur.

Hukuki ve etik sınır

Hukuki sınır, tarayıcı otomasyonunda teknik sınırdan önce gelir: betik yalnız sizin hesabınızın görmeye yetkili olduğu veriyi, kullanım şartlarının izin verdiği biçimde almalıdır. Kendi işletmenizin faturalarını, ekstrelerini ya da sipariş listesini indirmek ile başkalarına ait kayıtları toplamak hukuken farklı şeylerdir. Kişisel veri içeren dosyalar 6698 sayılı Kişisel Verilerin Korunması Kanunu kapsamında işlendiğinden, verinin nerede saklandığı ve kimin eriştiği baştan belirlenmelidir.

Kullanım şartlarında otomatik erişimi açıkça yasaklayan portallar vardır; böyle bir maddeyi görmezden gelmek hesabın kapatılmasına ve sözleşme ihlaline yol açabilir. Emin olmadığınızda sağlayıcıya yazılı olarak sorun ve cevabı saklayın. Konunun Türkiye özelindeki çerçevesini web scraping'de etik ve hukuki sınırlar yazısında ele aldım; oradaki kurallar bir portala giriş yapan betik için de aynen geçerlidir.

Sağlayıcının altyapısına saygı da bu sınırın parçasıdır. Betiği iş saatleri dışında, günde bir kez ve tek oturumla çalıştırmak hem sunucuyu yormaz hem de hesabınızın bot olarak işaretlenmesini önler. Portal bir hız sınırı ya da insan doğrulaması gösteriyorsa bunu aşmaya çalışmak yerine durun; o sinyal, sağlayıcının otomatik erişimi istemediğinin açık ifadesidir.

Sonuç

Playwright ile tarayıcı otomasyonu, API'si olmayan bir portaldan düzenli veri almanın güvenilir bir yoludur; koşulu oturumu saklamak, indirilen dosyayı doğrulamak ve arayüz değiştiğinde anlaşılır biçimde durmaktır. Betiğin kendisi kısadır; asıl iş çevresindeki doğrulama, bildirim ve hukuki çerçeve katmanındadır.

İlk adım olarak elle yaptığınız en sık indirme işini seçin, adımlarını yazın ve yukarıdaki iskeletle tek bir dönemi indiren betiği kurun. Birkaç hafta cron ile çalıştırıp loglarını izledikten sonra diğer portalları ekleyin. Portal otomasyonu kurarken bir yol haritasına ya da mevcut hattınızın gözden geçirilmesine ihtiyaç duyarsanız iletişim bölümünden ulaşabilirsiniz.