Global Data Enrichment Engine: Şirket Verilerini Otomatik Zenginleştirme
Python ile veri zenginleştirme pipeline kurulumu: SQLite iş kuyruğu, robots.txt uyumu, çok kaynaklı birleştirme ve veri kalitesi skoru bir arada.
Danışmanlık projelerinde en sık karşılaştığım tablo şu: elimde binlerce satırlık bir şirket listesi var — isim, ülke, bazen bir web sitesi — ama e-posta yok, sosyal medya yok, adresin doğru olup olmadığı belirsiz. Veri zenginleştirme (data enrichment) tam bu noktada devreye giriyor: eldeki ham kayıtları otomatik bir süreçle iletişim bilgileri, sosyal medya bağlantıları ve doğrulanmış alan adlarıyla tamamlamak.
Bu yazıda kendi kullandığım Python veri pipeline mimarisini anlatacağım: SQLite üzerinde idempotent bir iş kuyruğu, web sitelerinden nazikçe veri çıkaran bir toplayıcı, çok kaynaklı birleştirme katmanı ve her kayda verdiğim bir veri kalitesi skoru. Hazır SaaS araçları yerine neden kendi motorumu yazdığımı da yeri geldikçe açıklayacağım; kısa cevap, şirket verisi zenginleştirme işinde kontrolün ve maliyetin bir noktadan sonra sizde olması gerektiği.
Amaç gösterişli bir framework değil; uzun listeleri günlerce sorunsuz işleyebilen, süreç yarıda kesildiğinde kaldığı yerden devam eden ve aynı işi asla iki kez yapmayan sıkıcı ama güvenilir bir motor.
SQLite ile idempotent ve kaldığı yerden devam eden iş kuyruğu
İlk denemede şuna takıldım: pipeline saatlerce çalışıp bir ağ hatasıyla düştüğünde her şeye baştan başlıyordum. Kuyruğu bir Python listesinde tutuyordum, süreç ölünce liste de ölüyordu; üstelik hangi kayıtların bittiğini bilmediğim için yeniden başlatmak, aynı sitelere gereksiz istekler atmak demekti. Ders basitti — işin durumu bellekte değil diskte yaşamalı. Bunun için Redis ya da RabbitMQ kurmaya gerek yok; tek makinede çalışan bir zenginleştirme işi için sqlite3 fazlasıyla yeterli, üstelik Python ile birlikte geliyor.
Tasarımın üç kuralı var:
- Idempotentlik: aynı şirket kaydını iki kez eklemek ya da işlemek sonucu değiştirmemeli. Bunu birincil anahtar ve INSERT OR IGNORE sağlıyor.
- Devam edebilirlik: süreç ölse bile durum tablosu diskte duruyor; yeniden başlatınca yalnızca durumu hâlâ bekliyor olan kayıtlar işleniyor.
- Sınırlı yeniden deneme: sürekli hata veren bir kayıt kuyruğu sonsuza dek tıkamasın diye deneme sayacı tutuyorum.
import sqlite3
from dataclasses import dataclass
DB_YOLU = "zenginlestirme.db"
SEMA = """
CREATE TABLE IF NOT EXISTS isler (
sirket_id TEXT PRIMARY KEY,
site TEXT,
durum TEXT NOT NULL DEFAULT 'bekliyor',
deneme INTEGER NOT NULL DEFAULT 0,
sonuc TEXT,
guncelleme TEXT DEFAULT (datetime('now'))
);
"""
@dataclass
class Is:
sirket_id: str
site: str
def kuyruk_ac():
con = sqlite3.connect(DB_YOLU)
con.execute(SEMA)
return con
def isleri_ekle(con, isler):
con.executemany(
"INSERT OR IGNORE INTO isler (sirket_id, site) VALUES (?, ?)",
[(i.sirket_id, i.site) for i in isler],
)
con.commit()
def siradaki(con, limit=50):
satirlar = con.execute(
"SELECT sirket_id, site FROM isler "
"WHERE durum = 'bekliyor' AND deneme < 3 LIMIT ?",
(limit,),
).fetchall()
return [Is(*s) for s in satirlar]
def tamamla(con, sirket_id, sonuc_json):
con.execute(
"UPDATE isler SET durum='bitti', sonuc=?, "
"guncelleme=datetime('now') WHERE sirket_id=?",
(sonuc_json, sirket_id),
)
con.commit()
Kuyruğu dolduran taraf CSV'yi bir kez okuyup isleri_ekle fonksiyonunu çağırıyor; işleyen taraf ise siradaki ile küçük partiler çekip her kaydı bitirdikçe tamamla diyor. Süreç herhangi bir anda kesilse bile veri kaybı olmuyor, çünkü durum her adımda diske işleniyor. Bu yaklaşımın bana en büyük getirisi aslında psikolojik: uzun sürecek bir işi gönül rahatlığıyla gece çalışmaya bırakabiliyorum.
Web sitesinden iletişim ve sosyal medya çıkarımı
Zenginleştirmenin en verimli kaynağı çoğu zaman şirketin kendi web sitesi. Ana sayfa ve iletişim sayfası; e-posta adreslerinin, telefonların ve sosyal medya profillerinin büyük kısmını zaten barındırıyor. Burada iki araç yetiyor: HTML'i ayrıştırmak için BeautifulSoup, metinden e-posta ve telefon yakalamak için regex desenleri. Tek uyarım şu: mükemmel e-posta regex'i diye bir şey yok. Pratikte sade bir desen kurup yanlış pozitifleri sonradan filtrelemek, her uç durumu kapsamaya çalışan dev bir desenden çok daha sağlıklı.
import re
import time
import urllib.robotparser
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup
EPOSTA = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
SOSYAL = ("linkedin.com", "instagram.com", "facebook.com", "x.com")
UA = "ZenginlestirmeBot/1.0 (iletisim: site sahibi)"
def robots_izinli(url):
kok = "{0.scheme}://{0.netloc}".format(urlparse(url))
rp = urllib.robotparser.RobotFileParser(urljoin(kok, "/robots.txt"))
try:
rp.read()
except OSError:
return True
return rp.can_fetch(UA, url)
def sayfayi_zenginlestir(url, bekleme=2.0):
if not robots_izinli(url):
return {"durum": "robots_engelli"}
time.sleep(bekleme) # nazik hiz siniri
yanit = requests.get(url, timeout=10, headers={"User-Agent": UA})
yanit.raise_for_status()
corba = BeautifulSoup(yanit.text, "html.parser")
epostalar = set(EPOSTA.findall(corba.get_text(" ")))
sosyal = {a["href"] for a in corba.find_all("a", href=True)
if any(s in a["href"] for s in SOSYAL)}
return {"epostalar": sorted(epostalar), "sosyal": sorted(sosyal)}
Buradaki üç ayrıntı, kodun kendisinden daha önemli:
- İstek atmadan önce robots.txt kontrolü yapıyorum; izin yoksa kaydı robots_engelli olarak işaretleyip geçiyorum.
- Kendimi gizlemiyorum: User-Agent alanında botun kim olduğu açıkça yazıyor.
- Her istekten önce sabit bir bekleme var; aynı alan adına art arda hızlı istek atmamak, işin temel nezaket kuralı.
Telefon ve adres: işin dağınık kısmı
E-posta ve sosyal medya bağlantıları nispeten temiz çıkar; telefon numaraları ise ülkeden ülkeye format değiştirdiği için en dağınık alan. Kendi pipeline'ımda telefonu tek bir regex ile çözmeye çalışmak yerine ülke bilgisini işin içine katıyorum: kayıttaki ülke kodunu bildiğim için o ülkenin numara uzunluğuna ve önekine uymayan adayları baştan eliyorum. Adres için de benzer bir mantık geçerli — sayfadan çıkan ham metni olduğu gibi kaydetmek yerine, en azından ülke ve şehir alanlarıyla tutarlılığını kontrol edip öyle yazıyorum. Bu küçük doğrulamalar, sonraki birleştirme adımının yükünü ciddi biçimde hafifletiyor.
Çok kaynaklı birleştirme ve veri kalitesi skoru
Tek kaynak hiçbir zaman yeterli olmuyor. Web sitesi, ticaret sicili benzeri açık kayıtlar ve halihazırda elimde olan CRM verisi, çoğu zaman aynı şirketi farklı yazımlarla içeriyor. Kaynaklar arasında kayıtları birleştirirken isimler birebir eşleşmediği için fuzzy matching ile şirket eşleştirme yaklaşımını kullanıyorum; Acme GmbH ile ACME Gesellschaft mbH aynı şirket, ama bunu ancak normalizasyon ve benzerlik skoruyla yakalayabiliyorsunuz.
Birleştirmeden önce bir de alan adı doğrulama adımı çalıştırıyorum. Kaynaklardan gelen web sitesi bilgisi şaşırtıcı sıklıkta hatalı: yazım yanlışı, çoktan kapanmış site ya da başka bir şirkete devredilmiş alan adı. DNS çözümlemesi yapıp siteye gerçekten ulaşılabildiğini teyit etmeden o alan adını kayda temel yapmıyorum; çünkü yanlış bir alan adı, üzerine kurulan e-posta ve sosyal medya çıkarımlarını da zincirleme kirletiyor.
Birleştirme sonrasında her alan için basit bir öncelik sırası uyguluyorum: şirketin kendi sitesinden gelen bilgi, üçüncü taraf kaynaktan gelene baskın. İki kaynak çelişiyorsa iki değeri de saklayıp kaydı incelemeye işaretliyorum; sessizce veri ezmek, deneyimimde zenginleştirme projelerinin en pahalı hatası.
Veri kalitesi skoru ise çıktının sigortası. Her kayda basit ve açıklanabilir bir puan veriyorum:
- Alan adı çözümleniyor ve site gerçekten yanıt veriyor mu?
- Bulunan e-posta adresinin alan adı, web sitesinin alan adıyla tutarlı mı?
- Aynı bilgiyi kaç bağımsız kaynak doğruluyor?
- Adres bilgisi, kaydın ülke alanıyla çelişiyor mu?
def kalite_skoru(kayit):
puan = 0
if kayit.get("site_yanit_verdi"):
puan += 40
if kayit.get("epostalar"):
puan += 20
site = (kayit.get("site") or "").replace("www.", "")
if site and any(e.endswith(site) for e in kayit["epostalar"]):
puan += 15 # e-posta alan adi siteyle tutarli
puan += min(kayit.get("kaynak_sayisi", 0), 5) * 5
return min(puan, 100)
Belirlediğim eşiğin altında kalan kayıtlar otomatik akıştan çıkıp elle inceleme kuyruğuna düşüyor. Deneyimimde bu tek adım, zenginleştirilmiş verinin güvenilirliğini her şeyden çok artırıyor; çünkü yanlış veri, eksik veriden çok daha tehlikeli. Eksik alanı görürsünüz, yanlış alanı fark etmeden CRM'inize taşırsınız.
Hız sınırı, robots.txt ve hukuki çerçeve
Scraping teknik olduğu kadar hukuki bir konu ve bu sınırları sonradan eklemek yerine baştan koda gömüyorum:
- robots.txt bağlayıcıdır: teknik olarak aşılabilir olması bir şeyi değiştirmez; izin verilmeyen yolu taramıyorum.
- Hizmet şartları: bir platform kendi şartlarında otomatik erişimi yasaklıyorsa — çoğu sosyal ağ yasaklar — o platformu kazımak yerine varsa resmi API'sini kullanıyorum.
- KVKK ve kişisel veri ayrımı: info@ gibi kurumsal adresler ile bir kişinin ad-soyadını içeren adresler aynı şey değil. Kişisel veri topluyorsanız işleme amacını, saklama süresini ve hukuki dayanağı netleştirmeden o alanı doldurmayın.
- Hız sınırı: alan adı başına düşük frekans, yoğun olmayan saatlerde çalıştırma ve toplam istek bütçesi. Karşı tarafın sunucusunu yormak hem etik dışı hem de IP engeliyle projenizi bitirir.
Bu kurallar işi yavaşlatıyor mu? Evet. Ama sürdürülebilir bir zenginleştirme motoru ancak böyle kuruluyor; kısa yoldan gidenin listesi eninde sonunda engellere ve hukuki risklere takılıyor.
Sonuç
Ham şirket listesini gerçek bir varlığa dönüştüren şey ne tek bir kütüphane ne de akıllıca yazılmış bir regex; disiplinli bir mimari: diskte yaşayan idempotent bir kuyruk, kaynaklara saygılı bir toplayıcı, çok kaynaklı birleştirme ve acımasız bir kalite skoru. Bu dört parça yerine oturduğunda pipeline sıkıcılaşıyor — ve veri işlerinde sıkıcı, iyinin ta kendisidir.
Benzer bir yapıyı kendi listeleriniz için kurmayı düşünüyorsanız veri otomasyonu çalışmalarıma göz atabilir, projenizi konuşmak için iletişim bölümünden bana ulaşabilirsiniz.