Neden Cheerio + Axios ile Web Kazıma?
Node.js ekosisteminde web kazıma (scraping) yapmanın en hafif ve en hızlı yolu, sunucu tarafında HTML'yi ayrıştırmaktır. Puppeteer veya Playwright gibi headless tarayıcılar güçlüdür ama her istek için bir Chromium örneği başlatmak bellek ve zaman açısından pahalıdır. Cheerio, jQuery benzeri bir API ile ham HTML'yi ayrıştırır; axios ise HTTP isteklerini yönetir. İkisi birlikte, bir tarayıcıya ihtiyaç duymadan saniyede yüzlerce sayfa çekebilir.
Ancak ölçeklendikçe bir sorun ortaya çıkar: IP engellemeleri. Tek bir IP'den binlerce istek attığınızda, hedef site sizi 403 veya 429 ile reddeder. İşte bu noktada proxy rotasyonu devreye girer. Bu rehberde, axios + Cheerio zincirine proxy desteğini nasıl entegre edeceğinizi, dönen bir residential proxy havuzunu yeniden kullanılabilir bir axios interceptor olarak nasıl saracağınızı ve 10.000 URL'lik bir listeyi nasıl güvenli şekilde kazıyacağınızı adım adım göstereceğiz.
Axios + Cheerio ile Sunucu Tarafı HTML Ayrıştırma
Temel akış basittir: axios ile HTML'i indirin, Cheerio ile ayrıştırın, CSS seçicileriyle veriyi çıkarın. Başlangıç için herhangi bir proxy gerekmez — tek bir sayfayı çekmek için yeterlidir.
import axios from 'axios';
import * as cheerio from 'cheerio';
async function scrapeProduct(url) {
const { data: html } = await axios.get(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Language': 'en-US,en;q=0.9',
},
timeout: 15_000,
});
const $ = cheerio.load(html);
return {
title: $('h1.product-title').text().trim(),
price: $('span.price').first().text().trim(),
availability: $('span.stock-status').text().trim(),
image: $('img.product-image').attr('src'),
};
}
const product = await scrapeProduct('https://example-shop.com/product/12345');
console.log(product);
Bu yaklaşımın gücü basitliğinde yatar. Bir tarayıcı başlatmadan, DOM render beklemeden, JavaScript çalıştırmadan ham HTML'yi ayrıştırırsınız. Statik sayfalar için bu genellikle yeterlidir.
Proxy Entegrasyonu: https-proxy-agent ve Axios
Bir proxy üzerinden istek yapmak için axios'un proxy yapılandırmasını kullanabilirsiniz, ancak SOCKS5 desteği ve daha ince kontrol için https-proxy-agent tercih edilir. ProxyHat'ın residential proxy'sini axios ile şu şekilde kullanırsınız:
import { HttpsProxyAgent } from 'https-proxy-agent';
import axios from 'axios';
const proxyAgent = new HttpsProxyAgent(
'http://user-country-US:PASSWORD@gate.proxyhat.com:8080'
);
const { data: html } = await axios.get('https://example-shop.com/product/12345', {
httpsAgent: proxyAgent,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
timeout: 15_000,
});
console.log(html.slice(0, 200));
Geo-Hedefleme ve Sticky Oturumlar
ProxyHat kullanıcı adı alanında ülke, şehir ve oturum parametreleri destekler. Bu, aynı IP'yi bir süre tutmak (sticky session) veya belirli bir bölgeden görünmek için kullanılır:
// Almanya - Berlin IP'si ile sticky oturum
const stickyAgent = new HttpsProxyAgent(
'http://user-country-DE-city-berlin-session-abc123:PASSWORD@gate.proxyhat.com:8080'
);
// Japonya IP'si ile her istekte yeni IP (rotasyon)
const rotatingAgent = new HttpsProxyAgent(
'http://user-country-JP:PASSWORD@gate.proxyhat.com:8080'
);
session-abc123 parametresi, aynı oturum kimliğiyle yapılan isteklerin aynı IP'ye yönlendirilmesini sağlar. Farklı bir oturum kimliği verdiğinizde yeni bir IP alırsınız. Per-request rotasyon için session parametresini kaldırmanız yeterlidir.
Cheerio Ne Zaman Yeterli? Statik vs. Dinamik Sayfalar
Cheerio, yalnızca sunucudan gelen ham HTML'i ayrıştırabilir. Eğer sayfa içeriği JavaScript ile client-side render ediliyorsa (React, Vue, Next.js client-side), Cheerio boş bir DOM görür. Hangi yöntemi seçeceğinize karar vermek için sayfanın kaynak koduna bakın: tarayıcıda View Source ile gördüğünüz HTML'de veri varsa Cheerio yeterlidir; yoksa headless tarayıcı gerekir.
| Kriter | Cheerio + Axios | Puppeteer / Playwright |
|---|---|---|
| Hız (sayfa/saniye) | 50–200+ | 5–15 |
| Bellek kullanımı | ~20 MB | ~300–500 MB |
| JS render desteği | Hayır | Evet |
| CAPTCHA karşılaşma | Düşük (ham HTML) | Yüksek (tam render) |
| Bakım maliyeti | Düşük | Yüksek |
| En uygun senaryo | SSR siteler, e-ticaret, bloglar | SPA'lar, infinite scroll, JS-dependent |
Kural şudur: Hedef sitenin kaynak kodunda veriyi görebiliyorsanız Cheerio kullanın. Headless tarayıcı, yalnızca JavaScript çalıştırmak zorunda olduğunuzda bir seçenektir. Ayrıca, Cheerio ile yapılan istekler daha az şüpheli görünür çünkü bir tarayıcı parmak izi (fingerprint) bırakmazlar.
Dönen Residential Proxy Havuzu: Axios Interceptor
Proxy rotasyonunu her istekte manuel olarak yönetmek sürdürülebilir değildir. Bunun yerine, bir axios interceptor ile her başarısız istekte proxy'yi değiştiren, otomatik rotasyon yapan bir katman yazabilirsiniz. Bu interceptor, tüm axios isteklerine şeffaf şekilde entegre edilir.
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
const PROXY_CONFIG = {
host: 'gate.proxyhat.com',
port: 8080,
username: 'user',
password: 'PASSWORD',
countries: ['US', 'DE', 'GB', 'FR'],
};
function createRotatingProxyAgent(sessionId) {
const country =
PROXY_CONFIG.countries[
Math.floor(Math.random() * PROXY_CONFIG.countries.length)
];
const proxyUrl = `http://${PROXY_CONFIG.username}-country-${country}-session-${sessionId}:${PROXY_CONFIG.password}@${PROXY_CONFIG.host}:${PROXY_CONFIG.port}`;
return new HttpsProxyAgent(proxyUrl);
}
function createScraperClient(baseConfig = {}) {
const client = axios.create({
timeout: 20_000,
...baseConfig,
});
client.interceptors.request.use((config) => {
const sessionId = `sess-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
config.httpsAgent = createRotatingProxyAgent(sessionId);
config.metadata = { sessionId, retries: 0 };
return config;
});
client.interceptors.response.use(
(response) => response,
async (error) => {
const config = error.config;
const retries = config.metadata?.retries ?? 0;
const MAX_RETRIES = 3;
if (retries >= MAX_RETRIES) return Promise.reject(error);
const status = error.response?.status;
if ([403, 429, 503].includes(status) || error.code === 'ECONNRESET') {
const newSessionId = `sess-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
config.httpsAgent = createRotatingProxyAgent(newSessionId);
config.metadata.retries = retries + 1;
config.metadata.sessionId = newSessionId;
const backoff = Math.pow(2, retries) * 1000 + Math.random() * 500;
await new Promise((r) => setTimeout(r, backoff));
return client.request(config);
}
return Promise.reject(error);
}
);
return client;
}
const scraper = createScraperClient();
const { data } = await scraper.get('https://example-shop.com/product/12345');
Bu interceptor üç kritik iş yapar: (1) Her isteğe yeni bir proxy oturumu atar, (2) 403/429/503 hatalarında otomatik olarak yeni bir IP ile yeniden dener, (3) Üstel geri çekilme (exponential backoff) ile hedef siteyi aşırı yüklememek için bekler. Böylece iş mantığı kodunuz proxy detaylarından tamamen soyutlanır.
Eşzamanlı Kazıma: p-limit ile Paralel İstekler
10.000 URL'yi sıralı olarak kazımak saatler sürebilir. Ancak sınırsız paralelliğe izin verirseniz, hedef siteyi DDoS gibi bombardıman edersiniz ve IP'leriniz hızla engellenir. p-limit, eşzamanlı istek sayısını kontrol altında tutarken maksimum verimlilik sağlar.
import pLimit from 'p-limit';
import * as cheerio from 'cheerio';
const CONCURRENCY = 20;
const limit = pLimit(CONCURRENCY);
async function scrapeWithCheerio(client, url) {
const { data: html } = await client.get(url);
const $ = cheerio.load(html);
return {
url,
title: $('h1').text().trim(),
price: $('[data-price]').first().text().trim(),
inStock: $('.availability').text().includes('In Stock'),
};
}
async function scrapeBatch(client, urls) {
const tasks = urls.map((url) =>
limit(() =>
scrapeWithCheerio(client, url).catch((err) => ({
url,
error: err.message,
status: err.response?.status,
}))
)
);
return Promise.all(tasks);
}
const urls = Array.from({ length: 100 }, (_, i) =>
`https://example-shop.com/product/${i + 1}`
);
const results = await scrapeBatch(scraper, urls);
console.log(`Başarılı: ${results.filter((r) => !r.error).length}`);
console.log(`Başarısız: ${results.filter((r) => r.error).length}`);
CONCURRENCY değerini hedef sitenin toleransına göre ayarlayın. Residential proxy'ler ile 15–25 arası iyi bir başlangıçtır. Datacenter proxy'ler ile bu değeri 50'ye çıkarabilirsiniz, ancak engellenme riski artar. Her zaman küçük bir değerle başlayıp kademeli olarak artırın.
Gerçek Dünya Örneği: 10.000 URL'lik E-Ticaret Kazıma
Şimdi tüm parçaları birleştirelim: bir e-ticaret sitesinden 10.000 ürün sayfasını kazıyan, proxy rotasyonu yapan, hataları yöneten ve sonuçları diske yazan tam bir pipeline.
import fs from 'fs/promises';
import pLimit from 'p-limit';
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as cheerio from 'cheerio';
// --- Yapılandırma ---
const CONCURRENCY = 20;
const BATCH_SIZE = 200;
const MAX_RETRIES = 3;
const OUTPUT = 'products.jsonl';
const PROXY = {
user: 'user',
pass: 'PASSWORD',
host: 'gate.proxyhat.com',
port: 8080,
};
function proxyAgent(country = 'US') {
const sid = Math.random().toString(36).slice(2, 10);
return new HttpsProxyAgent(
`http://${PROXY.user}-country-${country}-session-${sid}:${PROXY.pass}@${PROXY.host}:${PROXY.port}`
);
}
// --- Circuit Breaker ---
class CircuitBreaker {
constructor(threshold = 5, cooldown = 30_000) {
this.failures = 0;
this.threshold = threshold;
this.cooldown = cooldown;
this.openUntil = 0;
}
recordFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.openUntil = Date.now() + this.cooldown;
console.warn(`[CircuitBreaker] Açılıyor — ${this.cooldown}ms soğuma`);
}
}
recordSuccess() {
this.failures = 0;
}
async waitIfOpen() {
if (Date.now() < this.openUntil) {
const wait = this.openUntil - Date.now();
await new Promise((r) => setTimeout(r, wait));
this.failures = 0;
}
}
}
// --- Scraper ---
const limit = pLimit(CONCURRENCY);
const breaker = new CircuitBreaker();
async function scrapeProduct(url, retryCount = 0) {
await breaker.waitIfOpen();
try {
const countries = ['US', 'DE', 'GB', 'FR', 'CA'];
const country = countries[Math.floor(Math.random() * countries.length)];
const { data: html } = await axios.get(url, {
httpsAgent: proxyAgent(country),
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Language': 'en-US,en;q=0.9',
},
timeout: 20_000,
});
const $ = cheerio.load(html);
breaker.recordSuccess();
return {
url,
title: $('h1.product-title').text().trim(),
price: $('span.price').first().text().trim(),
availability: $('span.stock-status').text().trim(),
image: $('img.product-image').attr('src') ?? null,
};
} catch (err) {
const status = err.response?.status;
breaker.recordFailure();
if (retryCount < MAX_RETRIES && [403, 429, 503].includes(status)) {
const backoff = Math.pow(2, retryCount) * 1000 + Math.random() * 1000;
console.warn(`[Retry ${retryCount + 1}] ${status} — ${url} — ${Math.round(backoff)}ms`);
await new Promise((r) => setTimeout(r, backoff));
return scrapeProduct(url, retryCount + 1);
}
return { url, error: true, status: status ?? err.code };
}
}
// --- Ana Pipeline ---
async function main() {
const urls = Array.from(
{ length: 10_000 },
(_, i) => `https://example-shop.com/product/${i + 1}`
);
const stream = await fs.open(OUTPUT, 'w');
let completed = 0;
let failed = 0;
for (let i = 0; i < urls.length; i += BATCH_SIZE) {
const batch = urls.slice(i, i + BATCH_SIZE);
const results = await Promise.all(
batch.map((url) =>
limit(() => scrapeProduct(url).then((r) => {
completed++;
if (r.error) failed++;
if (completed % 500 === 0) {
console.log(`İlerleme: ${completed}/${urls.length} (başarısız: ${failed})`);
}
return r;
}))
)
);
for (const result of results) {
await stream.write(JSON.stringify(result) + '\n');
}
}
await stream.close();
console.log(`Tamamlandı: ${completed} başarılı, ${failed} başarısız`);
}
main();
Bu pipeline'ın önemli tasarım kararları:
- Batch processing: 10.000 URL'yi 200'lü gruplar halinde işler, bellek patlamasını önler.
- Circuit breaker: Üst üste 5 hata alırsa 30 saniye duraklar, hedef siteyi rahatlatır.
- JSONL çıktı: Her satır bir JSON nesnesi — hata durumunda bile veri kaybı olmaz, akış şeklinde yazılır.
- Rastgele ülke rotasyonu: Her istek farklı bir ülkeden gelir, IP bloklarını aşar.
Hata Yönetimi: 403/429 ve Devre Kesici Deseni
Ölçekli kazımada hatalar kaçınılmazdır. Önemli olan, hataları nasıl dönüştürdüğünüzdür. Karşılaşacağınız temel HTTP durum kodları ve stratejileri:
403 Forbidden
Hedef site, IP'nizi veya istek kalıbınızı tanımış. Çözüm: proxy rotasyonu ile IP'yi değiştirin. Eğer kalıcı olarak 403 alıyorsanız, User-Agent ve başlıklarınızı çeşitlendirin. Residential proxy'ler, datacenter IP'lerden çok daha az engellenir çünkü gerçek İSS'lerden gelir.
429 Too Many Requests
Rate limit'e takılmışsınız. Çözüm: üstel geri çekilme ile bekleyin ve yeni bir proxy IP ile yeniden deneyin. Eşzamanlılık (concurrency) oranınızı düşürmeyi de düşünün. Rate limit yönetimi hakkında ayrıntılı rehberimize göz atabilirsiniz.
503 Service Unavailable
Sunucu geçici olarak yanıt veremiyor. Kısa bir bekleme sonrası yeniden deneyin. Üst üste 503'ler, hedef sunucunun yük altında olduğuna işaret edebilir — circuit breaker devreye girmelidir.
ENOTFOUND / ECONNRESET
DNS çözümleme veya bağlantı sıfırlama hataları. Proxy'niz geçici olarak yanıt vermiyor olabilir. Farklı bir proxy ile yeniden deneyin.
Devre kesici (circuit breaker) deseni, sistemin kendini iyileştirmesini sağlar: hata oranı belirli bir eşiği aştığında istekleri duraklatır, hedef siteyi rahatlatır ve ardından kademeli olarak yeniden başlatır. Yukarıdaki örnekteki CircuitBreaker sınıfı bu amaca hizmet eder.
Ölçeklendirme: Containerization ve Headless Filo
Tek bir Node.js işlemi, Cheerio ile saniyede 50–200 sayfa çekebilir. Ancak 100.000+ URL'lik iş yükleri için yatay ölçeklendirme gerekir. Temel stratejiler:
- Docker container'ları: Her container bir batch işler. URL listesini Redis kuyruğundan tüketir, sonuçları S3'e yazar.
p-limitile container içi eşzamanlılık kontrol edilir. - Job queue: BullMQ veya Kafka ile URL'leri kuyruğa atın. Her worker bir batch alır, işler, sonucu bildirir. Başarısız olan işler otomatik olarak yeniden kuyruğa eklenir.
- Graceful shutdown:
SIGTERMsinyalini dinleyin, aktif isteklerin tamamlanmasını bekleyin, sonuçları diske yazın ve ardından process'i kapatın. Bu, container yeniden başlatmalarında veri kaybını önler.
Headless tarayıcı filosu gerektiğinde (SPA siteler), Puppeteer + puppeteer-cluster ile browser instance'larını havuzlayın. Cheerio ile işleyemediğiniz sayfaları bu filoya yönlendirin. Web scraping kullanım senaryoları sayfamızda farklı senaryolara göre araç seçimini inceleyebilirsiniz.
ProxyHat İpucu: Residential proxy'ler, e-ticaret ve SERP kazımada engellenme oranını %95'ten %5'in altına düşürür. ProxyHat fiyatlandırması ile ihtiyacınıza uygun planı seçin. Ücretsiz deneme ile başlayabilirsiniz.
Temel Çıkarımlar
- Cheerio + axios, statik HTML'li siteler için en hafif ve hızlı kazıma yöntemidir — headless tarayıcıya gerek yoktur.
- Proxy rotasyonunu bir axios interceptor ile soyutlayın; iş mantığınız proxy detaylarından temiz kalır.
- p-limit ile eşzamanlılığı sınırlayın; sınırsız paralellik IP engellemesine ve rate limit'e yol açar.
- 403/429 hataları, proxy değişikliği ve exponential backoff ile yönetilir; circuit breaker ile sistemin kendini korumasını sağlayın.
- Batch processing ve JSONL çıktısı, büyük veri setlerinde bellek yönetimini ve hata toleransını garanti eder.
- Residential proxy'ler, datacenter IP'lere kıyasla engellenme riskini dramatik şekilde azaltır; e-ticaret ve SERP kazımada tercih edilmelidir.
Cheerio ile proxy destekli kazıma pipeline'ınızı kurmak için ProxyHat lokasyonları sayfasından desteklenen ülkeleri inceleyin ve dashboard üzerinden hesabınızı oluşturun. İlk 1 GB trafik ücretsiz.






