Web sayfası değişiklik izleme: parmak izi, fark kaydı ve gürültüsüz bildirim
Web sayfası değişiklik izleme: HTML'den gürültüsüz parmak izi çıkarma, SQLite geçmişiyle okunur fark üretme, cron ve eşikle gürültüsüz bildirim, hukuki sınırlar.
Web sayfası değişiklik izleme, rakibin fiyat listesi, tedarikçinin teslimat koşulları, bir kurumun mevzuat duyurusu ya da kendi sitenizin kritik sayfaları gibi belirli adresleri düzenli aralıklarla çekip içerik değiştiğinde haber veren küçük bir otomasyondur. Küçük ve orta ölçekli işletmelerde bu iş çoğu zaman bir çalışanın sabahları beş on sayfayı elle açıp göz gezdirmesiyle yürür. Değişiklik gözden kaçar; kaçmadığında da tam olarak ne zaman olduğu bilinmez.
Hazır izleme servisleri var, ancak izlenecek sayfa sayısı arttıkça abonelik maliyeti büyür ve hangi değişikliğin anlamlı sayılacağına çoğu zaman siz karar veremezsiniz. Standart kütüphaneyle yazılmış yüz satırlık bir Python betiği aynı işi görür, geçmişi kendi veri tabanınızda tutar ve gürültü kurallarını işinize göre ayarlamanıza izin verir.
Bu yazıda dört adımda böyle bir izleyici kuruyoruz: sayfanın gürültüden arındırılmış parmak izini çıkarmak, yeni çekimi SQLite geçmişiyle karşılaştırıp okunur bir fark üretmek, zamanlamayı ve bildirimi gürültüsüz ayarlamak, hukuki ve etik sınırların içinde kalmak. Kod örneklerinin dış bağımlılığı yoktur; Python 3.8 ve üzeri sürümlerde olduğu gibi çalışır.
Sayfanın gürültüsüz parmak izini çıkarmak
Sayfa parmak izi, HTML'den görünür metni ayıklayıp her istekte değişen parçaları temizledikten sonra alınan özet değeridir; iki çekim arasında bu değer aynıysa sayfa aynı sayılır. Ham HTML'in özetini almak işe yaramaz, çünkü çoğu site her istekte farklı oturum kimliği, CSRF belirteci, reklam betiği ya da saat damgası döndürür ve izleyici her turda sahte alarm üretir.
Temizlenmesi gereken gürültü kaynakları çoğu sitede aynıdır:
script,stylevenoscriptblokları: içerikleri okura görünmez ama sürüm numarası ve belirteç taşır.- Boşluk ve satır sonu farkları: şablon motorları aynı içeriği farklı girintiyle üretebilir.
- Tarih ve saat damgaları: "son güncelleme", "bugün" gibi alanlar içerik değişmeden değişir.
- Sayaçlar ve kişiselleştirme: "şu anda 12 kişi bakıyor" ya da "size özel" türü alanlar her istekte farklıdır.
import hashlib
import re
import urllib.request
from html.parser import HTMLParser
class MetinAyiklayici(HTMLParser):
"""script, style ve noscript dışındaki görünür metni toplar."""
ATLA = {"script", "style", "noscript", "template"}
def __init__(self):
super().__init__()
self.parcalar = []
self.derinlik = 0
def handle_starttag(self, tag, attrs):
if tag in self.ATLA:
self.derinlik += 1
def handle_endtag(self, tag):
if tag in self.ATLA and self.derinlik:
self.derinlik -= 1
def handle_data(self, data):
if not self.derinlik:
self.parcalar.append(data)
def normallestir(html):
ayiklayici = MetinAyiklayici()
ayiklayici.feed(html)
metin = " ".join(ayiklayici.parcalar)
metin = re.sub(r"\s+", " ", metin).strip()
# tarih ve saat damgaları her istekte değişir; parmak izinden çıkar
metin = re.sub(r"\d{1,2}[./]\d{1,2}[./]\d{2,4}( \d{1,2}:\d{2})?", "", metin)
return metin
def parmak_izi(url):
istek = urllib.request.Request(
url, headers={"User-Agent": "degisiklik-izleyici/1.0 (+mailto:[email protected])"}
)
with urllib.request.urlopen(istek, timeout=30) as cevap:
ham = cevap.read().decode("utf-8", errors="replace")
metin = normallestir(ham)
return hashlib.sha256(metin.encode("utf-8")).hexdigest(), metin
Fonksiyon özetin yanında metni de döndürür, çünkü bir sonraki adımda farkı göstermek için özet tek başına yetmez. Kullanıcı aracısı başlığına iletişim adresi yazmak isteğe bağlı bir incelik değildir; site sahibi günlüklerinde düzenli trafiği gördüğünde kime yazacağını bilmelidir. Sayfanın yalnız bir bölümü ilginizi çekiyorsa metni ayıklamadan önce ilgili kapsayıcıyı seçmek sahte alarmı daha da azaltır; standart kütüphanede CSS seçici olmadığından bu adım için selectolax ya da BeautifulSoup eklenebilir.
Değişikliği kaydetmek ve okunur fark üretmek
Değişiklik kaydı, her çekimin özetini ve metnini zaman damgasıyla saklayan, yeni çekimi yalnız son kayıtla karşılaştıran ve yalnız değişiklik olduğunda satır ekleyen bir geçmiş tablosudur. Özet eşitse hiçbir şey yazılmaz; böylece tablo izlenen sayfanın gerçek sürüm geçmişine dönüşür ve aylar sonra fiyatın hangi gün değiştiğini tek sorguyla bulabilirsiniz.
import difflib
import sqlite3
from datetime import datetime, timezone
VT = "izleme.db"
def tablo_hazirla(baglanti):
baglanti.execute(
"""CREATE TABLE IF NOT EXISTS anlik (
id INTEGER PRIMARY KEY,
url TEXT NOT NULL,
alinma TEXT NOT NULL,
ozet TEXT NOT NULL,
metin TEXT NOT NULL
)"""
)
def son_kayit(baglanti, url):
satir = baglanti.execute(
"SELECT ozet, metin FROM anlik WHERE url = ? ORDER BY id DESC LIMIT 1", (url,)
).fetchone()
return satir
def kaydet_ve_karsilastir(url, ozet, metin):
with sqlite3.connect(VT) as baglanti:
tablo_hazirla(baglanti)
onceki = son_kayit(baglanti, url)
if onceki and onceki[0] == ozet:
return None # değişiklik yok, kayıt da ekleme
simdi = datetime.now(timezone.utc).isoformat(timespec="seconds")
baglanti.execute(
"INSERT INTO anlik (url, alinma, ozet, metin) VALUES (?, ?, ?, ?)", (url, simdi, ozet, metin)
)
if onceki is None:
return "ilk kayıt alındı"
fark = difflib.unified_diff(
onceki[1].split(". "), metin.split(". "),
fromfile="önceki", tofile="şimdi", lineterm="", n=0,
)
return "\n".join(satir for satir in fark if not satir.startswith(("---", "+++", "@@")))
Fark cümle düzeyinde üretiliyor. Satır düzeyi tek paragrafa sıkışmış metinde işe yaramaz, karakter düzeyi ise okunmaz. difflib.unified_diff çıktısından başlık satırları atılınca geriye eksi ile başlayan eski cümleler ve artı ile başlayan yeni cümleler kalır; bildirimde okura tam olarak bu gösterilir.
Metnin tamamını saklamak disk açısından kaygı yaratabilir, ancak kayıt yalnız değişiklikte eklendiği için tablo yavaş büyür. Çok uzun sayfalarda metni zlib ile sıkıştırıp BLOB olarak yazmak yeterli bir önlemdir. Geçmişin tek bir SQLite dosyasında durması, yedeklemeyi de dosya kopyalamaya indirger.
Zamanlama, eşik ve bildirim
İzleme sıklığı, sayfanın gerçek değişim hızına göre seçilen çekim aralığıdır; fiyat listesi için günde bir, mevzuat sayfası için haftada bir çekim çoğu durumda yeterlidir. Daha sık çekmek hem karşı sunucuyu yorar hem de engellenme olasılığını artırır. Günde bir ile başlayıp geçmiş tablosundaki değişim aralıklarına bakarak sıklığı sonradan ayarlamak en güvenli yoldur.
# her gün 07:10'da çalıştır; çıktı ve hatalar günlük dosyasına
10 7 * * * cd /opt/izleyici && flock -n izleyici.lock python3 izle.py >> izleyici.log 2>&1
Betiğin ana döngüsü basittir: izlenecek adresler bir metin dosyasında satır satır durur, her adres için parmak izi alınır, kayıt fonksiyonu çağrılır ve dönen fark boş değilse bildirim listesine eklenir. Adres listesini koda gömmek yerine dosyada tutmak, yeni bir sayfa eklemek için betiğe dokunma gereğini ortadan kaldırır; iş birimi listeyi kendisi yönetebilir.
Zamanlayıcı satırındaki flock, önceki tur bitmeden yenisinin başlamasını engeller; aynı sayfayı iki sürecin aynı anda çekmesi hem gereksiz trafik hem de çift kayıt demektir. Kilit, günlük ve güvenli yeniden çalıştırma kuralları için zamanlanmış iş tasarımı yazısındaki düzen burada da aynen geçerlidir.
Gürültüyü bildirim katmanında bir kez daha süzmek gerekir:
- Eşik: fark yalnız birkaç karakterse ya da yalnız rakam içeren bir sayaç değiştiyse bildirim gönderme, geçmişe yine de yaz.
- Toplama: bir turda değişen tüm sayfaları tek iletide gönder; on ayrı e-posta on ayrı dikkat dağıtıcıdır.
- Hata ayrımı: HTTP 404 ya da 5xx cevabını değişiklik olarak kaydetme; ayrı bir hata durumu tut ve ardışık üç başarısız turdan sonra uyar. Aksi halde geçici bir kesinti "sayfa boşaldı" alarmına dönüşür.
- Kanal: standart kütüphanedeki
smtplibile e-posta ya da ekip sohbet aracının gelen webhook adresine tek birPOSTisteği yeterlidir.
Bant genişliğini korumak için koşullu istek kullanılabilir: sunucu ETag ya da Last-Modified başlığı döndürüyorsa bir sonraki çekimde If-None-Match ya da If-Modified-Since gönderilir ve sayfa değişmediyse gövdesiz bir 304 cevabı alınır. Dinamik sayfaların çoğu bu başlıkları vermez; o durumda parmak izi karşılaştırması zaten işi görür.
Hukuki ve etik sınırlar
Değişiklik izlemenin hukuki çerçevesi, klasik web kazımayla aynıdır: herkese açık bir sayfayı makul sıklıkla okumak çoğu durumda sorun çıkarmaz, ancak kullanım koşullarını ihlal eden erişim, teknik engelleri aşma ve kişisel veri toplama ayrı sorumluluklar doğurur. İzlediğiniz sayfa fiyat ve koşul içeriyorsa ortada kişisel veri yoktur; yorum, profil ya da üye listesi içeriyorsa KVKK kapsamına girersiniz.
Uygulamada şu çizgilerin içinde kalmak yeterli olur:
robots.txtdosyasına bak; sayfa taramaya kapatılmışsa izleyiciye ekleme.- Giriş, CAPTCHA ya da hız sınırı gibi teknik engelleri aşmaya çalışma; engel, site sahibinin açık tercihidir.
- İçeriği yeniden yayımlama; fark raporu iç kullanım içindir, rakibin metnini kopyalayıp kendi sitene koymak telif sorunu doğurur.
- İstek sıklığını insan davranışının altında tut ve kullanıcı aracısında iletişim adresi bırak.
- Kişisel veri içeren sayfaları izlemek gerekiyorsa önce hukuki dayanağı ve saklama süresini belirle.
Türkiye'deki mevzuat, mahkeme kararları ve pratik çizgiler web scraping'de etik ve hukuki sınırlar yazısında ayrıntılı ele alınmıştı; izleme kurmadan önce o yazıdaki kontrol listesini geçmek iyi bir alışkanlıktır.
Sonuç: tek sayfa, tek cron satırıyla başlayın
Web sayfası değişiklik izleme, üç parçadan oluşan küçük bir sistemdir: gürültüden arındırılmış parmak izi, yalnız değişiklikte büyüyen bir geçmiş tablosu ve eşikle süzülmüş bildirim. Üçü de standart kütüphaneyle kurulur; asıl emek hangi sayfaların gerçekten karar değiştirdiğini seçmeye ve sahte alarmı yok edecek temizleme kurallarını bulmaya gider.
İlk adım olarak işinizi en çok etkileyen tek bir sayfayı seçin, yukarıdaki iki fonksiyonu tek bir betikte birleştirin ve günde bir çalışan cron satırıyla bir hafta boyunca yalnız geçmiş biriktirin. Hafta sonunda tabloya bakın: kaç gerçek değişiklik var, kaç sahte alarm üretildi, hangi temizleme kuralı eksik. Bildirimi ancak bu sorular cevaplandıktan sonra açın.
Rakip, tedarikçi ya da mevzuat takibini otomasyona bağlamak istiyorsanız, izlenecek sayfaları ve gürültü kurallarını birlikte belirleyebiliriz: iletişim.