Geonode logo
Geonode Team

Geonode Team

Güncellenme: 16 Ekim 2026

Yayınlanma: 16 Ekim 2026

Artımlı Web Taraması: Her Şeyi Yeniden Taramadan Değişen Sayfaları Güncelleme

Artımlı web taraması, sitenin tamamını değil, yalnızca değişen sayfaları yeniden alma uygulamasıdır. Bu kılavuzun sonunda, bir site haritasından veya Geonode'un `/v1/map` uç noktasından URL'leri bulan, bunları normalleştiren, URL başına durumu SQLite'ta tutan, ETag ve içerik hash'lerini kullanarak neyin değiştiğine karar veren, yeniden ziyaret aralıklarını uyarlayan, silme işlemlerini algılayan ve değişen kümeyi `/v1/extract` veya `/v1/batch` aracılığıyla çeken bir Python modülüne sahip olacaksınız. Geonode, sayfaları alır; durum ve karşılaştırma ise komut dosyanızın sorumluluğundadır.

Artımlı web taraması, sitenin tamamını değil, yalnızca değişen sayfaları yeniden alma uygulamasıdır. Bu kılavuzun sonunda, bir site haritasından veya Geonode'un /v1/map uç noktasından URL'leri bulan, bunları normalleştiren, URL başına durumu SQLite'ta tutan, ETag ve içerik hash'lerini kullanarak neyin değiştiğine karar veren, yeniden ziyaret aralıklarını uyarlayan, silme işlemlerini algılayan ve değişen kümeyi /v1/extract veya /v1/batch aracılığıyla çeken bir Python modülüne sahip olacaksınız. Geonode, sayfaları alır; durum ve karşılaştırma ise komut dosyanızın sorumluluğundadır.

Neden tam yeniden taramalar bütçeyi boşa harcar?

Haftada %2 oranında değişiklik olan 50.000 sayfalık bir sitenin güncel kalabilmesi için haftada yaklaşık 1.000 veri alımına ihtiyaç vardır. Haftalık tam yeniden tarama ise 50.000 istek harcar. Bu, API istekleri, bant genişliği ve iş akışınızın I/O'da bloke kalma süresi açısından 50 katlık bir fark demektir.

Bu israf, her bir istek işleminin daha yavaş ve daha maliyetli olduğu JavaScript ile işlenen sayfalarda ve agresif bir şekilde istek sınırlaması uygulayan sitelerde daha da artar. Artımlı web taraması bu sorunu tersine çevirir: Her URL için az miktarda durum bilgisi tutarsınız ve bu durum bilgisinin bir sonraki isteği nereye yönlendireceğinizi belirlemesine izin verirsiniz.

Aşağıdaki modül yaklaşık 120 satırdan oluşur. Her bölüm bir parça ekler. Bağımlılığı pip install requests komutuyla yükleyin (tercih ederseniz httpx de aynı şekilde çalışır).

URL'leri Keşfetme

Tercih sırasına göre iki kaynak. Bir site haritası size URL'lerin yanı sıra isteğe bağlı bir lastmod değeri de sağlar; bu, mevcut en ucuz değişiklik sinyalidir. Bir sitenin kullanılabilir bir site haritası olmadığında, POST /v1/map , site haritası ayrıştırma ile HTML bağlantı çıkarma işlemlerini birleştirerek URL'leri keşfeder ve her URL'yi source değeriyle birlikte döndürür. Site haritasından alınan

import os
import xml.etree.ElementTree as ET
import requests

API = "https://scraper.geonode.io"
HEADERS = {"X-Api-Key": os.environ["GEONODE_API_KEY"]}
SM_NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}

def urls_from_sitemap(sitemap_url: str, session: requests.Session) -> list[tuple[str, str | None]]:
    """Return (loc, lastmod) pairs. Follows sitemap index files one level."""
    root = ET.fromstring(session.get(sitemap_url, timeout=30).content)
    out: list[tuple[str, str | None]] = []
    for sm in root.findall("sm:sitemap", SM_NS):          # sitemap index
        out.extend(urls_from_sitemap(sm.findtext("sm:loc", namespaces=SM_NS), session))
    for u in root.findall("sm:url", SM_NS):
        out.append((u.findtext("sm:loc", namespaces=SM_NS),
                    u.findtext("sm:lastmod", namespaces=SM_NS)))
    return out

def urls_from_map(site_url: str, session: requests.Session) -> list[tuple[str, None]]:
    body = {"url": site_url, "include_subdomains": False, "ignore_query_parameters": True}
    r = session.post(f"{API}/v1/map", json=body, headers=HEADERS, timeout=120)
    r.raise_for_status()
    return [(link["url"], None) for link in r.json()["links"]]

lastmod değeri bir ipucudur, garanti değildir. Bazı CMS’ler her URL’ye yayınlanma zamanını ekler; diğerleri ise bunu asla güncellemez. Bunu, kontrolü daha erken yapmak için bir neden olarak değerlendirin, asla kontrolü tamamen atlamak için bir neden olarak görmeyin.

URL normalleştirme

Normalleştirme yapılmazsa, https://Example.com/docs/?utm_source=x#top ve https://example.com/docs iki satır ve iki veri alma işlemi haline gelir. Durum deposuna herhangi bir işlem yapılmadan önce normalleştirme işlemi gerçekleştirilmelidir.

from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

TRACKING = {"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
            "gclid", "fbclid", "ref", "mc_cid", "mc_eid"}

def normalize(url: str) -> str:
    parts = urlsplit(url.strip())
    scheme = parts.scheme.lower()
    host = parts.netloc.lower()
    if (scheme, host.rsplit(":", 1)[-1]) in {("http", "80"), ("https", "443")}:
        host = host.rsplit(":", 1)[0]
    query = [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True)
             if k.lower() not in TRACKING]
    query.sort()
    path = parts.path or "/"
    if len(path) > 1 and path.endswith("/"):
        path = path[:-1]                       # pick one convention and stick to it
    return urlunsplit((scheme, host, path, urlencode(query), ""))  # fragment dropped

Sonundaki eğik çizgi kuralı bir politika tercihidir. Bazı çerçeveler /docs ve /docs/ adreslerinde farklı içerik sunar. Hedef site bunlardan biriyse, o satırı kaldırın ve iki URL'nin ayrı satırlar olarak kalmasına izin verin.

Durum deposu

Tek bir SQLite tablosu, tarayıcının karar vermesi için ihtiyaç duyduğu tüm bilgileri barındırır. Sütunlar, bu kılavuzun geri kalanında ele alınan sinyallere karşılık gelir.

import sqlite3
import time

SCHEMA = """
CREATE TABLE IF NOT EXISTS pages (
  normalized_url TEXT PRIMARY KEY,
  url            TEXT NOT NULL,
  last_seen      REAL,
  last_fetched   REAL,
  next_check     REAL DEFAULT 0,
  interval_s     INTEGER DEFAULT 86400,
  content_hash   TEXT,
  etag           TEXT,
  last_modified  TEXT,
  sitemap_lastmod TEXT,
  missing_runs   INTEGER DEFAULT 0,
  gone           INTEGER DEFAULT 0
);
"""

def open_db(path: str = "crawl_state.sqlite") -> sqlite3.Connection:
    db = sqlite3.connect(path)
    db.executescript(SCHEMA)
    return db

def upsert_discovered(db: sqlite3.Connection, found: list[tuple[str, str | None]]) -> None:
    now = time.time()
    for raw, lastmod in found:
        n = normalize(raw)
        row = db.execute("SELECT sitemap_lastmod FROM pages WHERE normalized_url=?", (n,)).fetchone()
        if row is None:
            db.execute("INSERT INTO pages (normalized_url,url,last_seen,sitemap_lastmod) VALUES (?,?,?,?)",
                       (n, raw, now, lastmod))
        else:
            bump = lastmod is not None and lastmod != row[0]
            db.execute("UPDATE pages SET last_seen=?, missing_runs=0, sitemap_lastmod=?, "
                       "next_check=CASE WHEN ? THEN 0 ELSE next_check END WHERE normalized_url=?",
                       (now, lastmod, bump, n))
    db.commit()

, URL’yi listeleyen son keşif çalışmasını kaydeder; bu, last_fetched ’den ayrıdır. sitemap_lastmod değerinde bir değişiklik, next_check değerini sıfıra sıfırlar; böylece sayfa bir sonraki değişiklik kuyruğuna alınır.

Koşullu istekler: ETag ve If-Modified-Since

Bir sitenin ETag veya Last-Modified gönderdiği durumlarda, koşullu bir istek, kaynak sunucunun gövdesiz bir 304 yanıtı vermesini sağlar. Bu başlıkları /v1/extract komutunun headers alanından gönderir ve kaynak sunucunun durumunu metadata.http_status'den okursunuz. Bu istek, planınızın kotasından yine de düşer, ancak tam bir render işleminden daha hızlıdır.

Her site doğrulayıcıları desteklemez ve CDN’ler bazen bunları kaldırır. Aşağıdaki kod, doğrulayıcılar mevcutsa bunları gönderir; yanıt 200 ise hash işlemini gerçekleştirir.

Geçici blokların çıkarılmasından sonra içerik hashleme

Mevcut saati, bir CSRF belirtecini veya "ilgili makaleler" bloğunu görüntüleyen sayfalarda, Markdown değişikliklerinin ham hash değeri her çalıştırmada değişir. Hash işlemi yapmadan önce bunları çıkarın. Çıkarma kurallarını tek bir yerde tutun, böylece siteye göre ayarlayabilirsiniz.

import hashlib
import re

VOLATILE = [
    re.compile(r"\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?\S*"),   # ISO timestamps
    re.compile(r"(?i)(csrf|nonce|session)[_-]?token[^\s]*"),
    re.compile(r"(?i)^#+\s*related (posts|articles).*?(?=^#|\Z)", re.M | re.S),
    re.compile(r"[ \t]+"),
]

def content_fingerprint(markdown: str) -> str:
    text = markdown
    for rx in VOLATILE:
        text = rx.sub(" ", text)
    text = "\n".join(line.strip() for line in text.splitlines() if line.strip())
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

def fetch_one(session: requests.Session, row: sqlite3.Row | tuple) -> dict:
    """Sync extract with conditional headers. Returns a dict the caller can store."""
    normalized_url, url, etag, last_modified = row
    cond = {}
    if etag:
        cond["If-None-Match"] = etag
    if last_modified:
        cond["If-Modified-Since"] = last_modified
    body = {"url": url, "formats": ["markdown"], "render_js": False,
            "processing_mode": "sync", "headers": cond or None, "timeout": 60000}
    r = session.post(f"{API}/v1/extract", json=body, headers=HEADERS, timeout=90)
    if r.status_code == 429:
        raise RuntimeError(f"rate limited: {r.json().get('error', r.json())}")
    r.raise_for_status()
    payload = r.json()
    status = payload["metadata"].get("http_status")
    md = (payload.get("data") or {}).get("markdown") or ""
    return {"http_status": status, "markdown": md,
            "hash": content_fingerprint(md) if status == 200 else None}

Kaynağın ETag ve Last-Modified yanıt başlıklarının metadata içinde gösterilip gösterilmediği, kullandığınız API sürümüne bağlıdır; bunlara güvenmeden önce hesabınızın yanıtını kontrol edin. Eğer gösterilmiyorlarsa, içerik hash'i yine de size güvenilir bir "değişti/değişmedi" cevabı verir; sadece 304 kısayolunu kullanamazsınız.

Değişiklik kuyruğu ve uyarlanabilir yeniden ziyaret

Kuyruk, artımlı web taramasının kalbidir ve bir veri yapısı değil, bir sorgudur: next_check değeri geçmişte kalan ve gone olarak işaretlenmemiş her satır. Her veri alımından sonra aralık değişir. Değişiklik tespit edildiğinde: aralığı yarıya indirin, bir saate yuvarlayın. Değişiklik yok: 1,5 ile çarpın, üst sınır 30 gün. Birkaç hafta içinde zamanlama, her sayfanın gözlemlenen değişiklik sıklığına yakınsar.

MIN_S, MAX_S = 3600, 30 * 86400

def change_queue(db: sqlite3.Connection, limit: int = 500) -> list[tuple]:
    return db.execute(
        "SELECT normalized_url, url, etag, last_modified FROM pages "
        "WHERE gone=0 AND next_check<=? ORDER BY next_check LIMIT ?",
        (time.time(), limit)).fetchall()

def record_result(db: sqlite3.Connection, normalized_url: str, res: dict) -> str:
    now = time.time()
    old_hash, interval = db.execute(
        "SELECT content_hash, interval_s FROM pages WHERE normalized_url=?",
        (normalized_url,)).fetchone()
    if res["http_status"] in (404, 410):
        db.execute("UPDATE pages SET gone=1, last_fetched=? WHERE normalized_url=?", (now, normalized_url))
        db.commit()
        return "gone"
    changed = res["http_status"] == 200 and res["hash"] != old_hash
    interval = max(MIN_S, interval // 2) if changed else min(MAX_S, int(interval * 1.5))
    db.execute("UPDATE pages SET last_fetched=?, next_check=?, interval_s=?, "
               "content_hash=COALESCE(?, content_hash) WHERE normalized_url=?",
               (now, now + interval, interval, res["hash"], normalized_url))
    db.commit()
    return "changed" if changed else "unchanged"

http_status == 304 ve hash içermeyen bir 304 yanıtı geldiğinde, bu satır değişmemiş dalına girer ve aralık uzar. Herhangi bir 5xx veya ağ hatası, satırı olduğu gibi bırakmalı ve bir sonraki çalıştırmada yeniden denenmelidir; bunun için çalıştırıcınızda fetch_one'i bir try/except bloğu içine alın.

Silinen URL'leri Algılama

Birlikte kullanılan iki işaret. Doğrudan bir 404 veya 410 hatası, record_result işlevinde gone değerini anında değiştirir. Site haritasından kaybolan bir URL ise daha zayıf bir kanıttır; çünkü site haritaları gecikmeli çalışır ve bazen bazı bölümleri atlar. URL'nin bulunmadığı ardışık keşif çalıştırmalarını sayın ve ancak N kez tekrarlandıktan sonra doğrulama amaçlı bir get işlemi gerçekleştirin.

def mark_missing(db: sqlite3.Connection, run_started: float, n_runs: int = 3) -> None:
    """Call after upsert_discovered. Rows not seen this run get missing_runs+1."""
    db.execute("UPDATE pages SET missing_runs=missing_runs+1 WHERE last_seen<? AND gone=0", (run_started,))
    db.execute("UPDATE pages SET next_check=0 WHERE missing_runs>=? AND gone=0", (n_runs,))
    db.commit()

gone olarak işaretlenen satırlar tabloda kalır. Bu, eski bir gelen bağlantının URL'yi yeniymiş gibi yeniden eklemesini engeller. Sayfa geri gelirse, sonraki bir keşif çalıştırması onu tekrar listeleyecektir; bu davranışı istiyorsanız, upsert_discovered içinde gone değerini 0'a sıfırlayın.

Değişen kümeyi toplu olarak alma

Birkaç yüz URL için yukarıdaki senkronizasyon döngüsü yeterlidir ve olgun bir artımlı web tarama kurulumunda, değişen küme genellikle bu kadar küçüktür. Binlerce URL için, kuyruğu tek bir /v1/batch işi olarak gönderin ve bunu sorgulayın. Toplu iş gövdesi, tek bir ayıklama işleminde olduğu gibi aynı formats, render_js, proxy, headers ve wait_config alanlarını kabul eder ve bunları her URL'ye uygular; bu nedenle, koşullu başlıklar bir toplu iş içindeki URL'lere göre değişemez. Düz içerik-hash yolu için toplu işi kullanın ve ETag'ınızın bulunduğu URL'ler için senkronizasyon yolunu koruyun.

def batch_fetch(session: requests.Session, urls: list[str], poll_s: int = 10) -> dict[str, dict]:
    body = {"urls": urls, "formats": ["markdown"], "render_js": False, "ignore_invalid_urls": True}
    job = session.post(f"{API}/v1/batch", json=body, headers=HEADERS, timeout=60).json()
    while True:
        st = session.get(f"{API}/v1/batch/{job['job_id']}", headers=HEADERS, timeout=60).json()
        if st["status"] in ("completed", "failed", "cancelled"):
            break
        time.sleep(poll_s)
    out = {}
    for res in st.get("results", []):
        if res["status"] != "completed":
            continue
        md = (res.get("data") or {}).get("markdown") or ""
        out[res["url"]] = {"http_status": res["metadata"].get("http_status"),
                           "markdown": md, "hash": content_fingerprint(md)}
    return out

Alternatif olarak, tek tek /v1/extract çağrılarında processing_mode: "async" seçeneğini kullanabilirsiniz; bu durumda 202 yanıtı ve GET /v1/extract/{job_id} adresinde sorgulayabileceğiniz bir job_id döndürülür. Bu yöntem, URL başına başlıkları korur ancak her URL için bir istek gerektirir. Her iki durumda da, sonuçları senkronizasyon yolunda olduğu gibi record_result aracılığıyla aktarın.

Çalıştırmayı Oluşturma

Tek bir çalıştırma şu adımlardan oluşur: keşfetme, upsert, eksik olarak işaretleme, kuyruğu boşaltma, kaydetme. Bunu, güncelliği hedeflediğiniz sıklıkta cron veya bir zamanlayıcıdan çalıştırın; uyarlanabilir aralıklar, çalıştırıcının ne sıklıkta tetiklendiğine bakılmaksızın getirme sayısını düşük tutar.

def run(sitemap_url: str | None, site_url: str) -> None:
    db = open_db()
    session = requests.Session()
    started = time.time()
    found = urls_from_sitemap(sitemap_url, session) if sitemap_url else urls_from_map(site_url, session)
    upsert_discovered(db, found)
    mark_missing(db, started)
    queue = change_queue(db)
    with_etag = [r for r in queue if r[2] or r[3]]
    plain = [r for r in queue if not (r[2] or r[3])]
    for row in with_etag:
        try:
            record_result(db, row[0], fetch_one(session, row))
        except Exception as exc:          # leave row for next run
            print("retry later:", row[1], exc)
    if plain:
        results = batch_fetch(session, [r[1] for r in plain])
        for row in plain:
            if row[1] in results:
                record_result(db, row[0], results[row[1]])

Bu, artımlı web tarama modülünün tamamıdır. Aşağıdaki tablo, her bir sinyalin ne işe yaradığını özetlemektedir; böylece ihtiyacınız olmayan parçaları çıkarabilirsiniz.

SinyalMaliyetGüvenilirlikBu modülde ne işe yarar
Site Haritası lastmodbir site haritası alımıdüşük ila ortanext_check'i 0'a sıfırlar
ETag / Son Değişiklik Tarihibir istek, görüntüleme yokdesteklendiği yerlerde yüksek304, değişmedi anlamına gelir
İçerik hash'ibir tam alımyüksekgerçek değer karşılaştırması
Uyarlanabilir aralıkyokyokne zaman yeniden kontrol edileceğine karar verir
404 / 410bir istekyüksekgone olarak işaretler
Site haritasında N kez bulunmamayokdüşükdoğrulama amaçlı bir getirme işlemini zorlar

Sık yapılan hatalar

Geçici içeriği temizlemeden önce karma hesaplama

"14:02:11'de oluşturuldu" yazan bir sayfa her alımda değişir; bu nedenle tarayıcınız aralık süresini yarıya indirir ve en kısa süreye ulaştığında saatlik olarak yeniden alır. Her çalıştırmada değişiklik bildiren herhangi bir sayfa için ardışık iki alımın farklarına bakın ve bu kalıbı VOLATILE'ye ekleyin.

İlk eklemeden sonra normalleştirme

İlk çalıştırmada ham URL'ler depolanır ve normalleştirme daha sonra eklenirse, her satır kaçınılmaz bir yinelemeye dönüşür. Şemada olduğu gibi, ilk günden itibaren normalleştirin ve normalized_url'yi birincil anahtar yapın.

Yalnızca site haritasının yokluğuna güvenmek

Site haritaları dağıtımların gerisinde kalır, dizin dosyalarına bölünür ve bazen bir CMS değişikliğinden sonra bölümlerin tamamı kaybolur. Bir site haritası alımından kaybolduğu için bir sayfayı yok olarak işaretlemek, dizininizden canlı içeriği siler. Onaylayıcı bir 404 veya 410 yanıtı gerektirin.

URL başına koşullu başlıklarla toplu iş çalıştırma

/v1/batch, işteki her URL’ye tek bir headers nesnesi uygular. Bir sayfa için tasarlanan bir If-None-Match’ı binlerce başka sayfaya göndermek anlamsız sonuçlar doğurur. run()’ın yaptığı gibi, ETag içeren satırları senkronizasyon yolundan, geri kalanını ise toplu iş yolundan yönlendirin.

Sonraki adım

Yukarıdaki modül durumu tutar, içeriği karşılaştırır ve yeniden ziyaretleri planlar; bunun için, dayandığı 304 ve 404 hataları da dahil olmak üzere her URL için doğru bir http_status değeri içeren temiz Markdown metni döndüren bir veri alma katmanına ihtiyaç duyar. Geonode Scraper API, bu katmanı sağlar: istek başına başlık içeren tek sayfalar için /v1/extract, toplu olarak değiştirilen setler için /v1/batch ve site haritası olmadığında /v1/map. Ücretsiz kademede ayda 1.5K istek hakkı bulunur; bu, bir plan seçmeden önce gerçek bir sitede artımlı döngüyü test etmek için yeterlidir. Planlar için Scraper API sayfasına, istek kademeleri için ise fiyatlandırma sayfasına bakabilirsiniz.

SSS

Artımlı web taraması nedir?

Artımlı web taraması, her seferinde sitenin tamamını yeniden almak yerine, yalnızca son taramadan bu yana değişmiş olması muhtemel sayfaları yeniden ziyaret etmek anlamına gelir. Tarayıcı, her URL için bir parmak izi içeren bir durum deposu tutar, aday sayfaları düşük maliyetle tespit eder (sitemap lastmod, ETag, Last-Modified) ve indirme bütçesini gerçekten buna ihtiyaç duyan alt kümeye ayırır.

Sayfaları indirmeden hangilerinin değiştiğini nasıl anlarım?

Genellikle bir istek göndermeden kesin olarak bilemezsiniz, ancak aday sayfaları sıralayabilirsiniz. Site haritasındaki son değiştirilme tarihi (lastmod) en düşük maliyetli işarettir. If-None-Match veya If-Modified-Since içeren koşullu bir istek, doğrulayıcıları destekleyen sitelerde 304 yanıtı verir. Diğer tüm durumlarda, zaman damgaları ve oturum belirteçleri gibi geçici blokları ayıkladıktan sonra hesaplanan içerik hash'ini alın ve karşılaştırın.

Geonode Scraper API, durum ve değişiklik algılamasını benim için hallediyor mu?

Hayır. Scraper API, sayfaları alır ve Markdown veya HTML ile http_status gibi meta verileri döndürür. Durum deposu, URL normalizasyonu, hash hesaplama ve zamanlama işlemlerinden komut dosyanız sorumludur. Bu ayrım kasıtlıdır: API durum bilgisi içermez ve çağrılması ucuz kalırken, artımlı web tarama mantığınız sizin kontrol ettiğiniz ve test edebileceğiniz kodda yer alır.

Artımlı web taraması için yeniden ziyaretleri nasıl zamanlamalıyım?

Varsayılan bir aralıkla başlayın, ardından her URL’ye göre uyarlayın. Bir getirme işlemi bir değişiklik bulursa, aralığı bir alt sınıra kadar yarıya indirin; değişiklik bulmazsa, bir üst sınıra kadar çarpın. Sık güncellenen sayfalar sık sık kontrol edilirken, statik sayfalar üst sınıra doğru kayar. next_check değerini durum tablosunda saklayın ve next_check süresi geçmiş satırları sorgulayın.

Silinen sayfaları nasıl tespit edebilirim?

İki sinyali birlikte kullanın. Alım sırasında 404 veya 410 hatası güçlü bir sinyaldir. Arka arkaya N kez yapılan çalıştırmada site haritasında bulunmaması daha zayıf bir işarettir, çünkü site haritaları genellikle eksiktir. Yalnızca doğrudan bir getirme işlemi bunu doğruladığında satırı silinmiş olarak işaretleyin ve eski bağlantılardan URL'yi yeniden keşfedip yeniden almamak için satırı saklayın.

Uymam gereken sınırlar ve uyum kuralları nelerdir?

robots.txt dosyasını ve sitenin hizmet şartlarını dikkate alın, istek sıklığını makul seviyede tutun ve asla kimlik doğrulamayı atlamayın. Scraper API tarafında, ücretsiz kademede aylık 1.5K istek hakkı bulunurken, ücretli planlarda 2 ila 100 iş parçacığı arasında seçenekler mevcuttur; bu nedenle, değişiklik setinizin boyutunu planınıza göre ayarlayın. Koşullu istekler de istek olarak sayılır.