Rakip sitemap izleyici: rakip her gün ne yayınlıyor?

Rakiplerin ne yayınladığını takip etmenin en sessiz ve en ucuz yolu sitemap'leridir. Yeni bir kategori açtıklarında, bir içerik serisine başladıklarında ya da toplu olarak sayfa sildiklerinde bu değişiklik ilk önce sitemap'e yansır. Bunun için ücretli bir izleme aracına gerek yok; günde bir kez çalışan kısa bir PHP betiği yeterli.

Aşağıdaki betik bir sitemap'i (sitemap index ve gzip dahil) okuyor, her URL'nin loc ve lastmod değerini bir JSON anlık görüntüsüne yazıyor ve bir önceki günün görüntüsüyle karşılaştırıyor. Sonuçta yeni eklenen, kaldırılan ve lastmod'u değişen URL'lerin listesi çıkıyor.

Etik sınırlar

Başlamadan net olayım: bu betik sadece herkese açık sitemap dosyalarını okur, sayfa içeriği çekmez. Sitemap'ler zaten arama motorlarının ve herkesin okuması için yayınlanır. Yine de birkaç kurala uyuyorum:

  • Günde bir kereden sık çalıştırmıyorum. Sitemap'in saatlik değişimini izlemek hiçbir SEO kararına katkı sağlamaz.
  • Alt sitemap'leri çekerken aralara bekleme koyuyorum.
  • User-Agent satırında betiği ve bir iletişim adresini açıkça belirtiyorum, tarayıcı gibi görünmeye çalışmıyorum.
  • Sitemap'e robots.txt ile erişim engellenmişse ya da site giriş gerektiriyorsa o siteyi izlemiyorum.

Rakibin sitemap adresini bulmak için önce robots.txt dosyasındaki Sitemap: satırına bakıyorum. Yoksa /sitemap.xml ve /sitemap_index.xml gibi yaygın adresleri deniyorum.

Betik

Betik PHP 8 ile çalışıyor ve cURL, SimpleXML, zlib ile mbstring eklentilerini kullanıyor; bunlar çoğu paylaşımlı hostingte hazır geliyor. Okunabilirlik için iki parçaya böldüm, ikisi aynı dosyanın içinde alt alta duruyor. İlk parça indirme ve sitemap okuma:

izleyici.php
<?php
declare(strict_types=1);

const UA = 'SitemapIzleyici/1.0 (+https://ornek.com/iletisim)';
const DIZIN = __DIR__ . '/anlik';

function indir(string $url): string
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_USERAGENT => UA,
        CURLOPT_ENCODING => '', // aktarım sıkıştırmasını otomatik çöz
    ]);
    $govde = curl_exec($ch);
    $kod = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($govde === false || $kod !== 200) {
        throw new RuntimeException("İndirilemedi ($kod): $url");
    }
    // .xml.gz dosyası: gzip imzası ile tanı ve aç
    if (str_starts_with($govde, "\x1f\x8b")) {
        $govde = gzdecode($govde) ?: throw new RuntimeException("gzip açılamadı: $url");
    }
    return $govde;
}

function topla(string $url, array &$sonuc, int $derinlik = 0): void
{
    $xml = simplexml_load_string(indir($url));
    if ($xml === false) {
        throw new RuntimeException("XML okunamadı: $url");
    }
    if ($xml->getName() === 'sitemapindex' && $derinlik < 2) {
        foreach ($xml->sitemap as $alt) {
            sleep(2); // alt sitemap'ler arasında nazik bekleme
            topla(trim((string) $alt->loc), $sonuc, $derinlik + 1);
        }
        return;
    }
    foreach ($xml->url as $u) {
        $sonuc[trim((string) $u->loc)] = trim((string) $u->lastmod);
    }
}

İkinci parça karşılaştırma, rapor ve bildirim. Aynı dosyada ilk parçanın hemen altına geliyor:

function telegram(string $metin): void
{
    // Ortam değişkenleri yoksa bildirim gönderme
    $token = getenv('TG_TOKEN');
    $sohbet = getenv('TG_CHAT');
    if (!$token || !$sohbet) {
        return;
    }
    $ch = curl_init("https://api.telegram.org/bot{$token}/sendMessage");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => ['chat_id' => $sohbet, 'text' => mb_substr($metin, 0, 4000)],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
    ]);
    curl_exec($ch);
}

$sitemap = $argv[1] ?? exit("Kullanım: php izleyici.php https://rakip.com/sitemap.xml\n");
$host = parse_url($sitemap, PHP_URL_HOST);
is_dir(DIZIN) || mkdir(DIZIN, 0755, true);

$bugun = [];
topla($sitemap, $bugun);
$bugunDosya = DIZIN . "/$host-" . date('Y-m-d') . '.json';
file_put_contents($bugunDosya, json_encode($bugun, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));

// Bugünden önceki en son anlık görüntüyü bul
$eskiler = array_filter(glob(DIZIN . "/$host-*.json"), fn($f) => $f !== $bugunDosya);
sort($eskiler);
if (!$eskiler) {
    exit("İlk çalıştırma, " . count($bugun) . " URL kaydedildi.\n");
}
$dun = json_decode(file_get_contents(end($eskiler)), true);

$yeni = array_keys(array_diff_key($bugun, $dun));
$silinen = array_keys(array_diff_key($dun, $bugun));
$degisen = [];
foreach (array_intersect_key($bugun, $dun) as $url => $lastmod) {
    if ($lastmod !== $dun[$url]) {
        $degisen[] = "$url ({$dun[$url]} -> $lastmod)";
    }
}

$rapor = "$host: " . count($bugun) . " URL\n"
    . "\nYENİ (" . count($yeni) . ")\n" . implode("\n", $yeni)
    . "\n\nKALDIRILAN (" . count($silinen) . ")\n" . implode("\n", $silinen)
    . "\n\nLASTMOD DEĞİŞEN (" . count($degisen) . ")\n" . implode("\n", $degisen) . "\n";
echo $rapor;
if ($yeni || $silinen) {
    telegram($rapor);
}

Nasıl çalışıyor

Sitemap index ve gzip. Kök etiket sitemapindex ise betik her alt sitemap'i sırayla açıyor. Derinlik sınırı, hatalı yapılandırılmış sitelerde sonsuz döngüye girmemek için. CURLOPT_ENCODING boş bırakıldığında cURL, sunucunun Content-Encoding: gzip ile gönderdiği yanıtları kendisi açıyor. Ama sitemap.xml.gz gibi dosyanın kendisi sıkıştırılmışsa bu yetmez; bu yüzden gövdenin ilk iki baytında gzip imzası var mı diye bakıp gzdecode() ile açıyorum.

Anlık görüntüler. Her çalıştırma anlik/ klasörüne alan adı ve tarihle adlandırılmış bir JSON dosyası yazıyor. Karşılaştırma için "dün" dosyasını aramak yerine bugünden önceki en son dosyayı alıyorum. Böylece sunucu bir gün cron'u kaçırırsa karşılaştırma iki gün öncesiyle yapılıyor, betik bozulmuyor. Dosya adındaki tarih biçimi sayesinde alfabetik sıralama tarih sırasıyla aynı oluyor.

lastmod değişikliği. Bu sinyali temkinli yorumluyorum. Bazı CMS'ler lastmod'u her gün tüm URL'ler için güncelliyor ya da hiç vermiyor. İlk birkaç günün raporuna bakınca rakibin lastmod'u anlamlı kullanıp kullanmadığı hemen anlaşılıyor. Anlamsızsa sadece yeni ve kaldırılan URL'lere odaklanıyorum.

JSON mu, SQLite mı. Birkaç bin URL'lik sitelerde JSON dosyaları gayet yeterli ve okunması kolay. Yüz binlerce URL'lik bir rakibi izleyecekseniz, aynı mantığı PDO ile SQLite'a taşımak daha verimli olur. Eski JSON dosyalarını belli aralıklarla silmeyi de unutmayın.

Cron ile günlük çalıştırma

cPanel'de Cron İşleri bölümünden ya da sunucuda crontab -e ile şöyle bir satır ekliyorum:

# Her sabah 06:15'te çalıştır, çıktıyı log dosyasına ekle
15 6 * * * TG_TOKEN=123:abc TG_CHAT=123456 /usr/bin/php /home/kullanici/izleyici/izleyici.php https://rakip.com/sitemap.xml >> /home/kullanici/izleyici/log.txt 2>&1

PHP yolunu sunucunuzda which php ile kontrol edin; paylaşımlı hostinglerde farklı sürümler farklı yollarda olabiliyor. Birden fazla rakip için her birine ayrı bir cron satırı ekleyip saatlerini birkaç dakika kaydırıyorum.

Telegram bildirimi isteğe bağlı. BotFather üzerinden bir bot oluşturup token'ı ve kendi sohbet kimliğinizi ortam değişkeni olarak verdiğinizde, sadece yeni ya da kaldırılan URL olan günlerde mesaj geliyor. Telegram mesajları 4.096 karakterle sınırlı olduğu için metni kısaltıyorum; uzun raporların tamamı zaten log dosyasında duruyor. E-posta tercih ederseniz telegram() fonksiyonunun yerine PHP'nin mail() fonksiyonunu ya da hostinginizin SMTP ayarlarını kullanan bir fonksiyon koymak yeterli.

Raporu neye kullanıyorum

Yeni URL listesi, rakibin hangi konulara yatırım yaptığını gösteriyor. Aynı kalıpta art arda açılan URL'ler genellikle bir içerik serisi ya da yeni bir kategori demek. Bunları kendi konu planımla karşılaştırıyorum; atlanmış bir konu varsa not alıyorum. Kaldırılan URL'ler ise budama, birleştirme ya da stok dışı ürünlerin temizlenmesi olabilir. Toplu kaldırmalar çoğu zaman bir site yapısı değişikliğinin habercisi.

Rakibin yaptığını körü körüne kopyalamıyorum. Bu veri bir sinyal; asıl karar yine kendi sitemin verisine ve kullanıcılarıma göre veriliyor. Kendi sitenizin sitemap'inin temiz olup olmadığını kontrol etmek isterseniz sitemap oluşturucuya da göz atabilirsiniz.

Yorumlarken dikkat ettiğim tuzaklar

Sitemap her zaman sitenin gerçek durumunu yansıtmaz. Bazı sitelerde sitemap sadece belirli içerik türlerini listeler, bazılarında aylarca güncellenmez. Bu yüzden bir URL'nin sitemap'ten çıkması, sayfanın silindiği anlamına gelmeyebilir. Emin olmak istediğimde kaldırılan listesinden birkaç URL'yi elle açıp durum koduna bakıyorum: 404 mü, 301 mi, yoksa sayfa hâlâ yayında mı.

Bir diğer tuzak, parçalı sitemap'lerin geçici hataları. Alt sitemap'lerden biri o gün zaman aşımına uğrarsa, betik hata verip duruyor; bu bilinçli bir tercih. Hatayı yutup devam etseydim, o parçadaki binlerce URL raporda "kaldırılan" olarak görünürdü. Log dosyasında hata gördüğüm günlerde karşılaştırmayı bir sonraki çalıştırmaya bırakıyorum.

Son olarak, bazı siteler sitemap'teki URL'lerin sırasını ya da parametrelerini sık değiştirir. URL'ler sonunda farklı bir izleme parametresi taşıyorsa, karşılaştırmadan önce parametreleri temizleyen küçük bir normalleştirme adımı eklemek gerekebilir.

Özetle

  • Rakibin sitemap adresini robots.txt üzerinden bulun.
  • Betiği bir kez elle çalıştırıp ilk anlık görüntüyü oluşturun.
  • Günde bir kez çalışacak cron satırını ekleyin, bekleme sürelerini kısaltmayın.
  • İlk hafta lastmod sinyalinin anlamlı olup olmadığını kontrol edin.
  • Yeni ve kaldırılan URL'leri haftalık olarak kendi içerik planınızla karşılaştırın.

Comments / questions

If you have a question about any topic or wish to contact me: Telegram or send an email to [email protected] Thanks!

Related pages