Dlaczego Cheerio + axios to idealny duet do skrapingu
Zaczynasz pisać scraper w Node.js i od razu napotykasz blokady: rate limiting, CAPTCH-e, bany IP. Zamiast odpalać ciężkie instancje Puppeteer dla każdej strony, możesz parsować HTML bezpośrednio na serwerze — szybko, lekko i z pełną kontrolą nad rotacją proxy.
Cheerio to implementacja jQuery dla Node.js, która działa na surowym HTML-u. Nie uruchamia przeglądarki, nie renderuje CSS ani JS — po prostu wczytuje drzewo DOM i pozwala wybierać elementy selektorami, do których jesteś przyzwyczajony. axios to de facto standard klienta HTTP w ekosystemie Node.js, z wbudowanym wsparciem dla interceptorów — idealnym mechanizmem do wstrzykiwania rotacji proxy.
Razem dają Ci stack, który:
- Parsuje 100+ stron na sekundę na pojedynczym rdzeniu
- Kosztuje ułamek tego, co headless browser
- Skaluje się horyzontalnie w kontenerach Docker
- Integruje proxy jako middleware — bez hacków
W tym przewodniku zbudujesz od zera scraper e-commerce, który obsługuje 10 000 URL-i z rotacją residential proxy, circuit breakerem i limitami współbieżności.
Podstawy: axios + Cheerio — parsowanie HTML po stronie serwera
Zanim dodamy proxy, zobaczmy minimalny scraper, który pobiera stronę i wyciąga dane z jej statycznego HTML.
import axios from 'axios';
import * as cheerio from 'cheerio';
async function scrapeProduct(url) {
const { data: html } = await axios.get(url);
const $ = cheerio.load(html);
return {
title: $('h1.product-title').text().trim(),
price: parseFloat(
$('span.price').text().replace(/[^0-9.,]/g, '').replace(',', '.')
),
availability: $('span.stock').text().trim(),
images: $('div.gallery img')
.map((_, el) => $(el).attr('src'))
.get(),
};
}
const product = await scrapeProduct('https://shop.example.com/product/123');
console.log(product);
To wszystko — żadnej przeglądarki, żadnego renderowania. Cheerio wczytuje HTML w milisekundach, a selektory jQuery robią resztę. Jeśli strona serwuje pełny HTML w odpowiedzi HTTP, nie potrzebujesz niczego więcej.
Integracja proxy z axios
Kiedy scrapujesz w skali, jedno IP szybko trafi na rate limit. Rozwiązanie: residential proxy z rotacją. Z ProxyHat masz dostęp do puli milionów IP z całego świata.
Metoda 1: https-proxy-agent
Node.js nie wspiera proxy w axios natywnie — potrzebujesz agenta:
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 } = await axios.get('https://shop.example.com/product/123', {
httpsAgent: proxyAgent,
});
Metoda 2: natywna konfiguracja proxy w axios ≥ 1.x
Od wersji 1.x axios wspiera opcję proxy bez dodatkowych zależności — ale tylko dla HTTP, nie HTTPS. Dla HTTPS nadal polecam https-proxy-agent.
const { data } = await axios.get('http://httpbin.org/ip', {
proxy: {
host: 'gate.proxyhat.com',
port: 8080,
auth: { username: 'user-country-US', password: 'PASSWORD' },
},
});
Dla produkcyjnych scraperów HTTPS, https-proxy-agent jest bezpieczniejszym wyborem — obsługuje CONNECT tunelowanie i działa stabilnie z residential proxy.
Kiedy Cheerio wystarczy, a kiedy potrzebujesz headless browsera
To kluczowe pytanie, które oszczędzi Ci godziny debugowania. Oto prosta zasada:
| Kryterium | Cheerio + axios | Headless browser (Puppeteer/Playwright) |
|---|---|---|
| HTML w odpowiedzi HTTP | ✅ Tak | ⚠️ Przerost formy |
| Dane w inline <script> (JSON-LD, __NEXT_DATA__) | ✅ Parsuj z Cheerio | ⚠️ Możliwe, ale wolne |
| Renderowanie po JS (React SPA, Vue) | ❌ Nie zobaczysz DOM | ✅ Czekaj na render |
| Infinite scroll / lazy load | ❌ Brak scrollowania | ✅ Symuluj scroll |
| Logowanie przez OAuth / CAPTCHA v3 | ❌ Nie wyklikasz | ✅ Możliwe z pluginami |
| Zależności zewn. (API XHR) | ✅ Wywołaj API bezpośrednio | ⚠️ Przechwytuj w network |
Pro tip: Zanim odpalisz Puppeteer, sprawdź curl URL w terminalu. Jeśli widzisz dane w źródle strony — Cheerio wystarczy. Jeśli HTML jest pustą shellką z <div id="app"> — potrzebujesz browsera.
Rotująca pula residential proxy jako interceptor axios
Teraz przechodzimy do właściwej architektury. Zamiast wstrzykiwać proxy ręcznie w każdym requeście, zbudujesz axios interceptor, który automatycznie rotuje IP między żądaniami.
ProxyHat pozwala sterować geolokacją i sesją przez username — nie potrzebujesz puli adresów, po prostu zmieniasz parametry w username:
user-country-US— losowe IP z USAuser-country-DE-city-berlin— losowe IP z Berlinauser-session-abc123— sticky session (to samo IP przez cały czas życia sesji)
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
// ─── Konfiguracja ────────────────────────────────────
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;
const PROXY_USER = 'YOUR_USERNAME';
const PROXY_PASS = 'YOUR_PASSWORD';
let requestCounter = 0;
function buildProxyAgent(country = 'US', sessionId = null) {
let username = `${PROXY_USER}-country-${country}`;
if (sessionId) {
username += `-session-${sessionId}`;
}
const proxyUrl = `http://${username}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`;
return new HttpsProxyAgent(proxyUrl);
}
// ─── Axios instance z interceptorami ─────────────────
const client = axios.create({ timeout: 15000 });
// Interceptor: wstrzykuj rotujący proxy agent
client.interceptors.request.use((config) => {
requestCounter += 1;
const country = config.metadata?.country ?? 'US';
const sticky = config.metadata?.sticky ?? false;
const sessionId = sticky
? config.metadata.sessionId ?? `s${requestCounter}`
: undefined;
config.httpsAgent = buildProxyAgent(country, sessionId);
config.httpAgent = buildProxyAgent(country, sessionId);
return config;
});
// Interceptor: loguj zmianę IP
client.interceptors.response.use(
(res) => {
console.log(`[${res.config.metadata?.label ?? '?'}] ✓ ${res.status}`);
return res;
},
(err) => {
console.error(
`[${err.config?.metadata?.label ?? '?'}] ✗ ${err.response?.status ?? err.code}`
);
return Promise.reject(err);
}
);
// ─── Użycie ──────────────────────────────────────────
// Każdy request = nowy IP (rotacja per-request)
await client.get('https://httpbin.org/ip', {
metadata: { country: 'DE', label: 'test-de' },
});
// Sticky session — to samo IP przez całą sesję zakupową
await client.get('https://shop.example.com/cart', {
metadata: { country: 'US', sticky: true, sessionId: 'cart-42', label: 'cart' },
});
Zauważ wzór: bez sessionId dostajesz losowe IP per request (idealne do SERP scraping). Z sessionId dostajesz sticky session (idealne do multi-step flows — koszyk, checkout).
Współbieżne skrapowanie z p-limit
Skrapowanie 10 000 URL-i po kolei trwałoby godzinami. Potrzebujesz współbieżności — ale nie za dużo, bo proxy i serwer docelowy mają swoje limity.
p-limit to minimalistyczna biblioteka, która ogranicza liczbę jednocześnie wykonywanych Promise'ów. Prostsza niż p-queue, idealna do scrapingu.
import pLimit from 'p-limit';
const CONCURRENCY = 20; // 20 równoległych requestów
const limit = pLimit(CONCURRENCY);
const urls = Array.from({ length: 10000 }, (_, i) =>
`https://shop.example.com/product/${i + 1}`
);
const results = await Promise.all(
urls.map((url) =>
limit(async () => {
try {
const { data: html } = await client.get(url, {
metadata: { country: 'US', label: url },
});
const $ = cheerio.load(html);
return {
url,
title: $('h1.product-title').text().trim(),
price: parseFloat($('span.price').text().replace(/[^0-9.]/g, '')),
};
} catch (err) {
return { url, error: err.response?.status ?? err.code };
}
})
)
);
const succeeded = results.filter((r) => !r.error);
const failed = results.filter((r) => r.error);
console.log(`✓ ${succeeded.length} | ✗ ${failed.length}`);
Dlaczego 20? To zależy od target site i Twojego planu proxy. Residential proxy radzą sobie z 50-100 req/s, ale agresywny concurrency może triggerować rate limit na stronie. Zacznij od 20, monitoruj błędy 429, i zwiększaj.
Pełny przykład: scraping e-commerce na 10 000 URL-i z rotacją proxy
Teraz połączymy wszystko w jeden produkcyjny scraper. Dodajemy circuit breaker, retry z rotacją IP i zapis wyników.
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as cheerio from 'cheerio';
import pLimit from 'p-limit';
import { writeFileSync } from 'fs';
// ─── Konfiguracja ────────────────────────────────────
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;
const PROXY_USER = 'YOUR_USERNAME';
const PROXY_PASS = 'YOUR_PASSWORD';
const CONCURRENCY = 25;
const MAX_RETRIES = 3;
const CIRCUIT_BREAK_THRESHOLD = 5; // błędów z rzędu
const CIRCUIT_BREAK_COOLDOWN = 30_000; // 30s pauzy
// ─── Circuit Breaker ─────────────────────────────────
let consecutiveErrors = 0;
let circuitOpen = false;
let circuitTimer = null;
function tripCircuit() {
circuitOpen = true;
console.error('🔴 Circuit OPEN — pauza 30s');
circuitTimer = setTimeout(() => {
circuitOpen = false;
consecutiveErrors = 0;
console.log('🟢 Circuit CLOSED — wznawiam');
}, CIRCUIT_BREAK_COOLDOWN);
}
function recordSuccess() { consecutiveErrors = 0; }
function recordError() {
consecutiveErrors += 1;
if (consecutiveErrors >= CIRCUIT_BREAK_THRESHOLD) tripCircuit();
}
// ─── Proxy agent builder ─────────────────────────────
function buildAgent(country = 'US') {
const username = `${PROXY_USER}-country-${country}`;
const url = `http://${username}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`;
return new HttpsProxyAgent(url);
}
// ─── Scraper z retry + circuit breaker ────────────────
async function scrapeWithRetry(url, attempt = 1) {
if (circuitOpen) {
await new Promise((r) => setTimeout(r, CIRCUIT_BREAK_COOLDOWN));
}
try {
const { data: html } = await axios.get(url, {
httpsAgent: buildAgent('US'),
timeout: 15000,
});
const $ = cheerio.load(html);
recordSuccess();
return {
url,
title: $('h1.product-title').text().trim(),
price: parseFloat($('span.price').text().replace(/[^0-9.]/g, '')) || null,
inStock: !$('.out-of-stock').length,
};
} catch (err) {
recordError();
const status = err.response?.status;
// 403 / 429 → rotuj IP i retry
if ((status === 403 || status === 429) && attempt < MAX_RETRIES) {
console.warn(`↻ Retry ${attempt}/${MAX_RETRIES} — ${status} — ${url}`);
await new Promise((r) => setTimeout(r, 1000 * attempt)); // backoff
return scrapeWithRetry(url, attempt + 1);
}
return { url, error: status ?? err.code };
}
}
// ─── Main ─────────────────────────────────────────────
const urls = Array.from(
{ length: 10000 },
(_, i) => `https://shop.example.com/product/${i + 1}`
);
const limit = pLimit(CONCURRENCY);
const results = await Promise.all(
urls.map((url) => limit(() => scrapeWithRetry(url)))
);
writeFileSync('results.json', JSON.stringify(results, null, 2));
const ok = results.filter((r) => !r.error);
const fail = results.filter((r) => r.error);
console.log(`✓ Sukces: ${ok.length} | ✗ Błędy: ${fail.length}`);
Ten scraper automatycznie:
- Rotuje IP per request przez residential proxy ProxyHat
- Ponawia przy 403/429 z exponential backoff i nowym IP
- Otwiera circuit breaker po 5 błędach z rzędu — daje serwerowi czas na oddech
- Zapisuje wyniki do JSON, nawet jeśli część requestów zawiedzie
Obsługa błędów: 403, 429 i circuit breaking — wzorce produkcyjne
Błędy HTTP to nie wyjątki — to część normalnego scrapingu. Klucz to odróżnić błędy, które znikną po zmianie IP (403, 429), od błędów strukturalnych (404, 500).
Mapowanie strategii obsługi błędów
| Kod HTTP | Przyczyna | Strategia |
|---|---|---|
| 403 Forbidden | IP zbanowany / WAF | Rotuj IP, retry z nowym agentem |
| 429 Too Many Requests | Rate limit | Backoff + rotacja IP + zmniejsz concurrency |
| 404 Not Found | Strona usunięta | Zapisz jako błąd, nie retry |
| 500/502/503 | Serwer niedostępny | Retry z backoff, nie zmieniaj IP |
| Timeout (ECONNABORTED) | Wolny proxy / serwer | Retry z dłuższym timeoutem |
Circuit breaker — dlaczego jest niezbędny
Bez circuit breakera, jeśli serwer zablokuje całą Twoją pulę IP, scraper będzie bezsensownie retry'ował 10 000 URL-i. Circuit breaker wykrywa kaskadę błędów, pauzuje i wznawia po cooldownie — oszczędzając bandwidth proxy i czas.
Wzór z poprzedniego przykładu jest prosty: licznik błędów z rzędu → próg → pauza → reset. W produkcji warto dodać logikę, która przy 403/429 zmienia też country w parametrach proxy — czasami inna geolokacja omija blokadę.
Skalowanie: konteneryzacja i headless fleet
Pojedynczy proces Node.js z p-limit(25) przetworzy 10 000 URL-i w ~7 minut (zakładając ~200ms per request). Co jeśli masz 100 000 URL-i?
- Podziel listę URL-i na chunki po 10 000 i odpal osobny kontener na każdy chunk.
- Użyj
countryrotation — każdy kontener scrapuje z innej geolokacji (country-US,country-DE,country-JP), rozkładając load na różne IP pools. - Sticky sessions per kontener — jeśli target wymaga sesji, przypisz
sessionIdzprocess.env.CONTAINER_ID. - Redis jako kolejka — zamiast hardcoded array, użyj Redis list jako kolejki URL-i. Worker'y biorą po jednym URL-u, oznaczają jako done.
// docker-compose.yml — 3 workery, każdy z inną geolokacją
services:
scraper-us:
build: .
environment:
- COUNTRY=US
- REDIS_URL=redis://redis:6379
scraper-de:
build: .
environment:
- COUNTRY=DE
- REDIS_URL=redis://redis:6379
scraper-jp:
build: .
environment:
- COUNTRY=JP
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
Każdy worker czyta z tej samej kolejki Redis, ale scrapuje z IP z innego kraju — naturalny load balancing i geo-diversity.
Key Takeaways
Cheerio wystarczy, gdy HTML jest w odpowiedzi HTTP. Sprawdź
curl— jeśli widzisz dane, nie potrzebujesz Puppeteer.Rotacja proxy to interceptor, nie hack. Axios interceptory pozwalają wstrzykiwać nowy
HttpsProxyAgentper request — czysta architektura, testowalny kod.ProxyHat rotuje IP przez username. Zmień
user-country-XXdla nowej geolokacji, dodaj-session-IDdla sticky session — bez zarządzania pulą adresów.Circuit breaker jest obowiązkowy. Bez niego scraper wali w ścianę po rate limit i marnuje bandwidth proxy. 5 błędów → pauza 30s → resume.
p-limit > p-queue do prostego scrapingu. Jeśli nie potrzebujesz eventów czy priorytetów,
p-limitjest wystarczający i lżejszy.
FAQ
Czym się różni Cheerio od Puppeteer do scrapingu?
Cheerio parsuje statyczny HTML bez renderowania JavaScript — jest 10-50x szybszy i zużywa ułamek pamięci. Puppeteer uruchamia pełną przeglądarkę Chromium i renderuje JS. Używaj Cheerio, gdy dane są w surowym HTML; Puppeteer, gdy strona wymaga JS do wyrenderowania treści.
Jak rotować IP w axios bez manualnego zarządzania listą proxy?
Z ProxyHat nie potrzebujesz listy IP. Zamiast tego zmieniasz parametry w username: user-country-US daje losowe IP z USA, user-country-DE-city-berlin daje IP z Berlina. Każdy request z nowym parametrem = nowe IP. W axios wystarczy stworzyć nowy HttpsProxyAgent z odpowiednim username.
Jak obsłużyć błąd 429 Too Many Requests przy scrapingu?
Exponential backoff + rotacja IP. Przy 429 zrób pauzę (1s, 2s, 4s…), zmień IP przez nowy parametr w username proxy, i ponów request. Jeśli 429 powtarza się na wielu IP — zmniejsz concurrency (np. z 25 na 10) i dodaj losowy jitter między requestami.
Czy mogę używać sticky sessions z Cheerio i ProxyHat?
Tak. Dodaj -session-TWOJ_ID do username proxy, np. user-country-US-session-cart42. Wszystkie requesty z tym samym sessionId przejdą przez to samo IP — przydatne do koszyków, formularzy logowania i multi-step flows.
Ile concurrent requestów mogę wysłać przez residential proxy?
Zależy od planu i targetu. Z ProxyHat residential proxy zacznij od 20-50 concurrent connections. Monitoruj success rate — jeśli spada poniżej 90%, zmniejsz concurrency lub zwiększ delay między requestami. 10 000 URL-i przy concurrency 25 i success rate 95% zajmie ~7-10 minut.






