Web scraping'de etik ve hukuki sınırlar (Türkiye perspektifi)
Web scraping'de robots.txt, kullanım koşulları, hız sınırlama, kimlik belirtme ve kişisel veri ayrımı: teknik olarak kibar kazımanın pratik kuralları.
Web scraping, yani bir web sitesindeki veriyi programla toplama işi, veri projelerinin çoğunda bir noktada gündeme geliyor. Fiyat karşılaştırması, rakip ürün listesi, bayi ağı haritası, ihale duyurularının takibi... Kaynağın resmi bir API'si yoksa akla ilk gelen çözüm HTML'i çekip ayrıştırmak oluyor. Teknik tarafı görece kolay; asıl zor kısım, bunu karşı tarafa zarar vermeden, kimseyi rahatsız etmeden ve kişisel veri toplamadan yapmak.
Bu yazıda kendi projelerimde uyguladığım "kibar kazıma" pratiğini anlatacağım: robots.txt ve kullanım koşullarını nasıl okuduğumu, isteklerimi nasıl yavaşlatıp kimliğimi nasıl belirttiğimi, kamuya açık veri ile kişisel veri arasındaki çizgiyi nerede çektiğimi. Kod örnekleri Python; requests dışında standart kütüphane yetiyor.
Baştan söyleyeyim: bu bir hukuki tavsiye değil. Ben veri ve otomasyon tarafındayım; Türkiye'de KVKK ve ilgili mevzuat karşısında somut bir projede neyin yapılabileceğini hukukçuya sormak gerekiyor. Burada anlattığım şey, o görüşmeye teknik olarak hazırlıklı gitmek ve daha baştan kimseye zarar vermeyen bir kazıyıcı kurmak.
robots.txt ve kullanım koşulları: sitenin kapısındaki yazı
robots.txt, site sahibinin otomatik istemcilere "buraya girme, şuraya bu hızla gel" dediği düz metin bir dosya. Teknik olarak kimseyi engellemez; bir nezaket sözleşmesidir. Tam da bu yüzden ona uymak, karşı tarafa "kurallarını okudum" demenin en somut yolu. Ben bir siteyi taramadan önce üç şeye bakıyorum:
- robots.txt: Hangi yollar
Disallowaltında, benim ajanım için özel bir kural var mı,Crawl-delaybelirtilmiş mi. Google bu yönergeyi dikkate almıyor ama site sahibi yazmışsa ben uyuyorum; yazan kişi sunucusunun kaldırabileceğini benden iyi biliyor. - Kullanım koşulları: Çoğu sitede "otomatik araçlarla veri toplama" ifadesi açıkça geçer. Yasaklanıyorsa robots.txt izin veriyor olsa bile o siteyi projeden çıkarıyorum ya da site sahibine yazıp izin istiyorum. Şaşırtıcı sıklıkla olumlu cevap geliyor; çoğu zaman zaten bir dışa aktarma seçenekleri oluyor.
- Resmi API var mı: Kazımak yerine API kullanmak neredeyse her zaman daha sağlam. Google Maps'teki işletme verisi için HTML kazımak yerine Places API kullandığım yaklaşımı Google Maps ile lead keşfi yazısında anlatmıştım; kota ve maliyet var ama uyumluluk derdi yok.
robots.txt kontrolünü elle yapmak yerine kazıyıcının içine gömüyorum. Python'un standart kütüphanesindeki urllib.robotparser bunun için yeterli:
from urllib import robotparser
from urllib.parse import urlparse
USER_AGENT = "ekrem-veri-bot/1.0 (+https://ekremdegirmenci.com)"
def robots_kontrol(url: str, ajan: str = USER_AGENT):
"""(izin_var_mi, crawl_delay_saniye) dondurur."""
parca = urlparse(url)
rp = robotparser.RobotFileParser()
rp.set_url(f"{parca.scheme}://{parca.netloc}/robots.txt")
try:
rp.read()
except OSError:
# Dosyaya hic ulasilamadi: izin var sayiyorum ama yine de yavas gidiyorum
return True, None
return rp.can_fetch(ajan, url), rp.crawl_delay(ajan)
izin, gecikme = robots_kontrol("https://ornek-site.com/urunler?sayfa=2")
if not izin:
raise SystemExit("robots.txt bu yolu yasakliyor, kaziyici durdu.")
print("Sitenin istedigi bekleme:", gecikme or "belirtilmemis")
Dikkat edilecek bir davranış var: robots.txt isteği 401 ya da 403 dönerse kütüphane her yolu yasak sayıyor, 404'te ise her yola izin verilmiş kabul ediyor; 5xx'te dosya hiç okunmamış sayılıyor ve can_fetch temkinli davranıp yanlış dönüyor. Yani "dosya yok" ile "dosya var ama sana kapalı" farklı sonuç üretiyor. Bunu bilmeden hata ayıklamak bana bir öğleden sonraya mal olmuştu.
Hız sınırlama ve kimlik belirtme
Kazıyıcı yazan çoğu kişinin ilk hatası, her şeyi mümkün olduğunca hızlı çekmeye çalışmak. Oysa karşıdaki sunucu insan tarayıcılarından gelecek yüke göre boyutlandırılmış; saniyede onlarca istek, küçük bir e-ticaret sitesinde gerçek müşterileri yavaşlatabilir. Benim uyguladığım kurallar şunlar:
- Tek hat, tek site: Aynı alan adına eşzamanlı istek göndermiyorum. Paralellik gerekiyorsa farklı siteler arasında dağıtıyorum, aynı sitede değil.
- İstekler arası bekleme: robots.txt bir Crawl-delay verdiyse onu, vermediyse en az birkaç saniye. Araya rastgele küçük bir pay ekliyorum ki istekler saat gibi düzenli görünmesin; amaç gizlenmek değil, sunucuya ani yük tepeleri yaratmamak.
- 429 ve 503'e saygı: Sunucu "yavaşla" diyorsa (
429 Too Many Requests) ya daRetry-Afterbaşlığı gönderiyorsa söylediği süre kadar bekliyorum, ardından katlanarak artan gecikmeyle tekrar deniyorum. 403 ya da 404 geldiğinde ise ısrar etmiyorum. - Dürüst User-Agent: Kendimi Chrome gibi tanıtmıyorum. Ajan adının içinde proje adı ve bana ulaşılabilecek bir adres var. Bir site yöneticisi loglarında bu trafiği görüp rahatsız olursa bana yazabilsin istiyorum; engellemeye gitmeden konuşarak çözmenin yolu bu.
- Bir kez çek, sakla: Aynı sayfayı geliştirme sırasında elli kez çekmek yerine ham HTML'i diske yazıp ayrıştırmayı yerelde tekrarlıyorum. Sunucuya en az yükü bindiren istek, hiç gönderilmeyen istektir.
- Yoğun saatlerden kaçınmak: Türkiye'ye hizmet veren bir site için gece saatleri sunucunun en boş olduğu zaman. Zamanlanmış görevleri oraya koyuyorum.
Bunları her projede yeniden yazmamak için küçük bir "kibar istemci" sınıfı kullanıyorum. İstekler arası süreyi kendisi tutuyor, 429 ve 503'te geri çekiliyor, 4xx'te vazgeçiyor:
import random
import time
from pathlib import Path
import requests
USER_AGENT = "ekrem-veri-bot/1.0 (+https://ekremdegirmenci.com)"
class KibarIstemci:
def __init__(self, taban_bekleme: float = 3.0, azami_deneme: int = 4):
self.oturum = requests.Session()
self.oturum.headers.update({
"User-Agent": USER_AGENT,
"Accept-Language": "tr-TR,tr;q=0.9",
})
self.taban_bekleme = taban_bekleme
self.azami_deneme = azami_deneme
self._son_istek = 0.0
def _bekle(self, saniye: float):
gecen = time.monotonic() - self._son_istek
if gecen < saniye:
time.sleep(saniye - gecen)
def getir(self, url: str, crawl_delay=None):
bekleme = max(self.taban_bekleme, crawl_delay or 0) + random.uniform(0, 1)
for deneme in range(self.azami_deneme):
self._bekle(bekleme)
yanit = self.oturum.get(url, timeout=20)
self._son_istek = time.monotonic()
if yanit.status_code == 200:
return yanit
if yanit.status_code in (429, 503):
sonra = yanit.headers.get("Retry-After", "")
sure = float(sonra) if sonra.isdigit() else bekleme * (2 ** deneme)
print(f"{yanit.status_code} geldi, {sure:.0f} sn bekliyorum")
time.sleep(sure)
continue
if 400 <= yanit.status_code < 500:
return None # 403/404/410: israr etmiyorum
return None
istemci = KibarIstemci()
yanit = istemci.getir("https://ornek-site.com/urunler?sayfa=2", crawl_delay=10)
if yanit is not None:
Path("ham").mkdir(exist_ok=True)
Path("ham/urunler-2.html").write_text(yanit.text, encoding="utf-8")
Örnekte Retry-After yalnızca saniye biçiminde geldiğinde okunuyor; tarih biçiminde gelirse katlanan bekleme devreye giriyor. Üretimde bir de günlük toplam istek üst sınırı koyuyorum; bir hata yüzünden kazıyıcı döngüye girerse sabaha kadar aynı siteyi dövmesin.
Kamuya açık veri ile kişisel veri ayrımı
En çok yanlış anlaşılan konu bu. "Sayfa herkese açık, o hâlde içindeki her şeyi toplayabilirim" düşüncesi teknik olarak doğru, hukuki ve etik olarak değil. Bir verinin tarayıcıda görünüyor olması onun kişisel veri olmadığı anlamına gelmiyor. Çalışırken kullandığım kaba ayrım şu:
- Tüzel kişiye ait veri: Firma adı, adresi, kategorisi, ürün listesi, fiyat, çalışma saatleri. Bunlar bir insanla değil bir kurumla ilgili; kişisel veri kapsamına genelde girmiyor. Projelerimin büyük kısmı bu kümede kalıyor.
- Gerçek kişiye ait veri: Ad soyad, telefon, e-posta, profil fotoğrafı, kullanıcı adı, yorumlar, konum geçmişi. Herkese açık bir profilde dursa bile kişisel veri. Bir yorumun altındaki isim de, şahıs şirketi sahibinin cep numarası da bu kümede.
- Gri bölge: Firma sayfasındaki "yetkili kişi" alanı, meslek ağı profilleri, forum kullanıcı adları. Bunları toplamıyorum; ihtiyaç varsa müşteriyle birlikte hukukçuya soruyoruz.
Türkiye'de KVKK'da verinin "ilgili kişinin kendisi tarafından alenileştirilmiş olması" diye bir işleme şartı var; ama okuduğum Kurul kararlarında bu şart, alenileştirme amacıyla sınırlı yorumlanıyor. Yani birinin kendi sitesine koyduğu telefon numarasını toplu SMS listesine eklemek "zaten açıktı" diyerek meşrulaşmıyor. Tekrar ediyorum, bu benim okumam; nihai yorumu hukukçu yapmalı. Benim teknik cevabım ise basit: kişisel veriyi hiç toplamamak. Ayrıştırıcıda alan beyaz listesi kullanıyorum, listede olmayan hiçbir alan diske yazılmıyor. Serbest metin alanlarına sızan cep numarası ve e-posta için de kaba bir süzgeç var:
import re
IZINLI_ALANLAR = {"firma_adi", "sehir", "ilce", "kategori", "web_sitesi", "urun_sayisi"}
CEP_TELEFONU = re.compile(r"(\+90|0)?\s?5\d{2}[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}")
EPOSTA = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
def kisisel_veri_izi(metin: str) -> bool:
return bool(CEP_TELEFONU.search(metin) or EPOSTA.search(metin))
def temizle(kayit: dict) -> dict:
temiz = {k: v for k, v in kayit.items() if k in IZINLI_ALANLAR}
for k, v in temiz.items():
if isinstance(v, str) and kisisel_veri_izi(v):
temiz[k] = "" # sizmis cep no / e-posta diske hic yazilmaz
return temiz
ham = {"firma_adi": "Örnek Yapı Ltd.", "yetkili": "A. B.", "sehir": "Bursa",
"kategori": "inşaat malzemeleri 0532 000 00 00"}
print(temizle(ham))
Süzgeç bilerek yalnızca cep telefonu desenini arıyor; sabit hat numarası genelde işletmeye aittir. Bu üçüncü örnek, "toplayıp sonra silmek" ile "hiç toplamamak" arasındaki farkı gösteriyor. Geliştirme aşamasında ham HTML sakladığınızda o dosyalarda da kişisel veri olabileceğini unutmayın; ham arşivi proje bitince siliyorum.
Kibar kazıyıcı kontrol listesi
Yeni bir kazıma işine başlarken müşteriyle birlikte üzerinden geçtiğim liste:
- Resmi API ya da dışa aktarma seçeneği var mı? Varsa kazıma yok.
- robots.txt ve kullanım koşulları okundu mu, projede tarihli bir not var mı?
- Giriş, ödeme duvarı ya da CAPTCHA arkasındaki hiçbir sayfa hedefte değil; bunları aşmak için proxy havuzu ya da tarayıcı parmak izi taklidi kullanılmıyor.
- Aynı alan adına eşzamanlılık bir, istekler arası bekleme robots.txt'deki değer ya da en az birkaç saniye.
- User-Agent'ta proje adı ve iletişim adresi var.
- Toplanan alanlar beyaz listede; ad, telefon, e-posta gibi kişisel alanlar listede yok.
- Ham HTML arşivi tarihli ve proje sonunda siliniyor; çıktıya kimin eriştiği belli.
- Çıktının kalitesi ayrı bir adımda denetleniyor. Kazıma çıktısında en sık gördüğüm sorun kopya kayıtlar ve bozuk karakterler; bunun için Excel kalite kontrol otomasyonu yazısındaki kontrolleri neredeyse olduğu gibi kullanıyorum.
Bu listenin amacı bürokrasi değil; iş sonradan sorgulanırsa "biz şunlara baktık" diyebilmek. Gördüğüm kadarıyla site sahiplerinden itiraz, hemen her zaman ya çok hızlı giden ya da kimliğini gizleyen kazıyıcılara geliyor; kendini tanıtıp yavaş giden bota nadiren biri kızıyor.
Sonuç
Web scraping'de teknik başarı, sayfayı çekip ayrıştırabilmek değil. Asıl başarı, aylar sonra o veriye dayanan bir karar verilirken kimsenin "bunu nasıl topladık" diye endişelenmemesi. robots.txt ve kullanım koşullarını okumak, yavaş gitmek, kim olduğunu söylemek ve kişisel veriyi hiç toplamamak; bu dört alışkanlık kazıyıcıyı hem daha dayanıklı hem daha savunulabilir kılıyor. Mevzuat tarafında ise hukukçuya danışmayı projenin ilk haftasına alın, son haftasına değil.
Elinizde kazınması gereken bir kaynak varsa ve bunu hem teknik hem etik açıdan sağlam kurmak istiyorsanız, iletişim bölümünden yazabilirsiniz; ilk görüşmede genellikle önce "buna gerçekten kazıma mı gerekiyor" sorusunu konuşuyoruz.