Statik ve Dinamik Sayfalar: Headless Tarayıcı Gerekir mi?

Yayın tarihi:

12 dk okuma

Acar Diveroli
Yazar: Acar Diveroli
Kod satırlı statik bir pencere ile iskelet yüklemeli dinamik bir pencere arasında duran bot düğümü çizimi

Scraping'e yeni başlayan hemen herkes aynı anı yaşar: tarayıcıda ürünler, fiyatlar ve yorumlar gözünüzün önündedir; aynı adresi Python'da requests ile çektiğinizde ise seçiciler hiçbir şey bulamaz. Sayfanın HTML'ine baktığınızda ürün listesi yerine bir "Yükleniyor..." yazısı ve birkaç <script> etiketi görürsünüz. Sorun kodunuzda değildir; sayfa dinamiktir ve içeriği tarayıcıda çalışan JavaScript sonradan getirir.

Bu yazıda statik ve dinamik sayfaların farkını, bir sayfanın dinamik olup olmadığını birkaç dakikada nasıl anlayacağınızı ve headless tarayıcının ne olduğunu anlatıyoruz. Asıl üzerinde durduğumuz konu ise çoğu durumda headless tarayıcıya hiç gerek kalmadan veriye ulaşmanın yolları: sayfanın arka planda çağırdığı API isteğini bulmak ve HTML'e gömülü JSON'u okumak. Tarayıcı gerçekten gerektiğinde Playwright ile doğru bekleme stratejilerini ve maliyeti düşürme yollarını da gösteriyoruz. Örnekleri, içeriği JavaScript ile yükleyen yerel bir test sayfasında çalıştırdık.

Statik sayfa nedir?

Statik sayfada, tarayıcının gösterdiği içeriğin tamamı sunucunun ilk yanıtındaki HTML'de bulunur. Sunucu bu HTML'i önceden hazırlanmış bir dosyadan okuyabilir ya da her istekte bir veritabanından üretebilir; scraping açısından önemli olan, içeriğin tarayıcıya ulaştığı anda HTML'in içinde olmasıdır.

Statik bir sayfadan veri almak için tarayıcıya gerek yoktur:

python
import requests
from bs4 import BeautifulSoup

html = requests.get("https://ornek.com/kampanyalar", timeout=20).text
kartlar = BeautifulSoup(html, "html.parser").select("div.urun")
print("Bulunan ürün kartı:", len(kartlar))

Haber siteleri, bloglar, birçok kurumsal site ve sunucu tarafında çizilen (server-side rendering) e-ticaret sayfaları bu gruptadır.

Dinamik (JavaScript ile yüklenen) sayfa nedir?

Dinamik sayfada sunucunun ilk yanıtı bir iskelettir: sayfa düzeni, boş bir liste kapsayıcısı ve JavaScript dosyaları. İçerik şu adımlarla sonradan gelir:

  1. Tarayıcı HTML iskeletini alır ve ekrana "Yükleniyor..." gibi bir yer tutucu çizer.
  2. JavaScript dosyaları indirilir ve çalıştırılır. Çoğu zaman React, Vue veya benzeri bir çerçeve.
  3. JavaScript bir API isteği gönderir. Örneğin /api/urunler?kategori=kulaklik&sayfa=1 adresine fetch ile.
  4. Sunucu veriyi JSON olarak döner. Ürün adları, fiyatlar, stok bilgisi.
  5. JavaScript bu JSON'dan HTML üretip sayfaya yerleştirir. Ürün kartları ancak bu adımda görünür.
  6. Kaydırma veya tıklamayla yeni istekler gelir. Sonsuz kaydırma, "daha fazla göster" butonu, filtreler.

Python'daki requests gibi bir HTTP istemcisi yalnızca birinci adımı yapar; JavaScript çalıştırmaz. Bu yüzden aldığı HTML'de ürün kartları yoktur. Yerel test sayfamızda yukarıdaki requests kodu sıfır kart buldu; aynı sayfa tarayıcıda iki ürünü gösteriyordu.

Arada bir de karma yapı vardır: Next.js ve Nuxt gibi çerçeveler sayfanın ilk hâlini sunucuda üretir ve aynı veriyi JavaScript'in kullanması için HTML'in içine bir JSON bloğu olarak da gömer. Bu tür sayfalarda veri, JavaScript çalışmadan HTML'de bulunur ama bazen görünen kartlarda değil, bir <script> etiketinin içindedir.

Bir sayfanın dinamik olduğu nasıl anlaşılır?

Headless tarayıcı kurmadan önce birkaç dakikalık bir teşhis, doğru yolu seçmenizi sağlar:

  1. Kaynağı görüntüleyin. Sayfada Ctrl + U (macOS'ta Cmd + Option + U) sunucunun gönderdiği ham HTML'i gösterir. Sayfada gördüğünüz bir ürün adını veya fiyatı burada arayın. Bulursanız sayfa büyük olasılıkla statiktir.
  2. Elements paneliyle karşılaştırın. Geliştirici araçlarındaki Elements paneli JavaScript çalıştıktan sonraki sayfayı gösterir. Metin Elements'te var, kaynakta yoksa içerik JavaScript ile gelmiştir.
  3. JavaScript'i kapatıp yeniden yükleyin. Chrome geliştirici araçlarında Ctrl + Shift + P ile komut menüsünü açıp "Disable JavaScript" yazın ve sayfayı yenileyin. İçerik kayboluyorsa sayfa dinamiktir.
  4. Kodla kontrol edin. requests ile aldığınız HTML'de beklediğiniz bir metni arayın. Yoksa ve HTML çok kısaysa iskelet almışsınızdır.
  5. Network panelinde Fetch/XHR filtresine bakın. Sayfayı yenileyip bu filtrede JSON yanıtları görüyorsanız, veri büyük olasılıkla bu isteklerden birindedir.

Dördüncü adımın bir tuzağı vardır: HTML'de içerik yoksa bunun nedeni her zaman JavaScript değildir. Site size bir doğrulama sayfası veya farklı bir içerik döndürmüş olabilir. Yanıtın durum kodunu ve içeriğini kontrol edin; bu teşhisi Web Scraping'de Engellenmeden Veri Toplama Yöntemleri yazımızda ayrıntılı anlattık.

Headless tarayıcı nedir?

Headless tarayıcı, ekranda pencere açmadan çalışan gerçek bir tarayıcıdır. Chromium, Firefox veya WebKit motoru sayfayı normal bir tarayıcı gibi yükler, JavaScript'i çalıştırır, API isteklerini gönderir ve sayfayı oluşturur; siz de bu sayfayı kodla okursunuz. Yaygın araçlar Playwright, Puppeteer ve Selenium'dur.

Headless tarayıcının yetenekleri bir HTTP istemcisinden fazladır, ama maliyeti de öyledir:

  • İşlemci ve bellek. Her tarayıcı sekmesi, bir HTTP isteğinin kat kat fazlası kaynak kullanır. Aynı makinede çalıştırabileceğiniz eşzamanlı sayfa sayısı ağla değil, çekirdek ve bellekle sınırlanır.
  • Süre. Sayfanın bütün JavaScript'inin indirilip çalışması saniyeler alabilir.
  • İstek sayısı ve bant genişliği. Tek bir sayfa görseller, yazı tipleri, stil dosyaları, analiz betikleri ve reklamlarla onlarca ek istek üretir. Bu hem sizin trafiğinizi hem hedef sitenin yükünü artırır.
  • Bakım. Tarayıcı sürümleri, sürücüler ve bekleme mantığı ek karmaşıklık getirir.

Bu yüzden headless tarayıcı ilk tercih değil, son çare olmalıdır. Farklı araçların karşılaştırması için Web Kazıma: JavaScript mi Python mu? yazımıza, Selenium kurulumu için Selenium ile Proxy Kullanımı yazımıza bakabilirsiniz.

Önce API/XHR isteğini bulmak

Dinamik bir sayfadaki veri, tarayıcıya bir yerden JSON olarak gelir. O isteği bulursanız sayfayı hiç çizmeden veriyi doğrudan alabilirsiniz. Chrome'un Network paneli başvuru belgesi filtreleri ve kopyalama seçeneklerini ayrıntılı anlatır; kısa yol şöyledir:

  1. Geliştirici araçlarını açın (F12) ve Network sekmesine geçin.
  2. Filtre çubuğunda Fetch/XHR seçeneğini işaretleyin.
  3. Sayfayı yenileyin veya veriyi yükleyen eylemi yapın (sayfayı kaydırın, filtre seçin, sonraki sayfaya geçin).
  4. Listede adı api, graphql, search, products gibi ifadeler içeren isteklere tıklayın ve Response sekmesinde JSON'a bakın.
  5. Aradığınız veriyi içeren isteği bulduğunuzda Headers sekmesinden adresi, parametreleri ve gerekli başlıkları not edin. İsteğe sağ tıklayıp Copy > Copy as cURL seçeneğiyle komut satırında deneyebilirsiniz.

Bulduğunuz isteği Python'da tekrarlamak genellikle birkaç satırdır:

python
import requests

session = requests.Session()
session.headers.update({
    "User-Agent": "OrnekFiyatBotu/1.0 (+https://ornek.com/bot-hakkinda)",
    "Accept": "application/json",
})

yanit = session.get(
    "https://ornek.com/api/urunler",
    params={"kategori": "kulaklik", "sayfa": 1},
    timeout=20,
)
yanit.raise_for_status()
for urun in yanit.json()["urunler"]:
    print(urun["ad"], urun["fiyat"])

Bu yolun avantajları büyüktür: veri yapılandırılmış gelir, HTML seçicilerine gerek kalmaz, sayfa tasarımı değiştiğinde kodunuz bozulmaz ve tek bir istek bütün bir sayfanın yükünün küçük bir kısmıdır. Sayfalama çoğu zaman sayfa, offset veya cursor parametresiyle yapılır.

Dikkat edilmesi gerekenler:

  • Kimlik doğrulama ve belirteçler. Bazı API istekleri çerez, oturum belirteci veya kısa ömürlü imzalar ister. Bunları elle kopyalamak kısa süre çalışır, sonra bozulur.
  • Site şartları. Sitenin kendi ön yüzü için kullandığı iç API, herkese açık bir API değildir. Sitenin kullanım şartlarını ve robots.txt kurallarını bu isteklere de uygulayın; resmi bir API sunuluyorsa onu tercih edin. Hukuki çerçeve için Web Scraping Yasal mı? yazımıza bakabilirsiniz.
  • Hız. API istekleri hafif olduğu için çok hızlı gönderilebilir; bu, sitenin sınırlarını daha kolay aşmanız anlamına da gelir. Aynı hız kurallarını uygulayın.

HTML'e gömülü JSON'u okumak

Next.js, Nuxt ve benzeri çerçevelerle yapılmış sitelerde veri çoğu zaman HTML'in içinde bir <script> etiketinde hazır bekler. Kaynağı görüntüleyip __NEXT_DATA__, __NUXT__, __INITIAL_STATE__ veya application/ld+json ifadelerini arayın.

python
import json

import requests
from bs4 import BeautifulSoup

html = requests.get("https://ornek.com/kampanyalar", timeout=20).text
soup = BeautifulSoup(html, "html.parser")

veri = json.loads(soup.select_one("script#__NEXT_DATA__").string)
for urun in veri["props"]["pageProps"]["urunler"]:
    print(urun["ad"], urun["fiyat"])

application/ld+json blokları ise schema.org biçiminde ürün adı, fiyat, stok ve puan gibi yapılandırılmış veriyi taşır ve arama motorları için hazırlandıkları için genellikle kararlıdır. Seçicilerin tasarım değişikliğine dayanıklılığı konusunu CSS Selector ve XPath yazımızda ele aldık.

Playwright ile bekleme stratejileri

Veri ne API'de ne gömülü JSON'da bulunamıyorsa, ya da içeriğe ulaşmak için tıklama, kaydırma ve form doldurma gibi etkileşimler gerekiyorsa headless tarayıcı kullanılır. Bu durumda en sık hata ne zaman okunacağıdır: sayfa açıldığı anda okursanız içerik henüz gelmemiştir; sabit bir süre beklerseniz ya gereksiz zaman kaybedersiniz ya da yavaş bir yanıtta yine boş sonuç alırsınız.

Playwright'ın doğru bekleme araçları şunlardır:

  • Locator'ların otomatik beklemesi. page.locator("div.urun h2").inner_text() gibi işlemler, öğe sayfada belirene kadar varsayılan zaman aşımına kadar bekler. Playwright'ın eylem öncesi kontroller belgesi hangi durumların beklendiğini listeler.
  • Belirli bir öğeyi beklemek. page.locator("div.urun").first.wait_for() ilk ürün kartı görünene kadar bekler.
  • Belirli bir yanıtı beklemek. page.expect_response(...) sayfanın veri isteğinin dönmesini bekler ve JSON'a doğrudan erişmenizi sağlar.
  • Sabit bekleme kullanmamak. page.wait_for_timeout(5000) testlerde işe yarar, üretimde güvenilmezdir.

Playwright belgeleri networkidle yükleme durumunun, yani ağ trafiğinin bir süre durmasını beklemenin, önerilmediğini belirtir; analiz ve reklam betikleri sürekli istek gönderen sayfalarda bu durum hiç gelmeyebilir.

Aşağıdaki örnek, bir proxy üzerinden sayfayı açar, görsel ve yazı tiplerini engelleyerek yükü azaltır, veri isteğinin yanıtını yakalar ve ardından çizilmiş kartları okur:

python
from playwright.sync_api import sync_playwright

PROXY = {"server": "http://pr.proxynet.io:8000", "username": "kullanici", "password": "parola"}

with sync_playwright() as p:
    tarayici = p.chromium.launch(proxy=PROXY)
    sayfa = tarayici.new_page()

    # Görsel ve yazı tiplerini indirmeyerek süreyi ve trafiği azalt
    sayfa.route("**/*.{png,jpg,jpeg,webp,gif,woff2}", lambda route: route.abort())

    with sayfa.expect_response(lambda r: "/api/urunler" in r.url and r.ok) as yanit:
        sayfa.goto("https://ornek.com/kampanyalar")
    api_verisi = yanit.value.json()           # JSON'u doğrudan al

    sayfa.locator("div.urun").first.wait_for()  # ya da çizilmiş kartları bekle
    adlar = sayfa.locator("div.urun h2").all_inner_texts()

    print(len(api_verisi["urunler"]), adlar)
    tarayici.close()

Bu örnekte expect_response ile yakalanan JSON çoğu zaman tek başına yeterlidir; kartları okumaya gerek kalmaz. Tarayıcıyı yalnızca sayfanın o isteği doğru parametre ve çerezlerle göndermesini sağlamak için kullanmış olursunuz.

Proxy bilgisi Playwright'ta tarayıcı başlatılırken verilir; kullanıcı adı ve şifre ayrı alanlardadır. Oturum boyunca aynı IP'nin korunması gereken akışlarda sabit bir çıkış adresi, çok sayıda bağımsız sayfada ise her bağlantıda farklı IP veren Rotating Proxy kullanılabilir. Tarayıcı otomasyonunda doğrulama ekranlarının neden çıktığını Puppeteer ve CAPTCHA yazımızda anlattık.

Maliyet: headless tarayıcı ne zaman gereksiz?

YaklaşımNe zaman işe yarar?Hız ve kaynakKırılganlıkDikkat
HTTP istemcisi + HTMLSayfa statik, içerik kaynakta varÇok hızlı, çok hafifTasarım değişikliğine duyarlıSeçicileri kararlı özniteliklere bağlayın
HTML'e gömülü JSONNext.js, Nuxt gibi karma sayfalarÇok hızlı, hafifVeri yapısı değişebilirJSON yolunu kontrol eden hata yönetimi
Arka plandaki API isteğiİçerik JSON ile geliyorÇok hızlı, en hafifTasarımdan bağımsız, API değişebilirSite şartları, belirteçler, hız sınırı
Headless tarayıcıEtkileşim gerekiyor, veri başka yoldan alınamıyorYavaş, ağırBekleme mantığına duyarlıGörselleri engelleyin, eşzamanlılığı düşük tutun

Headless tarayıcının gereksiz olduğu tipik durumlar:

  • İçerik sayfa kaynağında zaten var.
  • Veri bir JSON isteğinden geliyor ve bu istek basit parametrelerle tekrarlanabiliyor.
  • Veri __NEXT_DATA__ veya JSON-LD bloğunda duruyor.
  • Yalnızca sayfalar arası gezinme için JavaScript kullanılıyor, içerik her sayfada sunucudan geliyor.

Headless tarayıcının gerçekten gerektiği durumlar:

  • İçerik ancak kaydırma, tıklama veya form etkileşimiyle yükleniyor ve ilgili API isteği tekrarlanamıyor.
  • İstekler, sayfadaki JavaScript'in ürettiği kısa ömürlü imzalar veya belirteçler gerektiriyor.
  • Sayfanın görsel çıktısı (ekran görüntüsü, düzen doğrulaması) işin kendisi.

Headless tarayıcı kullanıyorsanız, eşzamanlı sayfa sayısını makine kaynaklarına göre ayarlayın; bu konuyu Concurrency ve Parallelism yazımızda ele aldık.

Kullanım senaryoları

  • E-ticaret fiyat takibi: Kategori sayfası dinamik, ama ürün listesi tek bir /api/urunler isteğinden geliyor. Headless tarayıcı yerine API isteği; farklı ülkelerdeki fiyatları görmek için gerçek ev bağlantılarından çıkan Residential Proxy. Kurgusu e-ticaret proxy çözümü sayfamızda.
  • Next.js ile yapılmış ilan sitesi: Veri __NEXT_DATA__ içinde. HTTP istemcisi ve JSON ayrıştırma yeterli.
  • Sonsuz kaydırmalı yorum listesi: Kaydırma sırasında gönderilen sayfalama isteği bulunur ve cursor parametresiyle döngüye alınır.
  • Etkileşim gerektiren hesap paneli (kendi hesabınız): Giriş, filtre seçimi ve rapor indirme adımları headless tarayıcıyla; oturum boyunca aynı IP. Genel kurgu veri kazıma çözümü sayfamızda.

Sık yapılan hatalar

  • Önce headless tarayıcıya geçmek. Network panelindeki JSON isteği çoğu zaman aynı veriyi çok daha ucuza verir.
  • Sabit süre beklemek. time.sleep(5) hızlı yanıtta zaman kaybettirir, yavaş yanıtta yine boş sonuç verir.
  • networkidle beklemek. Sürekli istek gönderen sayfalarda hiç gelmeyebilir.
  • Bütün kaynakları indirmek. Görseller, yazı tipleri ve videolar veri için gerekmez; engellemek süreyi ve trafiği azaltır.
  • Boş sonucu dinamik sayfa sanmak. Doğrulama sayfası veya engelleme de boş sonuç verir; durum kodunu ve içeriği kontrol edin.
  • Gizli API'yi herkese açık API gibi kullanmak. Site şartları ve hız sınırları bu isteklere de uygulanır.
  • Tespit atlatma araçlarına yönelmek. Tarayıcıyı "insan gibi göstermeye" çalışan eklentiler sorunun kaynağını gizler; hız, kapsam ve izin tarafını gözden geçirmek daha sağlam bir yoldur. Bu tür araçların neden sorunlu olduğunu Undetected ChromeDriver ile Scraping yazımızda tartıştık.

Karar rehberi

Teşhis sonucuÖneri
Metin sayfa kaynağında varHTTP istemcisi + HTML ayrıştırma
Kaynakta __NEXT_DATA__ veya JSON-LD varHTTP istemcisi + JSON ayrıştırma
Network panelinde veriyi taşıyan JSON isteği varİsteği doğrudan tekrarlayın
JSON isteği kısa ömürlü imza istiyorHeadless tarayıcı + expect_response
İçerik yalnızca etkileşimle geliyorHeadless tarayıcı + locator beklemesi
Headless tarayıcı yavaş ve ağırGörselleri engelleyin, eşzamanlılığı düşürün
HTML'de içerik yok ve durum kodu 403/429Dinamik değil, engelleme; teşhis edin

Sık sorulan sorular

requests neden tarayıcıda gördüğüm içeriği döndürmüyor?

Çünkü requests yalnızca sunucunun ilk gönderdiği HTML'i alır ve JavaScript çalıştırmaz. Sayfa içeriği JavaScript ile sonradan yükleniyorsa bu HTML'de yalnızca iskelet vardır. Network panelinde verinin geldiği isteği bulun ya da HTML'e gömülü JSON'u arayın.

Headless tarayıcı ile normal tarayıcı arasındaki fark nedir?

Motor aynıdır; fark, headless modda ekrana pencere çizilmemesidir. Sayfa yükleme, JavaScript çalıştırma ve ağ istekleri normal tarayıcıdaki gibi yapılır. Bazı siteler headless tarayıcıyı küçük farklılıklardan ayırt edebilir.

Playwright mı Selenium mu?

İkisi de dinamik sayfaları çalıştırır. Playwright otomatik bekleme, ağ yanıtı yakalama ve istek engelleme gibi scraping'de işe yarayan özellikleri tek bir API'de sunar; Selenium ise daha eski, çok dilli ve geniş bir ekosisteme sahiptir. Mevcut projenizde hangisi varsa onunla devam etmek genellikle en pratik yoldur.

Gizli API isteğini kullanmak yasal mı?

İsteğin teknik olarak herkese açık olması, kullanımının serbest olduğu anlamına gelmez. Sitenin kullanım şartlarını, robots.txt kurallarını ve kişisel veri mevzuatını değerlendirin; resmi bir API varsa onu tercih edin. Emin değilseniz hukuki destek alın.

Headless tarayıcıyı proxy ile nasıl kullanırım?

Playwright'ta proxy, tarayıcı başlatılırken proxy parametresiyle verilir; sunucu adresi, kullanıcı adı ve şifre ayrı alanlardadır. Puppeteer ve Selenium'da ise tarayıcıya başlatma parametresi olarak verilir ve kimlik doğrulama ayrıca ele alınır.

Sonsuz kaydırmalı sayfalardan veri nasıl alınır?

Önce Network panelinde, kaydırdıkça gönderilen sayfalama isteğini bulun. Bu istek genellikle bir sayfa numarası veya cursor değeri taşır ve döngüyle tekrarlanabilir. İstek tekrarlanamıyorsa headless tarayıcıda sayfayı kademeli kaydırıp her adımda yeni kartların gelmesini bekleyin.

Özetle

Statik sayfada içerik HTML'de hazırdır ve basit bir HTTP isteği yeterlidir; dinamik sayfada içerik JavaScript ile sonradan gelir ve HTTP isteği iskelet döndürür. Headless tarayıcıya geçmeden önce kaynağı görüntüleyin, Network panelinde veriyi taşıyan JSON isteğini arayın ve HTML'e gömülü veriyi kontrol edin. Tarayıcı gerçekten gerektiğinde sabit beklemeler yerine locator beklemesi ve expect_response kullanın, görselleri engelleyin ve eşzamanlılığı düşük tutun. Veri toplama işleriniz için uygun proxy türlerini proxy hizmetlerimizde bulabilirsiniz.

ChatGPT'ye sorClaude'a sor