Bir scraper'ı hızlandırmak isteyen geliştiricinin ilk aklına gelen genellikle "daha çok çekirdek" olur: işi süreçlere bölmek, sunucuyu büyütmek. Oysa tipik bir scraping işinde işlemci zamanının çoğu boşta geçer. Program saniyelerce hedef sunucunun yanıtını, TLS el sıkışmasını, proxy'nin tüneli açmasını bekler. Bu bekleme süresini değerlendirmenin yolu concurrency (eşzamanlılık), işlemci gücünü değerlendirmenin yolu parallelism (paralellik) olarak adlandırılır ve ikisi farklı sorunları çözer.
Bu yazıda iki kavramın farkını, scraping'in neden çoğunlukla girdi/çıktı (I/O) ağırlıklı bir iş olduğunu ve Python'da asyncio, thread ve process seçeneklerinin hangi durumda işe yaradığını anlatıyoruz. Node.js'in event loop yapısına, hızı gerçekte neyin sınırladığına ve eşzamanlılık değerinin nasıl belirleneceğine de bakıyoruz. Örnek kodları yapay gecikmeli yerel bir sunucu üzerinde çalıştırdık; kendi işinizde ölçüm yapabilmeniz için ölçüm iskeletini de paylaşıyoruz.
Concurrency (eşzamanlılık) nedir?
Concurrency, birden fazla görevin aynı zaman diliminde ilerleyebilmesidir. Görevlerin aynı anda çalışması gerekmez; biri beklerken diğeri çalışır. Bir aşçının makarna suyunun kaynamasını beklerken salata doğraması eşzamanlılıktır: tek bir kişi, iki işi aynı sürede bitirir.
Programlamada bu, bir görevin bir yanıt beklediği anda kontrolü başka bir göreve bırakmasıyla sağlanır. Python'da asyncio, Node.js'te event loop bu mantıkla çalışır. Tek bir işlemci çekirdeği ve tek bir thread yüzlerce ağ isteğini aynı anda "açık" tutabilir, çünkü bu isteklerin hiçbiri beklerken işlemciyi kullanmaz.
Parallelism (paralellik) nedir?
Parallelism, birden fazla görevin fiziksel olarak aynı anda, farklı işlemci çekirdeklerinde yürütülmesidir. Mutfak örneğinde bu, iki aşçının aynı anda iki farklı yemeği hazırlamasıdır. İşin hızı çalışan sayısıyla doğrudan artar, ama her çalışanın kendi tezgâhı ve malzemesi olmalıdır.
Programlamada paralellik genellikle birden fazla süreç (process) veya gerçekten aynı anda çalışabilen thread'lerle sağlanır. Faydası, işin işlemciyi yoğun kullandığı durumlarda ortaya çıkar: büyük bir HTML belgesini ayrıştırmak, görsel işlemek, veri sıkıştırmak.
| Ölçüt | Concurrency (eşzamanlılık) | Parallelism (paralellik) |
|---|---|---|
| Temel fikir | Beklemeleri üst üste bindirmek | İşleri çekirdeklere bölmek |
| Çekirdek ihtiyacı | Tek çekirdek yeterli | Birden fazla çekirdek |
| Uygun olduğu iş | I/O ağırlıklı: ağ, disk, veritabanı | CPU ağırlıklı: ayrıştırma, hesaplama |
| Python araçları | asyncio, thread | multiprocessing, ProcessPoolExecutor |
| Node.js araçları | Event loop, Promise | worker_threads, birden fazla süreç |
| Bellek maliyeti | Düşük, görev başına küçük | Yüksek, süreç başına ayrı bellek |
| Scraping'deki rolü | İstekleri göndermek ve yanıtları almak | Sayfaları ayrıştırmak, headless tarayıcı çalıştırmak |
| Tipik hata | Sınırsız eşzamanlılıkla hedefi yormak | I/O işini süreçlere bölüp kaynak harcamak |
İki kavram birbirinin alternatifi değildir. Büyük bir scraping sistemi genellikle ikisini birlikte kullanır: her süreç kendi içinde yüzlerce isteği eşzamanlı yönetir, süreçler de farklı çekirdeklerde paralel çalışır.
Scraping neden I/O ağırlıklıdır?
Tek bir sayfa isteğinin geçtiği aşamalara bakınca işlemcinin ne kadar az çalıştığı görülür:
- DNS çözümlemesi. Alan adının IP adresine çevrilmesi. Ağ üzerinden bir sorgu ve yanıt beklenir.
- Proxy'ye bağlantı ve tünel. Proxy'ye TCP bağlantısı açılır,
CONNECTisteği gönderilir, proxy hedefe bağlanana kadar beklenir. - TLS el sıkışması. Hedef sunucuyla şifreli bağlantı kurulur. Birkaç gidiş dönüş gerektirir.
- İsteğin gönderilmesi ve yanıtın beklenmesi. Hedef sunucu sayfayı hazırlar; veritabanı sorgusu içeren sayfalarda bu süre uzar.
- Yanıtın indirilmesi. Sayfa boyutu ve bant genişliğine bağlıdır.
- Ayrıştırma. HTML'den verinin çıkarılması. Bu adım işlemciyi kullanan tek adımdır.
İlk beş adımda program ağdan gelecek baytları bekler. Sayfaları sırayla indiren bir betikte ayrıştırma, toplam sürenin küçük bir kısmıdır; betik zamanının büyük bölümünü bekleyerek geçirir ve işlemci bu sırada boştadır.
Ancak bu oran sabit değildir. Her yanıtı yapay olarak 300 milisaniye geciktiren yerel bir sunucuda, her biri 400 ürün kartı içeren 40 sayfayı indirip ayrıştırdık. Eşzamanlılığı 1'den 5'e ve 20'ye çıkarmak indirme süresini on kattan fazla kısalttı. Eşzamanlılık 20'deyken ise aynı sayfaları tek çekirdekte ayrıştırmak, indirmekten uzun sürdü; süreç havuzuna geçmek bu adımı yaklaşık yarıya indirdi. Yani eşzamanlılık arttıkça darboğaz ağdan işlemciye kayabilir. Kendi hedeflerinizde rakamlar farklı olacaktır; aşağıdaki ölçüm iskeletiyle kendi işinizi ölçmenizi öneririz.
Python'da asyncio, thread ve process farkı nedir?
Python'da üç araç vardır ve seçimi anlamanın anahtarı GIL (Global Interpreter Lock) kavramıdır. Standart CPython yorumlayıcısında GIL, aynı anda yalnızca bir thread'in Python kodu çalıştırmasına izin verir. Ancak bir thread ağdan veri beklerken GIL'i bırakır. Sonuç:
asyncio: Tek thread içinde çalışan bir event loop. Görevlerawaitnoktalarında kontrolü birbirine bırakır. Yüzlerce hatta binlerce eşzamanlı ağ isteği için en hafif seçenektir. asyncio belgesi bu modelin temel yapı taşlarını anlatır. Kütüphanenin de asenkron olması gerekir: HTTPX'inAsyncClient'ı veya AIOHTTP.- Thread (
ThreadPoolExecutor): Ağ beklerken GIL bırakıldığı için I/O işlerinde gerçek bir hızlanma sağlar. Requests gibi senkron kütüphanelerle eşzamanlılık kurmanın en kolay yoludur. Thread başına bellek maliyetiasynciogörevinden yüksektir, bu yüzden birkaç yüz eşzamanlı istekten sonra verimi düşer. - Process (
ProcessPoolExecutor,multiprocessing): Her süreç kendi yorumlayıcısı ve kendi GIL'iyle çalışır; bu yüzden işlemci ağırlıklı işlerde gerçek paralellik sağlar. Süreç başlatmak ve veriyi süreçler arasında taşımak maliyetlidir. concurrent.futures belgesi thread ve process havuzlarının ortak arayüzünü tanımlar.
Python 3.13 ile GIL'siz çalışabilen deneysel bir derleme de sunuldu; ancak kütüphane uyumluluğu henüz sınırlı olduğu için üretim scraping işlerinde standart derleme ve yukarıdaki üçlü yaygın kullanılmaya devam ediyor.
| Araç | Paralellik sağlar mı? | Uygun iş | Scraping'de kullanımı |
|---|---|---|---|
asyncio + HTTPX/AIOHTTP | Hayır, tek thread | Çok sayıda ağ isteği | Sayfaları indirmek |
ThreadPoolExecutor + Requests | I/O sırasında fiilen evet | Orta sayıda ağ isteği, senkron kod | Mevcut Requests kodunu hızlandırmak |
ProcessPoolExecutor | Evet | İşlemci ağırlıklı işler | Büyük sayfaları ayrıştırmak |
Python kütüphanelerinin eşzamanlılık destekleri arasındaki farkları HTTPX, Requests ve AIOHTTP yazımızda karşılaştırdık.
Örnek 1: asyncio.Semaphore ile sınırlı eşzamanlılık
Aşağıdaki kod sayfaları eşzamanlı indirir ama aynı anda açık istek sayısını limit değeriyle sınırlar. Semaphore olmadan asyncio.gather bütün istekleri tek seferde başlatır; bu hem kendi bağlantı havuzunuzu hem hedef siteyi zorlar.
import asyncio
import httpx
async def fetch(client, sem, url):
async with sem:
response = await client.get(url)
response.raise_for_status()
return response.text
async def fetch_all(urls, limit=10, proxy=None):
sem = asyncio.Semaphore(limit)
async with httpx.AsyncClient(proxy=proxy, timeout=30) as client:
return await asyncio.gather(*(fetch(client, sem, u) for u in urls), return_exceptions=True)
urls = [f"https://ornek.com/urun/{i}" for i in range(100)]
pages = asyncio.run(fetch_all(urls, limit=10, proxy="http://kullanici:parola@pr.proxynet.io:8000"))Tek bir AsyncClient bütün istekler için paylaşılır. Bu, bağlantıların yeniden kullanılmasını sağlar ve her istekte yeni TLS el sıkışması yapılmasını önler.
Örnek 2: indirme eşzamanlı, ayrıştırma paralel
Sayfalar büyükse ve ayrıştırma belirgin bir süre alıyorsa iki modeli birleştirmek mantıklıdır: indirme asyncio ile, ayrıştırma süreç havuzunda.
import asyncio
from concurrent.futures import ProcessPoolExecutor
from bs4 import BeautifulSoup
def parse(html):
soup = BeautifulSoup(html, "html.parser")
return [(u.h2.get_text(strip=True), u.select_one(".fiyat").get_text(strip=True)) for u in soup.select(".urun")]
async def scrape(urls, limit=10, proxy=None):
pages = await fetch_all(urls, limit=limit, proxy=proxy)
html_pages = [p for p in pages if isinstance(p, str)]
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
return await asyncio.gather(*(loop.run_in_executor(pool, parse, html) for html in html_pages))
if __name__ == "__main__":
results = asyncio.run(scrape(urls, limit=10))if __name__ == "__main__": satırı Windows ve macOS'ta zorunludur: süreç havuzu yeni süreçleri modülü baştan yükleyerek başlatır ve bu koruma olmadan kod kendini tekrar tekrar çalıştırır.
Bu yapının her zaman hızlandırmadığını bilmek önemlidir. Süreç başlatmanın ve HTML'i süreçler arasında taşımanın bir maliyeti vardır; sayfalar küçük ve az sayıdaysa bu maliyet ayrıştırmanın kendisinden büyük olabilir. Yukarıdaki testte 400 kartlık sayfalarda süreç havuzu ayrıştırmayı belirgin biçimde kısalttı, ama indirme ve ayrıştırmayı birleştiren toplam süreye süreç başlatma maliyeti de eklendi. Karar vermeden önce kendi sayfalarınızla ölçün.
Node.js event loop nasıl çalışır?
Node.js, JavaScript kodunu tek bir thread'de çalıştırır ve ağ işlemlerini bir event loop üzerinden yönetir. Bir fetch çağrısı yapıldığında Node.js isteği işletim sistemine bırakır ve JavaScript kodu başka işlere devam eder; yanıt geldiğinde ilgili geri çağırma sıraya alınır. Node.js'in event loop rehberi bu döngünün aşamalarını ayrıntılı anlatır.
Scraping açısından bunun üç sonucu vardır:
- Ağ istekleri doğal olarak eşzamanlıdır.
Promisetabanlı her istek, beklerken diğerlerini engellemez. Python'dakiasynciomodeline benzer. - İşlemciyi yoran kod bütün döngüyü durdurur. Büyük bir HTML'i ayrıştıran senkron bir fonksiyon çalışırken başka hiçbir yanıt işlenemez. Bu tür işler için
worker_threadsmodülüyle ayrı thread'ler veya ayrı süreçler kullanılır. - Alan adı çözümlemesi gizli bir darboğaz olabilir. Node.js'in varsayılan
dns.lookupfonksiyonu işletim sisteminin çözümleyicisini libuv'un küçük bir thread havuzunda çalıştırır. Çok sayıda farklı alan adına aynı anda bağlanan bir betikte bu havuz dolabilir. Proxy kullanırken hedef alan adları proxy tarafında çözüldüğü için bu etki azalır.
Eşzamanlılığı sınırlamak için ek kütüphane gerekmez; basit bir sınırlayıcı yeterlidir:
function limiter(max) {
let active = 0;
const queue = [];
const next = () => {
if (active >= max || queue.length === 0) return;
active++;
const { task, resolve, reject } = queue.shift();
task().then(resolve, reject).finally(() => {
active--;
next();
});
};
return (task) => new Promise((resolve, reject) => {
queue.push({ task, resolve, reject });
next();
});
}
const limit = limiter(10);
const results = await Promise.allSettled(
urls.map((url) => limit(() => fetch(url).then((r) => r.text()))),
);Node.js'te proxy'nin nasıl verileceğini, rotasyonlu ve yeniden denemeli bir örnekle birlikte Node.js'te Proxy Kullanımı yazımızda anlattık.
Hızı gerçekte ne sınırlar?
Eşzamanlılığı artırmak bir noktaya kadar hızı artırır, sonra ya etkisiz kalır ya da işi yavaşlatır. Scraping işlerinde üst sınırı genellikle işlemci değil, şu etkenler belirler:
- Hedef sitenin hız sınırı. Site aynı kaynaktan dakikada belirli sayıda isteğe izin veriyorsa, eşzamanlılığı artırmak yalnızca daha fazla
429yanıtı üretir. Bu kodların nasıl ele alınacağını Scraping'de HTTP Hata Kodları yazımızda anlattık. - Hedef sunucunun kapasitesi. Küçük bir sitenin sunucusu yavaşladığında sizin eşzamanlılığınız o sitenin yanıt süresini uzatır; hem sizin işiniz hem sitenin gerçek ziyaretçileri yavaşlar.
- Proxy kapasitesi. Proxy paketinin eşzamanlı bağlantı sınırı, havuzdaki IP sayısı ve çıkış noktalarının yanıt süresi. Tek adres üzerinden çok sayıda çıkış IP'si sunan Rotating Proxy ile istekler farklı adreslere dağılır, ama pakete tanımlı bağlantı sınırı yine geçerlidir.
- Bant genişliği. Büyük sayfalar ve görsel içeren yanıtlar indirilirken kendi bağlantınızın ya da proxy trafiğinizin sınırı devreye girer.
- Bağlantı kurma maliyeti. Her istekte yeni bağlantı ve yeni TLS el sıkışması yapılıyorsa süre artar. İstemciyi paylaşmak ve bağlantıları yeniden kullanmak bu maliyeti düşürür.
- Mesafe. Proxy çıkış noktası ile hedef sunucu arasındaki gecikme, her gidiş dönüşe eklenir. Gerçek ev bağlantılarından çıkan Residential Proxy adreslerinde bu süre konuma ve hatta göre değişir.
- İşlemci. Yalnızca headless tarayıcı çalıştırırken, çok büyük belgeleri ayrıştırırken veya aynı makinede yoğun veri işleme yaparken belirleyici olur.
Engellenmeden, siteye zarar vermeden hızı ayarlamanın diğer yollarını Web Scraping'de Engellenmeden Veri Toplama Yöntemleri yazımızda anlattık.
Eşzamanlılık kaça ayarlanmalı?
Her iş için geçerli tek bir sayı yoktur; doğru değer ölçülerek bulunur. Önerdiğimiz yöntem:
- Düşük bir değerle başlayın. Tek bir hedef site için birkaç eşzamanlı istek.
- Üç şeyi ölçün. Dakikada tamamlanan sayfa sayısı, ortalama yanıt süresi ve hata oranı (
429,503, zaman aşımı). - Değeri kademeli artırın. Her adımda aynı ölçümü tekrarlayın.
- Dönüm noktasını bulun. Tamamlanan sayfa sayısı artmayı bırakıp yanıt süresi veya hata oranı yükselmeye başladığında bir önceki değere dönün.
- Site başına sınır koyun. Birden fazla siteyi aynı anda tarıyorsanız toplam eşzamanlılık yüksek olabilir, ama her siteye düşen pay küçük kalmalıdır.
- Periyodik olarak tekrar ölçün. Hedef sitenin altyapısı ve hız sınırları zamanla değişir.
Ölçüm için basit bir iskelet:
import asyncio
import time
def measure(label, fn):
start = time.perf_counter()
result = fn()
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.2f} s")
return result
for limit in (1, 5, 10, 20):
pages = measure(f"limit={limit}", lambda: asyncio.run(fetch_all(urls, limit=limit)))
errors = sum(isinstance(p, Exception) for p in pages)
print(f" hata: {errors} / {len(pages)}")time.perf_counter(), sistem saatindeki değişikliklerden etkilenmeyen yüksek çözünürlüklü bir sayaçtır ve süre ölçümü için time.time()'dan daha uygundur. Ölçümü hedef siteye yük bindirmeyecek küçük bir URL listesiyle ve mümkünse sitenin yoğun olmadığı saatlerde yapın.
Kullanım senaryoları
- Tek siteden sınırlı sayıda sayfa: Senkron Requests ve istekler arasında bekleme. Eşzamanlılık gerekmez.
- Çok sayıda siteden fiyat ve katalog toplama:
asyncioile site başına küçük limitli eşzamanlılık, dağıtım için rotasyonlu proxy. Genel kurgu veri kazıma çözümü sayfamızda. - Büyük ölçekli tarama (crawl): Birden fazla süreç, her birinde
asyncio; alan adı başına kuyruk. Ölçekleme tarafı web crawler çözümü sayfamızda. - Mevcut Requests betiğini hızlandırmak: Kodu yeniden yazmadan
ThreadPoolExecutorile. Rotasyonlu bir Requests kurulumu için Python ile Rotating Proxy yazımıza bakabilirsiniz. - JavaScript ile yüklenen sayfalar: Headless tarayıcı işlemciyi ve belleği yoğun kullandığı için eşzamanlı tarayıcı sayısı çekirdek ve bellekle sınırlanır.
Sık yapılan hatalar
- I/O ağırlıklı işi süreçlere bölmek. Süreç başlatma ve bellek maliyetini ödersiniz, ama darboğaz ağ olduğu için hız artmaz.
asyncioiçinde senkron kütüphane kullanmak.async defiçindekirequests.get()çağrısı event loop'u durdurur ve bütün görevler sırayla çalışır.- Sınırsız
gatherveyaPromise.all. Binlerce isteği tek seferde başlatmak bağlantı hatalarına ve hedef sitede hız sınırına yol açar. - Her istekte yeni istemci oluşturmak. Bağlantılar yeniden kullanılmaz, her istek yeni TLS el sıkışması yapar.
- Toplam süreye bakıp hata oranına bakmamak. Hızlanan ama
429oranı artan bir iş, daha az veri toplar. - Node.js'te ayrıştırmayı ana thread'de yapmak. Büyük belgelerde event loop durur ve bekleyen yanıtlar zaman aşımına uğrayabilir.
Karar rehberi
| Durumunuz | Öneri |
|---|---|
| Çok sayıda sayfa, küçük HTML | asyncio + HTTPX/AIOHTTP, Semaphore ile limit |
| Mevcut senkron Requests kodu | ThreadPoolExecutor |
| Büyük sayfalar, ağır ayrıştırma | İndirme asyncio, ayrıştırma ProcessPoolExecutor |
| Node.js ile sayfa indirme | Promise + eşzamanlılık sınırlayıcı |
| Node.js'te ağır ayrıştırma | worker_threads |
| Headless tarayıcı | Çekirdek ve bellek sayısına göre az sayıda eşzamanlı tarayıcı |
429 oranı artıyor | Eşzamanlılığı düşürün, site başına sınır koyun |
| Hız artmıyor, hata da yok | Darboğazı ölçün: proxy, bant genişliği, hedef sunucu |
Sık sorulan sorular
Concurrency ile parallelism aynı şey mi?
Hayır. Concurrency, görevlerin aynı zaman diliminde ilerlemesidir ve tek çekirdekte de gerçekleşebilir. Parallelism, görevlerin birden fazla çekirdekte gerçekten aynı anda çalışmasıdır. Paralel çalışan her sistem eşzamanlıdır, ama eşzamanlı her sistem paralel değildir.
Scraping için asyncio mu thread mi?
Sıfırdan yazıyorsanız ve çok sayıda istek göndereceksiniz asyncio daha az kaynakla daha fazla eşzamanlı istek yönetir. Mevcut kodunuz Requests ile yazılmışsa ve eşzamanlı istek sayısı birkaç yüzü geçmeyecekse ThreadPoolExecutor kodu yeniden yazmadan hızlandırmanın en kolay yoludur.
GIL scraping'i yavaşlatır mı?
Ağ istekleri sırasında GIL bırakıldığı için sayfa indirmeyi pratikte yavaşlatmaz. Ayrıştırma gibi işlemci ağırlıklı adımlarda ise thread'ler aynı anda Python kodu çalıştıramaz; bu adımlar için süreç havuzu kullanılır.
Daha fazla proxy IP'si hızı artırır mı?
Hedef sitenin hız sınırı IP başına sayılıyorsa, yükü dağıtarak aynı sürede daha fazla sayfaya ulaşmanızı sağlayabilir. Ancak site bu sınırı başka ölçütlere göre uyguluyorsa ya da darboğaz bant genişliği veya hedef sunucunun kapasitesiyse IP sayısı sonucu değiştirmez. Siteye zarar vermemek, IP sayısından bağımsız bir sorumluluktur.
Eşzamanlılık değerini çok yüksek tutarsam ne olur?
Kendi tarafınızda bağlantı havuzu ve dosya tanıtıcısı sınırlarına, hedef tarafta 429 yanıtlarına, zaman aşımlarına ve sitenin yavaşlamasına yol açar. Bir noktadan sonra tamamlanan sayfa sayısı artmaz, hata sayısı artar.
Headless tarayıcılarda eşzamanlılık nasıl ayarlanır?
Her tarayıcı örneği belirgin miktarda bellek ve işlemci kullanır; bu yüzden eşzamanlı sayfa sayısı ağ değil makine kaynaklarıyla sınırlanır. Az sayıda tarayıcı örneğiyle başlayıp bellek ve işlemci kullanımını izleyerek artırın. Headless tarayıcının ne zaman gerçekten gerektiğini Statik ve Dinamik Sayfalar yazımızda anlattık.
Özetle
Concurrency beklemeleri değerlendirir, parallelism işlemci çekirdeklerini. Scraping'de zamanın büyük kısmı ağ yanıtı beklemekle geçtiği için hızı çoğunlukla asyncio, thread veya Node.js event loop ile kurulan sınırlı eşzamanlılık artırır; süreç havuzu yalnızca ağır ayrıştırma ve headless tarayıcı gibi işlemci yoğun adımlarda gerekir. Üst sınırı ise genellikle hedef sitenin hız sınırı, proxy kapasitesi ve bant genişliği belirler. Eşzamanlılık değerini tahminle değil ölçümle belirleyin ve site başına sınır koyun. Ölçekli veri toplama işleri için proxy hizmetlerimize göz atabilirsiniz.




